fix(desktop): use Electron's native Notification (reliable click-to-open)

node-notifier/SnoreToast click callback never fired without the crashing wait mode. Switch the
chat toast to Electron's built-in Notification: shows avatar + message and its 'click' event
reliably raises the app + opens the chat. No SnoreToast, no external tools.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 11:16:35 +05:30
parent f8978ff796
commit 2b27889e07
+19 -17
View File
@@ -8,7 +8,7 @@
// - external links open in the user's browser, not inside the app
//
// Server origin is configurable so the same build works against prod or a dev server.
const { app, BrowserWindow, session, desktopCapturer, shell, Menu, ipcMain, nativeImage } = require('electron');
const { app, BrowserWindow, session, desktopCapturer, shell, Menu, ipcMain, nativeImage, Notification } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
@@ -35,15 +35,11 @@ ipcMain.on('get-install-info', (e) => {
// on the next restart. No-op in dev (unpackaged).
let autoUpdater = null;
try { ({ autoUpdater } = require('electron-updater')); } catch (_) { /* not installed in dev */ }
// Chat toast: shows the sender/group avatar + message; clicking it opens the chat. Uses
// node-notifier's WindowsToaster for reliable DISPLAY. Inline text reply is intentionally not
// used — the bundled SnoreToast crashes (0xC0000409) handling a text reply — so this is a clean
// click-to-open toast. Installed-app only (needs the AppUserModelID shortcut the installer sets).
let notifier = null;
try { notifier = require('node-notifier'); } catch (_) { /* optional */ }
// Chat toast: sender/group avatar + message; clicking it raises the app and opens that chat.
// Uses Electron's OWN Notification (native, no SnoreToast) whose 'click' event fires reliably.
const APP_ID = 'com.bizgaze.connect.desktop';
// Write the avatar the renderer drew (a data: URL) to a temp PNG for the toast image.
// Write the avatar the renderer drew (a data: URL) to a temp PNG for the toast icon.
function writeTempPng(dataUrl) {
try {
if (!dataUrl || !/^data:image\/png;base64,/.test(dataUrl)) return null;
@@ -53,20 +49,26 @@ function writeTempPng(dataUrl) {
} catch (_) { return null; }
}
// Resolves {open} when the toast is clicked (the 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', (_e, payload = {}) => new Promise((resolve) => {
if (!notifier) return resolve(null);
if (!Notification.isSupported()) return resolve(null);
const img = writeTempPng(payload.avatar);
let done = false;
const finish = (v) => { if (!done) { done = true; if (img) { try { fs.unlinkSync(img); } catch (_) {} } resolve(v); } };
try {
notifier.notify(
Object.assign({ appID: APP_ID, title: payload.title || 'Biz Connect', message: payload.body || ' ' }, img ? { icon: img } : {}),
(err, response, metadata) => {
const act = String(response || (metadata && metadata.action) || '').toLowerCase();
finish(act.includes('activat') || act === 'click' ? { kind: payload.kind, id: payload.id, open: true } : null);
}
);
const n = new Notification({
title: payload.title || 'Biz Connect',
body: payload.body || '',
icon: img ? nativeImage.createFromPath(img) : undefined,
silent: false,
});
n.on('click', () => {
if (win) { if (win.isMinimized()) win.restore(); win.show(); win.focus(); } // raise the app
finish({ kind: payload.kind, id: payload.id, open: true });
});
n.on('close', () => finish(null));
n.show();
setTimeout(() => finish(null), 30000); // don't leave the promise pending forever
} catch (_) { finish(null); }
}));