feat(desktop): real Windows inline reply via SnoreToast -tb + sender/group avatar

- node-notifier's WindowsToaster forwards raw opts to SnoreToast, so inject -tb (reply box)
  + -p (image). Reuses its named-pipe + result parsing (exit 5 = TextEntered). No pwsh needed.
  Logs the raw toast result to userData/toast-debug.log to confirm the reply field on real HW.
- home.html: notifAvatarDataUrl draws the DM sender's pic / group's DP (else colored initials)
  to a round PNG and passes it as the toast image. Reply -> sendReplyTo; click -> open chat.
- dropped powertoast (ESM + needs pwsh 7, absent here). build batch22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 23:26:24 +05:30
parent ffe04e6bff
commit 3f209bf619
4 changed files with 94 additions and 36 deletions
+23 -7
View File
@@ -731,7 +731,7 @@
</head>
<body>
<script src="/icons.js?v=4"></script>
<script>window.__BUILD='2026-07-01-batch21';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
<script>window.__BUILD='2026-07-01-batch22';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);</script>
<div class="loading" id="loading">Loading…</div>
<header>
@@ -1895,17 +1895,33 @@ async function unsubscribePush(){
// 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||'')); }
// Draw the conversation's avatar (DM = sender pic, group = group DP; else colored initials) to
// a round PNG data URL for the desktop toast image.
function notifAvatarDataUrl(kind,id,fallbackName){
return new Promise((resolve)=>{
try{
const row=rowFor(kind,id)||{}; const src=row.avatar||''; const nm=row.name||fallbackName||'?';
const s=96, c=document.createElement('canvas'); c.width=s; c.height=s; const g=c.getContext('2d');
const initialsPng=()=>{ try{ g.clearRect(0,0,s,s); g.fillStyle=avColor(nm); g.beginPath(); g.arc(s/2,s/2,s/2,0,2*Math.PI); g.fill(); g.fillStyle='#334155'; g.font='bold 40px system-ui,Segoe UI,sans-serif'; g.textAlign='center'; g.textBaseline='middle'; g.fillText(initials(nm), s/2, s/2+2); resolve(c.toDataURL('image/png')); }catch(_){ resolve(null); } };
if(src){ const img=new Image(); img.crossOrigin='anonymous'; let settled=false; const done=(fn)=>{ if(!settled){ settled=true; fn(); } };
img.onload=()=>done(()=>{ try{ g.save(); g.beginPath(); g.arc(s/2,s/2,s/2,0,2*Math.PI); g.clip(); g.drawImage(img,0,0,s,s); g.restore(); resolve(c.toDataURL('image/png')); }catch(_){ initialsPng(); } });
img.onerror=()=>done(initialsPng); img.src=src; setTimeout(()=>done(initialsPng), 1500);
} else initialsPng();
}catch(_){ resolve(null); }
});
}
// 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.
// Desktop app (Phase D): native Windows toast with an inline reply box + sender/group avatar.
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(()=>{});
notifAvatarDataUrl(kind, id, title)
.then(avatar=>window.bizConnectNative.replyNotify({title, body, kind, id, avatar}))
.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.