fix(desktop): notification click no longer reloads to a dead page (0.1.9/batch62)
Root cause of the persistent "notification opens a dead page / no Join" bug: openFromNotif gated the in-place open on `window.ME`, but ME is declared with `let` — which never creates a global property — so window.ME was ALWAYS undefined and every click hit the full-page-reload fallback. Use bare `ME`. Also: - Notifications fire instantly: never block on the DP download. Use the photo only if already cached; warm the cache in the background + pre-warm all contact DPs on chat load (precache-avatars IPC). Removes the ~2.5s lag. - Wire call Join/Decline handlers BEFORE firing the OS notification so Join is live the instant the invite popup appears (was dead until the toast settled). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+17
-8
@@ -83,6 +83,13 @@ function avatarToTempPng(src) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-warm the DP cache for the renderer's contacts (called after chats load), so the FIRST
|
||||||
|
// notification from anyone already has their photo — no per-toast download wait.
|
||||||
|
ipcMain.handle('precache-avatars', async (_e, urls = []) => {
|
||||||
|
try { for (const u of (Array.isArray(urls) ? urls : []).slice(0, 100)) { try { await avatarToTempPng(u); } catch (_) {} } } catch (_) {}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Keep STRONG references to live notifications. Electron/Windows garbage-collects a Notification
|
// Keep STRONG references to live notifications. Electron/Windows garbage-collects a Notification
|
||||||
// with no reference, which closed the toast within ~1s and made clicks do nothing.
|
// with no reference, which closed the toast within ~1s and made clicks do nothing.
|
||||||
const activeNotifs = new Set();
|
const activeNotifs = new Set();
|
||||||
@@ -90,14 +97,16 @@ const activeNotifs = new Set();
|
|||||||
// Resolves {open} when the toast is clicked (renderer then opens that chat), else null.
|
// Resolves {open} when the toast is clicked (renderer then opens that chat), else null.
|
||||||
ipcMain.handle('reply-notification', async (_e, payload = {}) => {
|
ipcMain.handle('reply-notification', async (_e, payload = {}) => {
|
||||||
if (!Notification.isSupported()) return null;
|
if (!Notification.isSupported()) return null;
|
||||||
// Do NOT block the toast on the avatar download (that made desktop notifications lag ~15s vs the
|
// Fire the toast IMMEDIATELY — NEVER block on a download (waiting made notifications lag; a late
|
||||||
// browser's instant one). Race it against a cap: use the DP only if it's ready in time. Chat toasts
|
// call/chat alert is worse than one without a photo). Use the DP only if it's ALREADY cached
|
||||||
// stay snappy (700ms). CALL toasts (persistent) ring for ~40s, so we can afford to wait longer
|
// (instant). If not, kick off a background fetch so the SAME sender's NEXT notification has it.
|
||||||
// (2.5s) to actually show the caller's photo — the whole point of a call notification.
|
// Contacts are also pre-warmed on load (precache-avatars), so the photo is usually already cached.
|
||||||
const img = await Promise.race([
|
let img = null;
|
||||||
avatarToTempPng(payload.avatar),
|
try {
|
||||||
new Promise((r) => setTimeout(() => r(null), payload.persistent ? 2500 : 700)),
|
const c = payload.avatar && avatarCache.get(payload.avatar);
|
||||||
]);
|
if (c && fs.existsSync(c)) img = c;
|
||||||
|
else if (payload.avatar) avatarToTempPng(payload.avatar).catch(() => {}); // warm for next time
|
||||||
|
} catch (_) {}
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
let done = false;
|
let done = false;
|
||||||
let n;
|
let n;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "biz-connect-desktop",
|
"name": "biz-connect-desktop",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
|
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "BizGaze",
|
"name": "BizGaze",
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ contextBridge.exposeInMainWorld('bizConnectNative', Object.freeze({
|
|||||||
// Native Windows toast with an inline reply box. Resolves to {text} (replied), {open} (clicked)
|
// Native Windows toast with an inline reply box. Resolves to {text} (replied), {open} (clicked)
|
||||||
// or null. Lets the user reply to a chat straight from the notification.
|
// or null. Lets the user reply to a chat straight from the notification.
|
||||||
replyNotify: (payload) => ipcRenderer.invoke('reply-notification', payload),
|
replyNotify: (payload) => ipcRenderer.invoke('reply-notification', payload),
|
||||||
|
// Pre-warm the notification DP cache with contact photo URLs (called after chats load) so the first
|
||||||
|
// toast from anyone already has their photo — no per-notification download lag.
|
||||||
|
precacheAvatars: (urls) => { try { return ipcRenderer.invoke('precache-avatars', urls); } catch (_) { return Promise.resolve(false); } },
|
||||||
// Manual "Check for updates" from Settings. Resolves {status:'available'|'current'|'dev'|'error', version?}.
|
// Manual "Check for updates" from Settings. Resolves {status:'available'|'current'|'dev'|'error', version?}.
|
||||||
// On 'available' the shell downloads in the background and prompts to restart when ready.
|
// On 'available' the shell downloads in the background and prompts to restart when ready.
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-updates'),
|
checkForUpdates: () => ipcRenderer.invoke('check-updates'),
|
||||||
|
|||||||
+18
-3
@@ -840,7 +840,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=5"></script>
|
<script src="/icons.js?v=5"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></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-08-batch61';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-08-batch62';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
|
// 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)
|
// (emojis stay as plain Unicode). (#5)
|
||||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||||
@@ -1398,6 +1398,17 @@ async function loadSidebar(){
|
|||||||
ROWS=items;
|
ROWS=items;
|
||||||
renderChats(searchVal());
|
renderChats(searchVal());
|
||||||
updateRailUnread();
|
updateRailUnread();
|
||||||
|
prewarmNotifAvatars();
|
||||||
|
}
|
||||||
|
// Desktop only: hand the shell every contact/group DP URL so it caches them up front — the first
|
||||||
|
// notification (chat OR call) from anyone then shows their photo instantly, with no per-toast wait.
|
||||||
|
let _avatarsWarmed=false;
|
||||||
|
function prewarmNotifAvatars(){
|
||||||
|
try{
|
||||||
|
const n=window.bizConnectNative; if(!n||!n.precacheAvatars||_avatarsWarmed) return;
|
||||||
|
const urls=[...new Set((ROWS||[]).map(r=>r.avatar).filter(u=>u && /^https?:\/\//i.test(u)))];
|
||||||
|
if(urls.length){ _avatarsWarmed=true; n.precacheAvatars(urls); }
|
||||||
|
}catch(_){}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- conversation view -----
|
// ----- conversation view -----
|
||||||
@@ -1794,11 +1805,13 @@ function showCallInvite(room, byName, ret, sub, quiet){
|
|||||||
+'<button class="ci-join">'+ic('video',16)+' Join</button>'
|
+'<button class="ci-join">'+ic('video',16)+' Join</button>'
|
||||||
+'<button class="ci-decline" title="Decline">'+ic('callEnd',16)+' Decline</button>';
|
+'<button class="ci-decline" title="Decline">'+ic('callEnd',16)+' Decline</button>';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
if(!quiet){ try{ notify('📞 '+who, (sub?('Group call · '+sub):'is calling you'), ret&&ret.kind, ret&&ret.id, {persistent:true}); }catch(_){} } // OS notification too (skip when merely re-showing on chat open)
|
|
||||||
let closed=false;
|
let closed=false;
|
||||||
const close=()=>{ if(closed) return; closed=true; try{ el.remove(); }catch(_){} stopRing(); };
|
const close=()=>{ if(closed) return; closed=true; try{ el.remove(); }catch(_){} stopRing(); };
|
||||||
|
// Wire Join/Decline BEFORE firing the OS notification so the buttons are live the instant the popup
|
||||||
|
// shows (previously notify() ran first — the click did nothing until the toast plumbing settled).
|
||||||
el.querySelector('.ci-join').onclick=()=>{ close(); meetReturn=ret||null; switchTab('meeting'); enterMeeting(room); };
|
el.querySelector('.ci-join').onclick=()=>{ close(); meetReturn=ret||null; switchTab('meeting'); enterMeeting(room); };
|
||||||
el.querySelector('.ci-decline').onclick=()=>{ close(); if(ret&&ret.kind==='dm'){ postJSON('/api/calls/decline',{room}).catch(()=>{}); } }; // 1:1 → notify caller; group → silent
|
el.querySelector('.ci-decline').onclick=()=>{ close(); if(ret&&ret.kind==='dm'){ postJSON('/api/calls/decline',{room}).catch(()=>{}); } }; // 1:1 → notify caller; group → silent
|
||||||
|
if(!quiet){ try{ notify('📞 '+who, (sub?('Group call · '+sub):'is calling you'), ret&&ret.kind, ret&&ret.id, {persistent:true}); }catch(_){} } // OS notification too (skip when merely re-showing on chat open)
|
||||||
setTimeout(close, 45000);
|
setTimeout(close, 45000);
|
||||||
}
|
}
|
||||||
// Scheduled-meeting invitation (toast + the meeting shows up in their list).
|
// Scheduled-meeting invitation (toast + the meeting shows up in their list).
|
||||||
@@ -2353,7 +2366,9 @@ function openFromNotif(kind,id){
|
|||||||
// The app is already running (the notification came from it) → open the chat IN-PLACE. A full-page
|
// The app is already running (the notification came from it) → open the chat IN-PLACE. A full-page
|
||||||
// reload was slow (#19) and dropped live state: the sidebar hadn't reloaded so the header showed
|
// reload was slow (#19) and dropped live state: the sidebar hadn't reloaded so the header showed
|
||||||
// "Conversation" with no DP (#7), and an incoming call's Join/invite popup was lost (#2).
|
// "Conversation" with no DP (#7), and an incoming call's Join/invite popup was lost (#2).
|
||||||
if(window.ME && ME.id && typeof selectChat==='function'){
|
// NB: use bare `ME`, NOT `window.ME` — ME is declared with `let`, which does NOT create a global
|
||||||
|
// property, so `window.ME` was always undefined and EVERY click hit the reload fallback below.
|
||||||
|
if(ME && ME.id && typeof selectChat==='function'){
|
||||||
if(id){ try{ switchTab('chat'); }catch(_){} try{ selectChat(kind||'dm', id); }catch(_){} }
|
if(id){ try{ switchTab('chat'); }catch(_){} try{ selectChat(kind||'dm', id); }catch(_){} }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user