diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 7d95b520d..3fb8d231b 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -19,6 +19,7 @@ import { getMxIdLocalPart } from '../../utils/matrix'; import { BreakWord, LineClamp2, LineClamp3 } from '../../styles/Text.css'; import { ModalMobileFull } from '../../styles/Modal.css'; import { UserPresence } from '../../hooks/useUserPresence'; +import { describeLastActive } from '../../utils/lastActive'; import { AvatarPresence, PresenceBadge } from '../presence'; import { AvatarDecoration } from '../avatar-decoration/AvatarDecoration'; import { ImageViewer } from '../image-viewer'; @@ -110,6 +111,8 @@ type UserHeroNameProps = { status?: string; pronouns?: string; timezone?: string; + /** [Gitea #150] Presence for the "Active now / Last active …" line. */ + presence?: UserPresence; }; export function UserHeroName({ displayName, @@ -117,7 +120,9 @@ export function UserHeroName({ status, pronouns, timezone, + presence, }: UserHeroNameProps) { + const lastActive = describeLastActive(presence); const username = getMxIdLocalPart(userId); const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); const localTimeInfo = useLocalTime(timezone, !hour24Clock); @@ -158,6 +163,13 @@ export function UserHeroName({ )} + {lastActive && ( + + + {lastActive} + + + )} {status && ( diff --git a/src/app/utils/lastActive.test.ts b/src/app/utils/lastActive.test.ts new file mode 100644 index 000000000..765687df6 --- /dev/null +++ b/src/app/utils/lastActive.test.ts @@ -0,0 +1,53 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { describeLastActive } from './lastActive'; + +const now = 1_800_000_000_000; +test('wording buckets', () => { + assert.equal(describeLastActive(undefined), null); + assert.equal(describeLastActive({ presence: 'offline', active: false }), null); + assert.equal(describeLastActive({ presence: 'offline', active: false, lastActiveTs: 0 }), null); + assert.equal( + describeLastActive({ presence: 'online', active: true, lastActiveTs: now }, now), + 'Active now', + ); + assert.equal( + describeLastActive({ presence: 'online', active: false, lastActiveTs: now - 20_000 }, now), + 'Active just now', + ); + assert.equal( + describeLastActive( + { presence: 'unavailable', active: false, lastActiveTs: now - 12 * 60_000 }, + now, + ), + 'Last active 12 min ago', + ); + assert.equal( + describeLastActive( + { presence: 'offline', active: false, lastActiveTs: now - 3 * 3_600_000 }, + now, + ), + 'Last active 3 hours ago', + ); + assert.equal( + describeLastActive( + { presence: 'offline', active: false, lastActiveTs: now - 26 * 3_600_000 }, + now, + ), + 'Last active yesterday', + ); + assert.equal( + describeLastActive( + { presence: 'offline', active: false, lastActiveTs: now - 5 * 86_400_000 }, + now, + ), + 'Last active 5 days ago', + ); + assert.equal( + describeLastActive( + { presence: 'offline', active: false, lastActiveTs: now - 40 * 86_400_000 }, + now, + ), + 'Last active over a month ago', + ); +}); diff --git a/src/app/utils/lastActive.ts b/src/app/utils/lastActive.ts new file mode 100644 index 000000000..65236a219 --- /dev/null +++ b/src/app/utils/lastActive.ts @@ -0,0 +1,24 @@ +/** + * [Gitea #150] "Active now" / "Last active 12 min ago" from presence. + * Returns null when there is nothing honest to say: no presence data, an + * offline user with no timestamp (e.g. someone hiding their online status). + */ +export const describeLastActive = ( + presence: { presence: string; active: boolean; lastActiveTs?: number } | undefined, + now = Date.now(), +): string | null => { + if (!presence) return null; + if (presence.presence === 'online' && presence.active) return 'Active now'; + const ts = presence.lastActiveTs; + if (!ts || ts <= 0) return null; + const ago = Math.max(0, now - ts); + const min = Math.round(ago / 60_000); + if (min < 1) return 'Active just now'; + if (min < 60) return `Last active ${min} min ago`; + const hours = Math.round(min / 60); + if (hours < 24) return `Last active ${hours} ${hours === 1 ? 'hour' : 'hours'} ago`; + const days = Math.round(hours / 24); + if (days === 1) return 'Last active yesterday'; + if (days < 30) return `Last active ${days} days ago`; + return 'Last active over a month ago'; +};