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:
2026-07-08 18:00:56 +05:30
parent 6ed0fa0ea0
commit 62a5f750af
4 changed files with 39 additions and 12 deletions
+17 -8
View File
@@ -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
// with no reference, which closed the toast within ~1s and made clicks do nothing.
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.
ipcMain.handle('reply-notification', async (_e, payload = {}) => {
if (!Notification.isSupported()) return null;
// Do NOT block the toast on the avatar download (that made desktop notifications lag ~15s vs the
// browser's instant one). Race it against a cap: use the DP only if it's ready in time. Chat toasts
// stay snappy (700ms). CALL toasts (persistent) ring for ~40s, so we can afford to wait longer
// (2.5s) to actually show the caller's photo — the whole point of a call notification.
const img = await Promise.race([
avatarToTempPng(payload.avatar),
new Promise((r) => setTimeout(() => r(null), payload.persistent ? 2500 : 700)),
]);
// Fire the toast IMMEDIATELY — NEVER block on a download (waiting made notifications lag; a late
// call/chat alert is worse than one without a photo). Use the DP only if it's ALREADY cached
// (instant). If not, kick off a background fetch so the SAME sender's NEXT notification has it.
// Contacts are also pre-warmed on load (precache-avatars), so the photo is usually already cached.
let img = null;
try {
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) => {
let done = false;
let n;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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",
"author": {
"name": "BizGaze",
+3
View File
@@ -22,6 +22,9 @@ contextBridge.exposeInMainWorld('bizConnectNative', Object.freeze({
// 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.
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?}.
// On 'available' the shell downloads in the background and prompts to restart when ready.
checkForUpdates: () => ipcRenderer.invoke('check-updates'),