/** * Shared presence broadcaster for rApps. * * Any rApp component can call broadcastPresence() or startPresenceHeartbeat() * to announce the user's current module and context to all peers in the space. * The collab overlay listens for these messages and displays module context * in the people panel. */ // ── Identity helpers ── function getSessionInfo(): { username: string; userId: string } { try { const sess = JSON.parse(localStorage.getItem('encryptid_session') || '{}'); return { username: sess?.username || sess?.displayName || 'Anonymous', userId: sess?.userId || sess?.sub || 'anon', }; } catch { return { username: 'Anonymous', userId: 'anon' }; } } function userColor(id: string): string { let hash = 0; for (let i = 0; i < id.length; i++) { hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0; } const hue = Math.abs(hash) % 360; return `hsl(${hue}, 70%, 50%)`; } // ── Public API ── export interface PresenceOpts { module: string; context?: string; // human-readable label (e.g. "Notebook > Note Title") notebookId?: string; // module-specific metadata noteId?: string; // module-specific metadata itemId?: string; // generic item ID } /** * Broadcast current user's presence to all peers in the space. * Safe to call even if runtime isn't ready — silently no-ops. */ export function broadcastPresence(opts: PresenceOpts): void { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized || !runtime.isOnline) return; const session = getSessionInfo(); runtime.sendCustom({ type: 'presence', module: opts.module, context: opts.context || '', notebookId: opts.notebookId, noteId: opts.noteId, itemId: opts.itemId, username: session.username, color: userColor(session.userId), }); } /** * Broadcast a leave signal so peers can immediately remove us. */ export function broadcastLeave(): void { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized || !runtime.isOnline) return; const session = getSessionInfo(); runtime.sendCustom({ type: 'presence-leave', username: session.username, }); } /** * Start a 10-second heartbeat that broadcasts presence. * Returns a cleanup function to stop the heartbeat. * * @param getOpts - Called each heartbeat to get current presence state */ export function startPresenceHeartbeat(getOpts: () => PresenceOpts): () => void { broadcastPresence(getOpts()); const timer = setInterval(() => broadcastPresence(getOpts()), 10_000); // Clean up immediately on page unload const onUnload = () => broadcastLeave(); window.addEventListener('beforeunload', onUnload); return () => { clearInterval(timer); window.removeEventListener('beforeunload', onUnload); }; }