Files
cinny/src/app/hooks/useUserPresence.ts
T
jared 21276a47fc
CI / Build & Quality Checks (push) Successful in 10m45s
CI / Trigger Desktop Build (push) Successful in 14s
fix(audit): low-tail cleanup — session/logout/unread/presence/forward
Clears the clean 🟡 remainders from the feature audit (gate-green, 677 tests):
- F3: getFallbackSession prefers the session-blob/legacy source with the later
  expiresAt (a downgrade→upgrade could boot on a stale blob's dead token).
- F6: server-forced logout (SessionLoggedOut) now mirrors logoutClient —
  pushSessionToSW() + best-effort revokeOidcTokens for OIDC sessions (the search
  plaintext wipe was already added).
- N5: deleteUnreadInfo parent fallback `?? roomId` → `?? []` (latently spread the
  roomId string into chars).
- P10: useUserPresence re-seeds when the User object appears after first render.
- forward: strip m.mentions so forwarding doesn't re-ping the original mentions.

Left open: F5 (OIDC expiry not reachable in persistTokens), N6/H10/D7 (minor /
runtime-verify). See LOTUS_TODO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:57:09 -04:00

71 lines
2.5 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { User, UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
export enum Presence {
Online = 'online',
Unavailable = 'unavailable',
Offline = 'offline',
}
export type UserPresence = {
presence: Presence;
status?: string;
active: boolean;
lastActiveTs?: number;
};
const getUserPresence = (user: User): UserPresence => ({
presence: user.presence as Presence,
status: user.presenceStatusMsg,
active: user.currentlyActive,
lastActiveTs: user.getLastActiveTs(),
});
export const useUserPresence = (userId: string): UserPresence | undefined => {
const mx = useMatrixClient();
const user = mx.getUser(userId);
const [presence, setPresence] = useState(() => (user ? getUserPresence(user) : undefined));
useEffect(() => {
// Re-seed when the User object appears/changes after first render — the
// useState initializer only ran if `user` already existed at mount, so a
// late-arriving user would otherwise show no presence until the next event.
if (user) setPresence(getUserPresence(user));
// Subscribe on mx (MatrixClient) rather than on individual User objects.
// User objects have a default 10-listener limit; the same user can appear
// in many components simultaneously (avatars, member list, etc.) and
// per-user subscription causes MaxListenersExceededWarning at 11+.
const updatePresence: UserEventHandlerMap[UserEvent.Presence] = (event, u) => {
if (u.userId === user?.userId) {
setPresence(getUserPresence(u));
}
};
mx.on(UserEvent.Presence, updatePresence);
mx.on(UserEvent.CurrentlyActive, updatePresence);
mx.on(UserEvent.LastPresenceTs, updatePresence);
return () => {
mx.removeListener(UserEvent.Presence, updatePresence);
mx.removeListener(UserEvent.CurrentlyActive, updatePresence);
mx.removeListener(UserEvent.LastPresenceTs, updatePresence);
};
}, [mx, user]);
return presence;
};
export const usePresenceLabel = (): Record<Presence, string> =>
useMemo(
// Keep this vocabulary aligned with the status setter (PresencePicker in
// SettingsTab.tsx): online -> "Online", unavailable -> "Idle". Previously
// these read "Active"/"Busy"/"Away", so a user who set "Idle" showed as
// "Busy" to others.
() => ({
[Presence.Online]: 'Online',
[Presence.Unavailable]: 'Idle',
[Presence.Offline]: 'Offline',
}),
[],
);