Add filter for last online

This commit is contained in:
MartinBraquet
2026-02-27 12:53:52 +01:00
parent 8c1a75e26b
commit 59d52d4c11
8 changed files with 149 additions and 4 deletions

View File

@@ -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",

View File

@@ -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`

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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<FilterFields> = {
big5_agreeableness_max: undefined,
big5_neuroticism_min: undefined,
big5_neuroticism_max: undefined,
last_active: undefined,
orderBy: 'created_time',
}

View File

@@ -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 */}
<CustomizeableDropdown
buttonContent={(open: boolean) => (
<DropdownButton
open={open}
content={
<LastActiveFilterText
last_active={filters.last_active}
highlightedClass={open ? 'text-primary-500' : ''}
/>
}
/>
)}
dropdownMenuContent={(close) => (
<Col className="mx-2 mb-4">
<LastActiveFilter filters={filters} updateFilter={updateFilter} close={close} />
</Col>
)}
popoverClassName="bg-canvas-50"
menuWidth="w-50"
/>
</>
)
}

View File

@@ -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 (
<Row className="items-center gap-0.5">
<span className={highlightedClass}>
{t('filter.last_active.label', 'Active')}: {label}
</span>
</Row>
)
}
export function LastActiveFilter(props: {
filters: Partial<FilterFields>
updateFilter: (newState: Partial<FilterFields>) => void
close?: () => void
}) {
const {filters, updateFilter, close} = props
return (
<DropdownOptions
items={INVERTED_LAST_ONLINE_CHOICES}
activeKey={filters.last_active || 'any'}
onClick={(option) => {
updateFilter({last_active: option === 'any' ? undefined : option})
close?.()
}}
/>
)
}
export function DropdownOptions(props: {
items: Record<string, any>
onClick: (item: any) => void
activeKey: string
}) {
const {items, onClick, activeKey} = props
return (
<Col className={'w-[150px]'}>
{Object.entries(items).map(([key, item]) => (
<div key={key}>
<button
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
onClick(key)
}}
className={clsx(
key == activeKey ? 'bg-primary-100' : 'hover:bg-canvas-100 hover:text-ink-900',
'text-ink-700',
'flex w-full gap-2 px-4 py-2 text-left text-sm rounded-md',
)}
>
{item.icon && <div className="w-5">{item.icon}</div>}
{item.label ?? item}
</button>
</div>
))}
</Col>
)
}

View File

@@ -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: {
>
<EducationFilter filters={filters} updateFilter={updateFilter} />
</MobileFilterSection>
{/* LAST ACTIVE */}
<MobileFilterSection
title={t('filter.last_active.title', 'Last active')}
openFilter={openFilter}
setOpenFilter={setOpenFilter}
isActive={!!filters.last_active}
selection={
<LastActiveFilterText
last_active={filters.last_active}
highlightedClass={filters.last_active ? 'text-primary-600' : 'text-ink-900'}
/>
}
>
<LastActiveFilter filters={filters} updateFilter={updateFilter} />
</MobileFilterSection>
</Col>
)
}