export interface ComradePresence { user: string; page: string; online: boolean; ts: number; } export interface NotesTyping { user: string; active: boolean; ts: number; } export interface PresenceState { comrades: Record; notesTyping: NotesTyping | null; selfUser: string | null; } export const initialPresenceState: PresenceState = { comrades: {}, notesTyping: null, selfUser: null, }; export type PresenceAction = | { type: 'set_self'; user: string | null } | { type: 'presence_snapshot'; comrades: ComradePresence[] } | { type: 'presence_update'; user: string; page: string; online: boolean; ts: number } | { type: 'notes_typing'; user: string; active: boolean; ts: number } | { type: 'clear_notes_typing' }; export function reducePresence(state: PresenceState, action: PresenceAction): PresenceState { switch (action.type) { case 'set_self': return { ...state, selfUser: action.user }; case 'presence_snapshot': { const comrades: Record = {}; for (const c of action.comrades) { if (!c.user || !c.online) continue; if (state.selfUser && c.user === state.selfUser) continue; comrades[c.user] = { ...c, online: true }; } return { ...state, comrades }; } case 'presence_update': { const next = { ...state.comrades }; if (!action.online || (state.selfUser && action.user === state.selfUser)) { delete next[action.user]; return { ...state, comrades: next }; } next[action.user] = { user: action.user, page: action.page || '/dashboard', online: true, ts: action.ts, }; return { ...state, comrades: next }; } case 'notes_typing': { if (state.selfUser && action.user === state.selfUser) { return state; } if (!action.active) { if (state.notesTyping?.user === action.user) { return { ...state, notesTyping: null }; } return state; } return { ...state, notesTyping: { user: action.user, active: true, ts: action.ts }, }; } case 'clear_notes_typing': return { ...state, notesTyping: null }; default: return state; } } export function onlineComrades(state: PresenceState): ComradePresence[] { return Object.values(state.comrades).filter((c) => c.online); } export function comradesOnPage(state: PresenceState, page: string): ComradePresence[] { const normalized = page.startsWith('/') ? page : `/${page}`; return onlineComrades(state).filter((c) => { const p = c.page.startsWith('/') ? c.page : `/${c.page}`; return p === normalized; }); }