feat(desktop): Phase D — inline-reply notifications (Windows toast reply box)

Reply to a chat straight from the OS notification, no app switch:
- node-notifier (bundles SnoreToast) renders a native Windows toast with a reply box —
  Electron's own Notification can't do Windows inline reply.
- main.js reply-notification handler resolves {text}|{open}|null; preload exposes replyNotify.
- home.html notify() routes chat toasts through it on desktop: a typed reply -> sendReplyTo()
  POSTs to /api/messages without opening the app; a click opens the chat. Web/PWA path unchanged.
- Works only in the installed app (needs the installer's AppUserModelID). desktop 0.1.2, build batch21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:36:02 +05:30
parent 160a66f934
commit 21ae3e1aa5
6 changed files with 133 additions and 13 deletions
+15 -2
View File
@@ -731,7 +731,7 @@
</head>
<body>
<script src="/icons.js?v=4"></script>
<script>window.__BUILD='2026-07-01-batch20';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
<script>window.__BUILD='2026-07-01-batch21';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
<div class="loading" id="loading">Loading…</div>
<header>
@@ -1894,11 +1894,24 @@ async function unsubscribePush(){
// Open the chat from an in-page notification. Navigation reliably repaints across browsers (a
// notification click is not an in-page gesture, so an in-place open won't paint until you
// tap). The reload is made fast by HTTP caching + a boot fast-path that opens the chat first.
function openFromNotif(kind,id){ try{ if(window.bizConnectNative&&window.bizConnectNative.focusApp) window.bizConnectNative.focusApp(); }catch(_){} try{ window.focus(); }catch(_){} location.assign('/home?openKind='+encodeURIComponent(kind||'')+'&openId='+encodeURIComponent(id||'')); }
// 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(_){} }
function notify(title, body, kind, id){
try{
// Desktop app (Phase D): native Windows toast with an inline reply box.
if(window.bizConnectNative && window.bizConnectNative.replyNotify){
window.bizConnectNative.replyNotify({title, body, kind, id}).then(r=>{
if(!r) return;
if(r.text) sendReplyTo(kind, id, r.text); // replied from the toast
else if(r.open) openFromNotif(kind, id); // clicked the toast
}).catch(()=>{});
return;
}
// Web / PWA: standard Notification.
if(!('Notification' in window) || Notification.permission!=='granted') return;
const n=new Notification(title, { body, icon:'/logo.png' });
n.onclick=()=>{ try{ window.focus(); }catch(_){} try{ if(window.bizConnectNative&&window.bizConnectNative.focusApp) window.bizConnectNative.focusApp(); }catch(_){} const u='/home?openKind='+encodeURIComponent(kind||'')+'&openId='+encodeURIComponent(id||''); n.close(); location.assign(u); };
n.onclick=()=>{ n.close(); openFromNotif(kind, id); };
setTimeout(()=>{ try{ n.close(); }catch(_){} }, 8000);
}catch(_){}
}