95 lines
2.7 KiB
TypeScript
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);
|
|
};
|
|
}
|