Files
AetherForge/server/web/src/context/presenceReducer.ts
AetherForge a32860b0d9
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
feat: alive UI wave, galaxy presence, spread and fleet enhancements
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
2026-06-04 22:36:17 -07:00

93 lines
2.7 KiB
TypeScript

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<string, ComradePresence>;
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<string, ComradePresence> = {};
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;
});
}