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:
+19
-17
@@ -8,7 +8,7 @@
|
|||||||
// - external links open in the user's browser, not inside the app
|
// - 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.
|
// 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 path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
@@ -35,15 +35,11 @@ ipcMain.on('get-install-info', (e) => {
|
|||||||
// on the next restart. No-op in dev (unpackaged).
|
// on the next restart. No-op in dev (unpackaged).
|
||||||
let autoUpdater = null;
|
let autoUpdater = null;
|
||||||
try { ({ autoUpdater } = require('electron-updater')); } catch (_) { /* not installed in dev */ }
|
try { ({ autoUpdater } = require('electron-updater')); } catch (_) { /* not installed in dev */ }
|
||||||
// Chat toast: shows the sender/group avatar + message; clicking it opens the chat. Uses
|
// Chat toast: sender/group avatar + message; clicking it raises the app and opens that chat.
|
||||||
// node-notifier's WindowsToaster for reliable DISPLAY. Inline text reply is intentionally not
|
// Uses Electron's OWN Notification (native, no SnoreToast) whose 'click' event fires reliably.
|
||||||
// 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 */ }
|
|
||||||
const APP_ID = 'com.bizgaze.connect.desktop';
|
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) {
|
function writeTempPng(dataUrl) {
|
||||||
try {
|
try {
|
||||||
if (!dataUrl || !/^data:image\/png;base64,/.test(dataUrl)) return null;
|
if (!dataUrl || !/^data:image\/png;base64,/.test(dataUrl)) return null;
|
||||||
@@ -53,20 +49,26 @@ function writeTempPng(dataUrl) {
|
|||||||
} catch (_) { return null; }
|
} 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) => {
|
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);
|
const img = writeTempPng(payload.avatar);
|
||||||
let done = false;
|
let done = false;
|
||||||
const finish = (v) => { if (!done) { done = true; if (img) { try { fs.unlinkSync(img); } catch (_) {} } resolve(v); } };
|
const finish = (v) => { if (!done) { done = true; if (img) { try { fs.unlinkSync(img); } catch (_) {} } resolve(v); } };
|
||||||
try {
|
try {
|
||||||
notifier.notify(
|
const n = new Notification({
|
||||||
Object.assign({ appID: APP_ID, title: payload.title || 'Biz Connect', message: payload.body || ' ' }, img ? { icon: img } : {}),
|
title: payload.title || 'Biz Connect',
|
||||||
(err, response, metadata) => {
|
body: payload.body || '',
|
||||||
const act = String(response || (metadata && metadata.action) || '').toLowerCase();
|
icon: img ? nativeImage.createFromPath(img) : undefined,
|
||||||
finish(act.includes('activat') || act === 'click' ? { kind: payload.kind, id: payload.id, open: true } : null);
|
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); }
|
} catch (_) { finish(null); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user