rspace-online/shared/collab-presence.ts

95 lines
2.7 KiB
TypeScript

/**
* 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);
};
}