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:
+23
-7
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user