// System tray + Windows taskbar overlay badge. Renderer pushes the // current aggregate unread count via CHANNELS.TRAY_UNREAD; we update the // tooltip and (on Windows) set an overlay icon on the main window's // taskbar button. // // Overlay image is a pre-rendered 16x16 red dot embedded as base64 so // the module is self-contained — no runtime canvas dependency. import { app, BrowserWindow, ipcMain, Menu, nativeImage, type NativeImage, Tray, } from 'electron'; import path from 'node:path'; import { CHANNELS } from '../ipc-types'; let trayRef: Tray | null = null; let overlayImage: NativeImage | null = null; function resolveIconPath(): string { return path.join(process.resourcesPath || app.getAppPath(), 'icon.ico'); } function resolveIconPathDev(): string { return path.join(app.getAppPath(), 'resources', 'icon.ico'); } function loadTrayIcon(): NativeImage { for (const p of [resolveIconPath(), resolveIconPathDev()]) { try { const img = nativeImage.createFromPath(p); if (!img.isEmpty()) return img; } catch { /* try next */ } } return nativeImage.createEmpty(); } function buildOverlay(): NativeImage { if (overlayImage) return overlayImage; // 16x16 PNG, solid red circle. Inline base64 so no external file lookup. const base64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAPElEQVR42mNkYGD4z0AGYBxVSF2F//' + '//Z2BgYGD4/58kBYz4FDAxMDAwMDIwMDD8//+foArGUYWjCoc1AABTgwUBf3lZtAAAAABJRU5ErkJggg=='; overlayImage = nativeImage.createFromBuffer(Buffer.from(base64, 'base64')); return overlayImage; } export function register(mainWindow: BrowserWindow): void { const icon = loadTrayIcon(); trayRef = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon); trayRef.setToolTip('ChatApp'); 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(); if (mainWindow.isVisible()) mainWindow.focus(); else mainWindow.show(); }); ipcMain.handle(CHANNELS.TRAY_UNREAD, async (_evt, count: number): Promise => { const n = Math.max(0, Math.floor(Number(count) || 0)); if (trayRef && !trayRef.isDestroyed()) { trayRef.setToolTip(n > 0 ? `ChatApp — ${n} unread` : 'ChatApp'); } if (mainWindow.isDestroyed()) return; if (process.platform === 'win32') { if (n > 0) { mainWindow.setOverlayIcon(buildOverlay(), `${n} unread`); } else { mainWindow.setOverlayIcon(null, ''); } } }); app.on('before-quit', () => { try { trayRef?.destroy(); } catch { /* already gone */ } trayRef = null; }); }