feat(chat): live typing indicators for DMs and groups

Ephemeral over the chat WebSocket (no DB). Composer emits chat-typing on/off
(throttled 2.5s, auto-stop after 4s idle / on send / on leaving the chat).
Server relays to the DM peer or fans out to group members (membership-checked).
Receiver shows 'typing…' in the conversation header subtitle and the sidebar
row preview (brand-blue italic), with per-sender auto-expiry so a dropped 'off'
can't stick. Group shows names ('Alice is typing…', 'N people are typing…').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 17:17:03 +05:30
parent 820e7a08fc
commit 3f791cb120
2 changed files with 99 additions and 12 deletions
+14
View File
@@ -41,6 +41,20 @@ function handle(ws, m, req) {
if (!msg.delivered_at) { R.messages.markDelivered(m.id); try { CHAT.pushToUser(msg.sender_id, { type: 'chat-delivered', id: m.id, with: msg.recipient_id }); } catch (_) {} }
break;
}
// Live "is typing…" — ephemeral, never persisted. Relay to the DM peer, or fan out to
// group members (membership-checked). The receiver auto-expires it, so a lost 'off' is harmless.
case 'chat-typing': {
const uid = ws._chatUserId; if (!uid) break;
const on = !!m.on;
let name = ''; try { const u = R.users.byId(uid); name = (u && u.name) || ''; } catch (_) {}
if (m.group) {
let members; try { if (!R.conversations.isMember(m.group, uid)) break; members = R.conversations.members(m.group); } catch (_) { break; }
for (const mid of members) { if (mid !== uid) { try { CHAT.pushToUser(mid, { type: 'chat-typing', group: m.group, from: uid, name, on }); } catch (_) {} } }
} else if (m.to) {
try { CHAT.pushToUser(m.to, { type: 'chat-typing', from: uid, name, on }); } catch (_) {}
}
break;
}
// --- Meetings (mesh): create a room, join by code, relay SDP/ICE peer-to-peer ---
case 'meeting-create': {
let code; do { code = A.numericCode(6); } while (meetingRooms.has(code));