From 472665b9801b62aec0ac37f11dbe7b0470b50ee9 Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 16:47:19 +0200 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20tray=20badge=20=E2=80=94=20IPC?= =?UTF-8?q?=20handler=20died=20when=20icon.ico=20missing=20from=20packaged?= =?UTF-8?q?=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/electron/modules/tray.ts | 79 ++++++++++++++++++--------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/apps/desktop/electron/modules/tray.ts b/apps/desktop/electron/modules/tray.ts index 27beb58..6c2a529 100644 --- a/apps/desktop/electron/modules/tray.ts +++ b/apps/desktop/electron/modules/tray.ts @@ -30,6 +30,17 @@ function resolveIconPathDev(): string { return path.join(app.getAppPath(), 'resources', 'icon.ico'); } +// 16×16 grey square — last-ditch fallback when neither the packaged +// nor dev icon file resolves. Tray constructor on Windows throws when +// handed an empty NativeImage, which would tear down the whole +// registrar before `ipcMain.handle(TRAY_UNREAD)` runs — leaving the +// taskbar overlay badge wired but the renderer's invoke rejecting +// with "No handler registered". A non-empty placeholder keeps the +// constructor happy so the IPC handler always gets registered. +const FALLBACK_TRAY_PNG_BASE64 = + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAH0lEQVR42mNk' + + 'YGD4z0ABYBxVOKpwVOGowlGFwwoBAEnYAR9XlIldAAAAAElFTkSuQmCC'; + function loadTrayIcon(): NativeImage { for (const p of [resolveIconPath(), resolveIconPathDev()]) { try { @@ -39,7 +50,7 @@ function loadTrayIcon(): NativeImage { /* try next */ } } - return nativeImage.createEmpty(); + return nativeImage.createFromBuffer(Buffer.from(FALLBACK_TRAY_PNG_BASE64, 'base64')); } function buildOverlay(): NativeImage { @@ -53,35 +64,49 @@ function buildOverlay(): NativeImage { } export function register(mainWindow: BrowserWindow): void { - const icon = loadTrayIcon(); - trayRef = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon); - trayRef.setToolTip('Netralax'); + // Tray icon is best-effort: if neither the packaged resource nor the + // dev path resolves (e.g. icon.ico isn't shipped under resourcesPath + // in packaged builds — only the app icon goes into the .exe metadata), + // we still want the taskbar overlay badge to work. setOverlayIcon is + // a BrowserWindow method, so it functions even when the systray icon + // creation fails. + try { + const icon = loadTrayIcon(); + if (!icon.isEmpty()) { + trayRef = new Tray(icon); + trayRef.setToolTip('Netralax'); - const menu = Menu.buildFromTemplate([ - { - label: 'Open', - click: (): void => { + const menu = Menu.buildFromTemplate([ + { + label: 'Open', + click: (): void => { + if (mainWindow.isDestroyed()) return; + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: 'separator' }, + { + label: 'Quit', + click: (): void => { + app.quit(); + }, + }, + ]); + trayRef.setContextMenu(menu); + trayRef.on('click', (): void => { if (mainWindow.isDestroyed()) return; if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.show(); - mainWindow.focus(); - }, - }, - { type: 'separator' }, - { - label: 'Quit', - click: (): void => { - app.quit(); - }, - }, - ]); - trayRef.setContextMenu(menu); - trayRef.on('click', (): void => { - if (mainWindow.isDestroyed()) return; - if (mainWindow.isMinimized()) mainWindow.restore(); - if (mainWindow.isVisible()) mainWindow.focus(); - else mainWindow.show(); - }); + if (mainWindow.isVisible()) mainWindow.focus(); + else mainWindow.show(); + }); + } + } catch (err: unknown) { + // Swallow: the overlay badge below is the user-visible bit. A missing + // systray icon is cosmetic and shouldn't take the unread handler with it. + console.warn('[tray] systray init failed, overlay badge still active', err); + } ipcMain.handle( CHANNELS.TRAY_UNREAD,