860a7bd6cf
#6 Emoji were slow because Twemoji swapped EVERY emoji for an <img> fetched individually from a CDN — opening the picker fired hundreds of image requests. Now uses the OS's own colour emoji font: instant, zero network. twemojify() kept as a no-op. #4 Message hover row reworked: three one-tap reactions (Like/Laugh/Surprised) + the emoji picker + (own) Edit. Reply / Forward / Copy / Delete moved behind a ⋮ menu. Added Copy. The row now sits FULLY above the bubble (was top:-14px, overlapping the first text line). #9 Phone numbers linkify to tel: — mobile gets the OS "call this number?" prompt; desktop has no dialer so it offers to copy. Regex kept conservative (10–15 digits, needs +/grouping) so it won't grab amounts, dates or 6-digit meeting codes. #3 Image preview zooms: wheel + pinch + double-click + ± buttons, drag to pan, keys (+/-/0), cursor-anchored. Arrows hide while zoomed so panning isn't hijacked. #8 Upload progress: fetch() can't report upload progress at all, so a large file just said "uploading…". Switched to XHR (upload.onprogress) → real bar + %, and cancel aborts in flight. #1 Clicking a sender in a group opens a mini profile card (photo, presence, last seen) with a Message button that opens the 1:1 (and a view-photo button). #2 Last seen: new users.last_seen column, stamped on connect and when the last socket drops; carried on the presence broadcast, so an offline contact reads "Last seen 10 minutes ago" instead of a bare "Offline". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
3.0 KiB
JavaScript
65 lines
3.0 KiB
JavaScript
// 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 };
|