feat(chat): dismiss a chat's notifications on other devices once seen (#13)
When you open/read a conversation, the server now also pushes a notif-clear to your OWN other sockets. Each device tags notifications by conversation (kind:id), so on notif-clear it closes the matching page Notification + any Service-Worker (Web Push) notifications, and drops matching activity-center entries so the bell badge stays in sync. (Desktop Electron native toasts are transient/auto-expire; the web+PWA surface is covered.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+17
-4
@@ -809,7 +809,7 @@
|
||||
<body>
|
||||
<script src="/icons.js?v=4"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||
<script>window.__BUILD='2026-07-07-batch51';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-07-batch52';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
|
||||
// (emojis stay as plain Unicode). (#5)
|
||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||
@@ -2302,6 +2302,16 @@ function notifAvatarDataUrl(kind,id,fallbackName){
|
||||
}
|
||||
// Reply to a conversation without opening it (used by the notification quick-reply).
|
||||
async function sendReplyTo(kind,id,text){ try{ await postJSON('/api/messages', kind==='group'?{group:id,body:text}:{to:id,body:text}); }catch(_){} }
|
||||
// #13: track shown page-notifications by conversation tag so reading elsewhere can dismiss them.
|
||||
const _shownNotifs={};
|
||||
function onNotifClear(d){
|
||||
if(!d) return; const tag=(d.kind||'')+':'+(d.id||'');
|
||||
try{ const n=_shownNotifs[tag]; if(n){ n.close(); } delete _shownNotifs[tag]; }catch(_){}
|
||||
// Also close any Service-Worker (Web Push) notifications for this chat.
|
||||
try{ if('serviceWorker' in navigator && navigator.serviceWorker.getRegistration) navigator.serviceWorker.getRegistration().then(reg=>{ if(reg&®.getNotifications) reg.getNotifications({tag}).then(ns=>ns.forEach(n=>{ try{ n.close(); }catch(_){} })); }).catch(()=>{}); }catch(_){}
|
||||
// Drop matching activity-center entries so the bell badge stays in sync.
|
||||
try{ if(Array.isArray(NOTIFS)){ const before=NOTIFS.length; NOTIFS=NOTIFS.filter(x=>!(x.link&&x.link.kind===d.kind&&x.link.id===d.id)); if(NOTIFS.length!==before){ saveNotifs&&saveNotifs(); updateBellBadge&&updateBellBadge(); } } }catch(_){}
|
||||
}
|
||||
function notify(title, body, kind, id){
|
||||
try{
|
||||
// Desktop app: native toast with the sender/group DP. Pass the DP URL directly — the shell
|
||||
@@ -2320,8 +2330,11 @@ function notify(title, body, kind, id){
|
||||
// canvas taints it and silently fell back to initials (the bug). Brand icon only if no DP.
|
||||
if(!('Notification' in window) || Notification.permission!=='granted') return;
|
||||
const _row=rowFor(kind,id)||{};
|
||||
const n=new Notification(title, { body, icon: _row.avatar || '/icon-192.png', badge:'/icon-192.png' });
|
||||
n.onclick=()=>{ n.close(); openFromNotif(kind, id); };
|
||||
const tag=(kind||'')+':'+(id||''); // #13: tag by conversation so it can be dismissed cross-device
|
||||
const n=new Notification(title, { body, icon: _row.avatar || '/icon-192.png', badge:'/icon-192.png', tag });
|
||||
_shownNotifs[tag]=n;
|
||||
n.onclick=()=>{ n.close(); delete _shownNotifs[tag]; openFromNotif(kind, id); };
|
||||
n.onclose=()=>{ if(_shownNotifs[tag]===n) delete _shownNotifs[tag]; };
|
||||
setTimeout(()=>{ try{ n.close(); }catch(_){} }, 8000);
|
||||
}catch(_){}
|
||||
}
|
||||
@@ -2452,7 +2465,7 @@ function connectChatWs(){
|
||||
if(_chatReconnectT){ clearTimeout(_chatReconnectT); _chatReconnectT=null; }
|
||||
chatWs=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
||||
chatWs.onopen=()=>{ try{ chatWs.send(JSON.stringify({type:'chat-hello'})); }catch(_){} if(_chatConnectedOnce) resyncChat(); _chatConnectedOnce=true; };
|
||||
chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-edited') onChatEdited(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='presence') onPresence(d); else if(d.type==='chat-typing') onTyping(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); };
|
||||
chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-edited') onChatEdited(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='presence') onPresence(d); else if(d.type==='chat-typing') onTyping(d); else if(d.type==='notif-clear') onNotifClear(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); };
|
||||
chatWs.onclose=()=>{ if(_chatReconnectT) clearTimeout(_chatReconnectT); _chatReconnectT=setTimeout(connectChatWs, 3000); }; // auto-reconnect (single pending timer)
|
||||
}catch(_){}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user