feat: live presence, delivery-tick sync, brand rollout, call fixes, desktop 0.1.3
Live presence (fixes stale in-call/status until refresh — impossible in apps): - server broadcasts a user's status over the chat socket on connect/disconnect, call join/leave, and status change (chat.js broadcastPresence; signaling + routes hooks). - client onPresence() updates the contact dot + open-chat header live. Chat delivery ticks (#6): chat-list row now mirrors the thread (delivered→double grey, read→blue) via a new 'with' field on the delivered relay + onChatRead/onChatDelivered. Call fixes: no bogus 'host handed over' when a 1:1 call ends (leaveMeeting forced); branded call-connecting + chat-thread loaders; header subtitle tracks live call state. Notifications: web notify + sw.js use sender/group DP + brand icon (not old wordmark); desktop shell drops Web Push so only the single native toast fires (#5). Brand: master icon/splash/loaders wired everywhere (PWA/favicon/apple-touch/.ico), branded login (blue + gold CTA), branded toasts (BZToast) on all pages, Electron splash. Desktop: dev auto-targets localhost (packaged→prod); version 0.1.3 with new multi-size icon; dropped unused node-notifier; removed home-mockup.html. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
-3
@@ -29,6 +29,7 @@ function handle(ws, m, req) {
|
||||
ws._chatUserId = u.id; ws._chatTeamId = u.team_id;
|
||||
CHAT.register(u.id, ws);
|
||||
ws.send(JSON.stringify({ type: 'chat-ready' }));
|
||||
CHAT.broadcastPresence(u.id); // tell contacts this user just came online
|
||||
break;
|
||||
}
|
||||
// Recipient's client acknowledges a DM was delivered → mark it + tell the sender.
|
||||
@@ -37,7 +38,7 @@ function handle(ws, m, req) {
|
||||
const msg = R.messages.byId(m.id);
|
||||
if (!msg || msg.conversation_id || msg.team_id !== ws._chatTeamId) break; // DMs only
|
||||
if (msg.recipient_id !== ws._chatUserId) break; // only the recipient can ack
|
||||
if (!msg.delivered_at) { R.messages.markDelivered(m.id); try { CHAT.pushToUser(msg.sender_id, { type: 'chat-delivered', id: m.id }); } catch (_) {} }
|
||||
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;
|
||||
}
|
||||
// --- Meetings (mesh): create a room, join by code, relay SDP/ICE peer-to-peer ---
|
||||
@@ -72,6 +73,7 @@ function handle(ws, m, req) {
|
||||
// …and tell existing peers a newcomer arrived.
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) p.ws.send(JSON.stringify({ type: 'meeting-peer-joined', peerId, name, avatar, uid: ws._meetingUserId || null })); }
|
||||
peers.set(peerId, { ws, name, avatar, uid: ws._meetingUserId || null });
|
||||
if (ws._meetingUserId) CHAT.broadcastPresence(ws._meetingUserId); // now in a call → update contacts live
|
||||
const tsubs = transcriptSubs.get(room); if (tsubs && tsubs.size > 0) ws.send(JSON.stringify({ type: 'meeting-transcribe-state', active: true })); // catch up: already transcribing
|
||||
break;
|
||||
}
|
||||
@@ -286,16 +288,20 @@ function leaveMeeting(ws) {
|
||||
const peers = meetingRooms.get(room);
|
||||
ws._meetingRoom = null;
|
||||
const pid = ws._peerId;
|
||||
if (!peers) return;
|
||||
const leaverId = ws._meetingUserId;
|
||||
if (!peers) { if (leaverId) CHAT.broadcastPresence(leaverId); return; }
|
||||
try { require('./calls').finalizeTranscript(room, ws._meetingUserId); } catch (_) {} // save THIS user's transcript
|
||||
peers.delete(pid);
|
||||
// 1:1 call: when either party leaves, end it for everyone (a DM call has no "remaining" call).
|
||||
if (roomToDmCall.has(room)) {
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) { try { p.ws.send(JSON.stringify({ type: 'meeting-ended' })); } catch (_) {} p._meetingRoom = null; } }
|
||||
const others = [...peers.values()].map((p) => p.ws && p.ws._meetingUserId).filter(Boolean);
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) { try { p.ws.send(JSON.stringify({ type: 'meeting-ended' })); } catch (_) {} p.ws._meetingRoom = null; } }
|
||||
meetingRooms.delete(room);
|
||||
try { require('./calls').finalizeTranscript(room); } catch (_) {} // any remaining buffers (safety)
|
||||
roomHost.delete(room);
|
||||
try { require('./calls').endCallByRoom(room); } catch (_) {}
|
||||
if (leaverId) CHAT.broadcastPresence(leaverId);
|
||||
others.forEach((uid) => CHAT.broadcastPresence(uid)); // both parties are now out of the call → live update
|
||||
return;
|
||||
}
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) p.ws.send(JSON.stringify({ type: 'meeting-peer-left', peerId: pid })); }
|
||||
@@ -305,11 +311,15 @@ function leaveMeeting(ws) {
|
||||
roomHost.delete(room);
|
||||
try { require('./calls').endCallByRoom(room); } catch (_) {}
|
||||
}
|
||||
if (leaverId) CHAT.broadcastPresence(leaverId); // this user left the call → update contacts live
|
||||
}
|
||||
|
||||
function cleanup(ws) {
|
||||
const goneUserId = ws._chatUserId; // capture before unregister so we can announce the change
|
||||
CHAT.unregister(ws);
|
||||
leaveMeeting(ws);
|
||||
if (goneUserId) CHAT.broadcastPresence(goneUserId); // now reflects offline / no-longer-in-call
|
||||
|
||||
if (ws.kind === 'agent' && ws.machineId) onlineAgents.delete(ws.machineId);
|
||||
if (ws.kind === 'sharer' && ws.shareCode) pendingShares.delete(ws.shareCode);
|
||||
if (ws.sessionId) {
|
||||
|
||||
Reference in New Issue
Block a user