// Chat presence + real-time delivery. A logged-in user opens a WebSocket and sends // `chat-hello`; signaling.js registers the socket here. Messages are persisted over HTTP // (routes.js) and pushed live to the recipient's sockets via pushToUser(). const { chatClients, meetingRooms } = require('./presence'); let _repos = null; const repos = () => (_repos || (_repos = require('./repos'))); // lazy: avoid a require cycle function register(userId, ws) { if (!chatClients.has(userId)) chatClients.set(userId, new Set()); chatClients.get(userId).add(ws); ws._chatUserId = userId; try { repos().users.touchSeen(userId); } catch (_) {} // "last seen" (#2) } function unregister(ws) { const id = ws && ws._chatUserId; if (!id) return; const set = chatClients.get(id); if (set) { set.delete(ws); // Their LAST socket went away → stamp when they were last here, so contacts can show "last seen …". if (!set.size) { chatClients.delete(id); try { repos().users.touchSeen(id); } catch (_) {} } } } function isOnline(userId) { const s = chatClients.get(userId); return !!(s && s.size); } function pushToUser(userId, obj) { const s = chatClients.get(userId); if (!s) return; const data = JSON.stringify(obj); for (const ws of s) { if (ws.readyState === 1) { try { ws.send(data); } catch (_) {} } } } // --- Live presence ------------------------------------------------------------------------- // A user's effective status (online / in-a-call / away / …) changes with no HTTP round-trip: // they connect or disconnect a socket, or join/leave a call. Without pushing that change, OTHER // users only see it after a full page reload — impossible in the desktop/mobile apps. So whenever // it changes we broadcast the user's fresh status to everyone else's sockets, and the client // updates that contact's dot/subtitle in place. function isInCall(userId) { for (const [, peers] of meetingRooms) { for (const [, p] of peers) { if (p.ws && p.ws._meetingUserId === userId) return true; } } return false; } function effectiveStatus(userId) { if (isInCall(userId)) return 'incall'; // derived (overrides the stored status) try { const u = repos().users.byId(userId); return (u && u.status) || 'active'; } catch (_) { return 'active'; } } function broadcastPresence(userId) { if (!userId) return; // Carry last_seen too, so a contact going offline immediately reads "Last seen just now" instead of a // bare "Offline" until the next sidebar reload (#2). let lastSeen = null; try { const u = repos().users.byId(userId); lastSeen = (u && u.last_seen) || null; } catch (_) {} const payload = JSON.stringify({ type: 'presence', userId, online: isOnline(userId), status: effectiveStatus(userId), lastSeen }); for (const [uid, set] of chatClients) { if (uid === userId) continue; // no need to tell someone about their own status for (const ws of set) { if (ws.readyState === 1) { try { ws.send(payload); } catch (_) {} } } } } module.exports = { register, unregister, isOnline, pushToUser, broadcastPresence };