feat(chat): emoji speed, action rework, copy, phone links, image zoom, upload %, profile card, last seen (batch82)

#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>
This commit is contained in:
2026-07-14 23:46:52 +05:30
parent 667b4c69d5
commit 860a7bd6cf
5 changed files with 279 additions and 33 deletions
+11 -2
View File
@@ -8,13 +8,18 @@ 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); if (!set.size) chatClients.delete(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) {
@@ -45,7 +50,11 @@ function effectiveStatus(userId) {
}
function broadcastPresence(userId) {
if (!userId) return;
const payload = JSON.stringify({ type: 'presence', userId, online: isOnline(userId), status: effectiveStatus(userId) });
// 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 (_) {} } }