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';
+};