import type {NativeImage, WebviewTag} from "electron"; import {remote} from "electron"; import path from "path"; import * as ConfigUtil from "../../common/config-util"; import type {RendererMessage} from "../../common/typed-ipc"; import {ipcRenderer} from "./typed-ipc-renderer"; const {Tray, Menu, nativeImage, BrowserWindow} = remote; let tray: Electron.Tray | null = null; const ICON_DIR = "../../resources/tray"; const TRAY_SUFFIX = "tray"; const APP_ICON = path.join(__dirname, ICON_DIR, TRAY_SUFFIX); const iconPath = (): string => { if (process.platform === "linux") { return APP_ICON + "linux.png"; } return ( APP_ICON + (process.platform === "win32" ? "win.ico" : "macOSTemplate.png") ); }; const winUnreadTrayIconPath = (): string => APP_ICON + "unread.ico"; let unread = 0; const trayIconSize = (): number => { switch (process.platform) { case "darwin": return 20; case "win32": return 100; case "linux": return 100; default: return 80; } }; // Default config for Icon we might make it OS specific if needed like the size const config = { pixelRatio: window.devicePixelRatio, unreadCount: 0, showUnreadCount: true, unreadColor: "#000000", readColor: "#000000", unreadBackgroundColor: "#B9FEEA", readBackgroundColor: "#B9FEEA", size: trayIconSize(), thick: process.platform === "win32", }; const renderCanvas = function (arg: number): HTMLCanvasElement { config.unreadCount = arg; const SIZE = config.size * config.pixelRatio; const PADDING = SIZE * 0.05; const CENTER = SIZE / 2; const HAS_COUNT = config.showUnreadCount && config.unreadCount; const color = config.unreadCount ? config.unreadColor : config.readColor; const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor; const canvas = document.createElement("canvas"); canvas.width = SIZE; canvas.height = SIZE; const ctx = canvas.getContext("2d")!; // Circle // If (!config.thick || config.thick && HAS_COUNT) { ctx.beginPath(); ctx.arc(CENTER, CENTER, SIZE / 2 - PADDING, 0, 2 * Math.PI, false); ctx.fillStyle = backgroundColor; ctx.fill(); ctx.lineWidth = SIZE / (config.thick ? 10 : 20); ctx.strokeStyle = backgroundColor; ctx.stroke(); // Count or Icon if (HAS_COUNT) { ctx.fillStyle = color; ctx.textAlign = "center"; if (config.unreadCount > 99) { ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.4}px Helvetica`; ctx.fillText("99+", CENTER, CENTER + SIZE * 0.15); } else if (config.unreadCount < 10) { ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.5}px Helvetica`; ctx.fillText(String(config.unreadCount), CENTER, CENTER + SIZE * 0.2); } else { ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.5}px Helvetica`; ctx.fillText(String(config.unreadCount), CENTER, CENTER + SIZE * 0.15); } } return canvas; }; /** * Renders the tray icon as a native image * @param arg: Unread count * @return the native image */ const renderNativeImage = function (arg: number): NativeImage { if (process.platform === "win32") { return nativeImage.createFromPath(winUnreadTrayIconPath()); } const canvas = renderCanvas(arg); const pngData = nativeImage .createFromDataURL(canvas.toDataURL("image/png")) .toPNG(); return nativeImage.createFromBuffer(pngData, { scaleFactor: config.pixelRatio, }); }; function sendAction( channel: Channel, ...args: Parameters ): void { const win = BrowserWindow.getAllWindows()[0]; if (process.platform === "darwin") { win.restore(); } ipcRenderer.sendTo(win.webContents.id, channel, ...args); } const createTray = function (): void { const contextMenu = Menu.buildFromTemplate([ { label: "Zulip", click() { ipcRenderer.send("focus-app"); }, }, { label: "Settings", click() { ipcRenderer.send("focus-app"); sendAction("open-settings"); }, }, { type: "separator", }, { label: "Quit", click() { ipcRenderer.send("quit-app"); }, }, ]); tray = new Tray(iconPath()); tray.setContextMenu(contextMenu); if (process.platform === "linux" || process.platform === "win32") { tray.on("click", () => { ipcRenderer.send("toggle-app"); }); } }; ipcRenderer.on("destroytray", (_event: Event) => { if (!tray) { return; } tray.destroy(); if (tray.isDestroyed()) { tray = null; } else { throw new Error("Tray icon not properly destroyed."); } }); ipcRenderer.on("tray", (_event: Event, arg: number): void => { if (!tray) { return; } // We don't want to create tray from unread messages on macOS since it already has dock badges. if (process.platform === "linux" || process.platform === "win32") { if (arg === 0) { unread = arg; tray.setImage(iconPath()); tray.setToolTip("No unread messages"); } else { unread = arg; const image = renderNativeImage(arg); tray.setImage(image); tray.setToolTip(`${arg} unread messages`); } } }); function toggleTray(): void { let state; if (tray) { state = false; tray.destroy(); if (tray.isDestroyed()) { tray = null; } ConfigUtil.setConfigItem("trayIcon", false); } else { state = true; createTray(); if (process.platform === "linux" || process.platform === "win32") { const image = renderNativeImage(unread); tray!.setImage(image); tray!.setToolTip(`${unread} unread messages`); } ConfigUtil.setConfigItem("trayIcon", true); } const selector = "webview:not([class*=disabled])"; const webview: WebviewTag = document.querySelector(selector)!; ipcRenderer.sendTo(webview.getWebContentsId(), "toggle-tray", state); } ipcRenderer.on("toggletray", toggleTray); if (ConfigUtil.getConfigItem("trayIcon", true)) { createTray(); } export {};