#2: a message arriving in the currently-OPEN chat produced no notification when the app was minimized. onChatMessage marked the open chat read even while document.hidden, which fired a notif-clear that closed the very notification the service worker had just shown. Now the open chat is only marked read while visible; markOpenChatRead() catches up on focus/visibility return, and a message received while hidden stays unread with its alert intact. #3: reacting to a message fired no notification. The react route only pushed over the live socket (nothing for a closed app) and the client added a silent bell entry. Now the server sends a native/web push to the message owner, and onChatReaction pings + shows an OS/in-page popup (unless you're viewing that chat). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+34
-4
@@ -2491,7 +2491,21 @@ function onGroupUpdate(d){ if(!d||!d.group) return;
|
||||
const gi=document.getElementById('groupInfo'); if(gi){ gi.remove(); if(!d.removed && selected&&selected.kind==='group'&&selected.id===d.group) openGroupInfo(d.group); } // refresh open group-info
|
||||
}
|
||||
function onChatReaction(d){ const m=THREAD.find(x=>x.id===d.messageId); if(m && d.reactions){ m.reactions=d.reactions; updateBubble(m); }
|
||||
if(d.added && d.owner===ME.id && d.byId && d.byId!==ME.id){ addNotif({icon:'smilePlus', text:pEsc(d.by||'Someone')+' reacted '+(d.emoji||'')+' to your message', link:d.convId?{kind:'group',id:d.convId}:{kind:'dm',id:d.byId}}); }
|
||||
if(d.added && d.owner===ME.id && d.byId && d.byId!==ME.id){
|
||||
const kind=d.convId?'group':'dm'; const rid=d.convId||d.byId;
|
||||
addNotif({icon:'smilePlus', text:pEsc(d.by||'Someone')+' reacted '+(d.emoji||'')+' to your message', link:{kind,id:rid}});
|
||||
// #3: someone reacted to MY message → actually alert (ping + OS/in-page popup), like a new message,
|
||||
// unless I'm already looking at that chat. When hidden with push active, the SW/native push shows it.
|
||||
if(notifOn(kind)){
|
||||
const isViewing=selected&&selected.kind===kind&&selected.id===rid&¤tTab()==='chat'&&!document.hidden;
|
||||
if(!isViewing){ playPing();
|
||||
if(!(document.hidden && pushActive)){
|
||||
const title=(kind==='group')?(((rowFor('group',rid)||{}).name)||m&&m.groupName||'Group'):(d.by||'Reaction');
|
||||
notify(title, (d.by||'Someone')+' reacted '+(d.emoji||'')+' to your message', kind, rid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Read receipts (DM): the other party read my messages → mark mine as seen.
|
||||
function onChatRead(d){ if(!d||!d.by) return;
|
||||
@@ -4061,8 +4075,17 @@ async function hardReloadApp(){
|
||||
// Cheap by design: one ~35-byte request, answered from a variable the server read at startup. The
|
||||
// throttle above means refocusing the app repeatedly costs nothing.
|
||||
setInterval(()=>checkWebBuild(true), 5*60*1000); // every 5 min (the interval IS the schedule)
|
||||
window.addEventListener('focus', ()=>checkWebBuild()); // coming back to the app (throttled)
|
||||
document.addEventListener('visibilitychange', ()=>{ if(!document.hidden) checkWebBuild(); });
|
||||
// #2: when we come back to the app, mark the OPEN chat read (we deliberately skip that while hidden so a
|
||||
// backgrounded message keeps its notification). Only runs when actually visible + a chat is open.
|
||||
function markOpenChatRead(){
|
||||
if(document.hidden) return;
|
||||
if(!(selected && (selected.kind==='dm'||selected.kind==='group') && currentTab()==='chat')) return;
|
||||
const it=rowFor(selected.kind, selected.id); if(it && it.unread){ it.unread=0; try{ renderChats(searchVal()); updateRailUnread(); }catch(_){} }
|
||||
const body=JSON.stringify(selected.kind==='group'?{group:selected.id}:{with:selected.id});
|
||||
try{ fetch('/api/messages/read',{method:'POST',headers:{'Content-Type':'application/json'},body}); }catch(_){}
|
||||
}
|
||||
window.addEventListener('focus', ()=>{ checkWebBuild(); markOpenChatRead(); }); // coming back to the app (throttled)
|
||||
document.addEventListener('visibilitychange', ()=>{ if(!document.hidden){ checkWebBuild(); markOpenChatRead(); } });
|
||||
setTimeout(()=>checkWebBuild(true), 3000); // shortly after boot
|
||||
// Deliberately NO banner and NO toast: a web build is an implementation detail. Surfacing it would make
|
||||
// users think about "web build vs app version". It just updates.
|
||||
@@ -4214,7 +4237,14 @@ function onChatMessage(m){
|
||||
// Dedup by id: the server echoes our own sent message back (multi-tab/device sync), and
|
||||
// sendMessage already appended it optimistically — so skip if it's already in the thread.
|
||||
if(!THREAD.some(x=>x.id===m.id)){ THREAD.push(m); appendBubble(m); }
|
||||
if(m.from!==ME.id && !isSys){ if(it) it.unread=0; const body=JSON.stringify(kind==='group'?{group:rid}:{with:rid}); try{ fetch('/api/messages/read',{method:'POST',headers:{'Content-Type':'application/json'},body}); }catch(_){} }
|
||||
// #2: only mark the OPEN chat read when the app is actually VISIBLE. Marking it read while minimized
|
||||
// told the server we'd seen it → a notif-clear closed the notification the SW had just shown, so a
|
||||
// message from the OPEN chat produced no alert while backgrounded. When hidden, keep it unread; the
|
||||
// focus/visibilitychange handler (markOpenChatRead) catches up the moment we come back.
|
||||
if(m.from!==ME.id && !isSys){
|
||||
if(!document.hidden){ if(it) it.unread=0; const body=JSON.stringify(kind==='group'?{group:rid}:{with:rid}); try{ fetch('/api/messages/read',{method:'POST',headers:{'Content-Type':'application/json'},body}); }catch(_){} }
|
||||
else if(it) it.unread=(it.unread||0)+1; // hidden → stays unread (and the notification stands) until we look
|
||||
}
|
||||
} else if(m.from!==ME.id && !isSys && notifOn(kind)){
|
||||
if(window.BZToast) BZToast.message(m.body?(m.body.length>60?m.body.slice(0,60)+'…':m.body):'📎 Attachment', {title:(m.fromName||'New message')});
|
||||
}
|
||||
|
||||
@@ -1712,6 +1712,18 @@ route('POST', '/api/messages/react', async (req, res) => {
|
||||
try { CHAT.pushToUser(other, { type: 'chat-reaction', messageId, reactions: await reactionsForMessage(messageId, other, names), ...meta }); } catch (_) {}
|
||||
try { CHAT.pushToUser(u.id, { type: 'chat-reaction', messageId, reactions: await reactionsForMessage(messageId, u.id, names), ...meta }); } catch (_) {}
|
||||
}
|
||||
// #3: notify the message OWNER that someone reacted — a native/web push so a CLOSED app is alerted too
|
||||
// (previously reactions only pushed over the live socket, so a backgrounded owner got nothing). Only when
|
||||
// the reaction was ADDED (not removed) and by someone other than the owner.
|
||||
if (added && msg.sender_id && msg.sender_id !== u.id) {
|
||||
const reactor = u.name || u.email;
|
||||
if (msg.conversation_id) {
|
||||
const conv = await R.conversations.byId(msg.conversation_id); const gname = (conv && conv.name) || 'Group';
|
||||
try { PUSH.sendToUser(msg.sender_id, { title: gname, body: reactor + ' reacted ' + e + ' to your message', kind: 'group', id: msg.conversation_id, tag: 'react:' + messageId, icon: u.avatar_url || undefined }); } catch (_) {}
|
||||
} else {
|
||||
try { PUSH.sendToUser(msg.sender_id, { title: reactor, body: 'reacted ' + e + ' to your message', kind: 'dm', id: u.id, tag: 'react:' + messageId, icon: u.avatar_url || undefined }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
json(res, 200, { ok: true, messageId, added, reactions: await reactionsForMessage(messageId, u.id, names) });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user