From 59d52d4c11b973c20a49409f2b8d19a2c7f4c408 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Fri, 27 Feb 2026 12:53:52 +0100 Subject: [PATCH] Add filter for last online --- backend/api/package.json | 2 +- backend/api/src/get-profiles.ts | 20 ++++- common/src/api/schema.ts | 3 +- common/src/choices.ts | 10 +++ common/src/filters.ts | 2 + web/components/filters/desktop-filters.tsx | 23 ++++++ web/components/filters/last-active-filter.tsx | 76 +++++++++++++++++++ web/components/filters/mobile-filters.tsx | 17 +++++ 8 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 web/components/filters/last-active-filter.tsx diff --git a/backend/api/package.json b/backend/api/package.json index 4c557141..0e501a46 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -1,7 +1,7 @@ { "name": "@compass/api", "description": "Backend API endpoints", - "version": "1.17.2", + "version": "1.18.0", "private": true, "scripts": { "watch:serve": "tsx watch src/serve.ts", diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index 69af5d3f..a9c9b432 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -59,6 +59,7 @@ export type profileQueryType = { skipId?: string | undefined orderBy?: string | undefined lastModificationWithin?: string | undefined + last_active?: string | undefined locale?: string | undefined } & { [K in OptionTableKey]?: string[] | undefined @@ -118,6 +119,7 @@ export const loadProfiles = async (props: profileQueryType) => { lastModificationWithin, skipId, locale = 'en', + last_active, } = props const filterLocation = lat && lon && radius @@ -171,7 +173,7 @@ export const loadProfiles = async (props: profileQueryType) => { ) const joins = [ - orderByParam === 'last_online_time' && leftJoin(userActivityJoin), + (orderByParam === 'last_online_time' || last_active) && leftJoin(userActivityJoin), orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin), joinInterests && leftJoin(interestsJoin), joinCauses && leftJoin(causesJoin), @@ -413,6 +415,20 @@ export const loadProfiles = async (props: profileQueryType) => { lastModificationWithin, }), + last_active && + where(`user_activity.last_online_time >= NOW() - INTERVAL $(last_active_interval)`, { + last_active_interval: + last_active === 'today' + ? '1 day' + : last_active === '3days' + ? '3 days' + : last_active === 'week' + ? '7 days' + : last_active === 'month' + ? '30 days' + : '90 days', + }), + // Exclude profiles that the requester has chosen to hide userId && where( @@ -428,7 +444,7 @@ export const loadProfiles = async (props: profileQueryType) => { let selectCols = 'profiles.*, users.name, users.username, users.data as user' if (orderByParam === 'compatibility_score') { selectCols += ', cs.score as compatibility_score' - } else if (orderByParam === 'last_online_time') { + } else if (orderByParam === 'last_online_time' || last_active) { selectCols += ', user_activity.last_online_time' } if (joinInterests) selectCols += `, COALESCE(profile_interests.interests, '{}') AS interests` diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 9b98b29c..01ec6cc0 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -18,7 +18,7 @@ import {arrify} from 'common/util/array' import {z} from 'zod' import {LikeData, ShipData} from './profile-types' -import {FullUser} from './user-types' +import {FullUser} from './user-types' // mqp: very unscientific, just balancing our willingness to accept load // mqp: very unscientific, just balancing our willingness to accept load // with user willingness to put up with stale data @@ -583,6 +583,7 @@ export const API = (_apiTypeCheck = { work: arraybeSchema.optional(), relationship_status: arraybeSchema.optional(), languages: arraybeSchema.optional(), + last_active: z.string().optional(), wants_kids_strength: z.coerce.number().optional(), has_kids: z.coerce.number().optional(), is_smoker: zBoolean.optional().optional(), diff --git a/common/src/choices.ts b/common/src/choices.ts index 9df18779..e84e2012 100644 --- a/common/src/choices.ts +++ b/common/src/choices.ts @@ -226,6 +226,15 @@ export const GENDERS_PLURAL = { Other: 'other', } +export const LAST_ONLINE_CHOICES = { + Today: 'today', + 'Last 3 days': '3days', + 'Last week': 'week', + 'Last month': 'month', + 'Last 3 months': '3months', + 'Any time': 'any', +} + export const INVERTED_RELATIONSHIP_CHOICES = invert(RELATIONSHIP_CHOICES) export const INVERTED_RELATIONSHIP_STATUS_CHOICES = invert(RELATIONSHIP_STATUS_CHOICES) export const INVERTED_ROMANTIC_CHOICES = invert(ROMANTIC_CHOICES) @@ -237,3 +246,4 @@ export const INVERTED_LANGUAGE_CHOICES = invert(LANGUAGE_CHOICES) export const INVERTED_RACE_CHOICES = invert(RACE_CHOICES) export const INVERTED_MBTI_CHOICES = invert(MBTI_CHOICES) export const INVERTED_GENDERS = invert(GENDERS) +export const INVERTED_LAST_ONLINE_CHOICES = invert(LAST_ONLINE_CHOICES) diff --git a/common/src/filters.ts b/common/src/filters.ts index ac91a2ec..81503f23 100644 --- a/common/src/filters.ts +++ b/common/src/filters.ts @@ -11,6 +11,7 @@ import {cloneDeep} from 'lodash' export type FilterFields = { orderBy: 'last_online_time' | 'created_time' | 'compatibility_score' + last_active: string | undefined geodbCityIds: string[] | null lat: number | null lon: number | null @@ -112,6 +113,7 @@ export const initialFilters: Partial = { big5_agreeableness_max: undefined, big5_neuroticism_min: undefined, big5_neuroticism_max: undefined, + last_active: undefined, orderBy: 'created_time', } diff --git a/web/components/filters/desktop-filters.tsx b/web/components/filters/desktop-filters.tsx index 78dd493f..d2150b19 100644 --- a/web/components/filters/desktop-filters.tsx +++ b/web/components/filters/desktop-filters.tsx @@ -41,6 +41,7 @@ import {DietType, RelationshipType, RomanticType} from 'web/lib/util/convert-typ import {AgeFilter, AgeFilterText} from './age-filter' import {GenderFilter, GenderFilterText} from './gender-filter' +import {LastActiveFilter, LastActiveFilterText} from './last-active-filter' import {LocationFilter, LocationFilterProps, LocationFilterText} from './location-filter' import {MyMatchesToggle} from './my-matches-toggle' import {RelationshipFilter, RelationshipFilterText} from './relationship-filter' @@ -665,6 +666,28 @@ export function DesktopFilters(props: { popoverClassName="bg-canvas-50" menuWidth="w-50" /> + + {/* LAST ACTIVE */} + ( + + } + /> + )} + dropdownMenuContent={(close) => ( + + + + )} + popoverClassName="bg-canvas-50" + menuWidth="w-50" + /> ) } diff --git a/web/components/filters/last-active-filter.tsx b/web/components/filters/last-active-filter.tsx new file mode 100644 index 00000000..a1545486 --- /dev/null +++ b/web/components/filters/last-active-filter.tsx @@ -0,0 +1,76 @@ +import clsx from 'clsx' +import {INVERTED_LAST_ONLINE_CHOICES, LAST_ONLINE_CHOICES} from 'common/choices' +import {FilterFields} from 'common/filters' +import {Row} from 'web/components/layout/row' +import {useT} from 'web/lib/locale' + +import {Col} from '../layout/col' + +export function LastActiveFilterText(props: { + last_active: string | undefined + highlightedClass?: string +}) { + const {last_active, highlightedClass} = props + const t = useT() + const option = Object.values(LAST_ONLINE_CHOICES).find((opt) => opt === last_active) + const label = + INVERTED_LAST_ONLINE_CHOICES[option ?? ''] ?? t('filter.last_active.any', 'Any time') + + return ( + + + {t('filter.last_active.label', 'Active')}: {label} + + + ) +} + +export function LastActiveFilter(props: { + filters: Partial + updateFilter: (newState: Partial) => void + close?: () => void +}) { + const {filters, updateFilter, close} = props + + return ( + { + updateFilter({last_active: option === 'any' ? undefined : option}) + close?.() + }} + /> + ) +} + +export function DropdownOptions(props: { + items: Record + onClick: (item: any) => void + activeKey: string +}) { + const {items, onClick, activeKey} = props + return ( + + {Object.entries(items).map(([key, item]) => ( +
+ +
+ ))} + + ) +} diff --git a/web/components/filters/mobile-filters.tsx b/web/components/filters/mobile-filters.tsx index 2c15c9a0..35ca246a 100644 --- a/web/components/filters/mobile-filters.tsx +++ b/web/components/filters/mobile-filters.tsx @@ -42,6 +42,7 @@ import {AgeFilter, AgeFilterText, getNoMinMaxAge} from './age-filter' import {DrinksFilter, DrinksFilterText, getNoMinMaxDrinks} from './drinks-filter' import {GenderFilter, GenderFilterText} from './gender-filter' import {HasKidsFilter, HasKidsLabel} from './has-kids-filter' +import {LastActiveFilter, LastActiveFilterText} from './last-active-filter' import {LocationFilter, LocationFilterProps, LocationFilterText} from './location-filter' import {MyMatchesToggle} from './my-matches-toggle' import {RelationshipFilter, RelationshipFilterText} from './relationship-filter' @@ -551,6 +552,22 @@ function MobileFilters(props: { > + + {/* LAST ACTIVE */} + + } + > + + ) }