diff --git a/package.json b/package.json index a7d8a9ad05..079bb7ab09 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "electron-window-state": "^5.0.3", "keytar-forked": "7.10.0", "minimist": "^1.2.6", - "png-to-ico": "^2.1.1", + "png-to-ico": "^2.1.8", "uuid": "^11.0.0" }, "devDependencies": { diff --git a/src/badge.ts b/src/badge.ts new file mode 100644 index 0000000000..c28ae7f5cb --- /dev/null +++ b/src/badge.ts @@ -0,0 +1,49 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { app, ipcMain, type IpcMainEvent, nativeImage } from "electron"; + +import { _t } from "./language-helper.js"; + +// Handles calculating the correct "badge" for the window, for notifications and error states. +// Tray icon updates are handled in tray.ts + +if (process.platform === "win32") { + // We only use setOverlayIcon on Windows as it's only supported on that platform, but has good support + // from all the Windows variants we support. + // https://www.electronjs.org/docs/latest/api/browser-window#winsetoverlayiconoverlay-description-windows + ipcMain.on( + "setBadgeCount", + function (_ev: IpcMainEvent, count: number, imageBuffer?: Buffer, isError?: boolean): void { + if (count === 0) { + // Flash frame is set to true in ipc.ts "loudNotification" + global.mainWindow?.flashFrame(false); + } + if (imageBuffer) { + global.mainWindow?.setOverlayIcon( + nativeImage.createFromBuffer(Buffer.from(imageBuffer)), + isError + ? _t("icon_overlay|description_error") + : _t("icon_overlay|description_notifications", { count }), + ); + } else { + global.mainWindow?.setOverlayIcon(null, ""); + } + }, + ); +} else { + // only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron + // has some Windows support too, and in some Windows environments this leads to two badges rendering atop + // each other. See https://github.com/vector-im/element-web/issues/16942 + ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void { + if (count === 0) { + // Flash frame is set to true in ipc.ts "loudNotification" + global.mainWindow?.flashFrame(false); + } + app.badgeCount = count; + }); +} diff --git a/src/electron-main.ts b/src/electron-main.ts index 0241b3be6f..d907312298 100644 --- a/src/electron-main.ts +++ b/src/electron-main.ts @@ -1,5 +1,5 @@ /* -Copyright 2018-2024 New Vector Ltd. +Copyright 2018-2025 New Vector Ltd. Copyright 2017-2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd @@ -23,6 +23,7 @@ import minimist from "minimist"; import "./ipc.js"; import "./seshat.js"; import "./settings.js"; +import "./badge.js"; import * as tray from "./tray.js"; import Store from "./store.js"; import { buildMenuTemplate } from "./vectormenu.js"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 27690be501..d4882be5e4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -35,6 +35,13 @@ "file_menu": { "label": "File" }, + "icon_overlay": { + "description_error": "Error", + "description_notifications": { + "one": "You have %(count)s unread notification.", + "other": "You have %(count)s unread notifications." + } + }, "menu": { "hide": "Hide", "hide_others": "Hide Others", diff --git a/src/ipc.ts b/src/ipc.ts index 3b3d3cce9c..580e335663 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -1,5 +1,5 @@ /* -Copyright 2022-2024 New Vector Ltd. +Copyright 2022-2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. @@ -12,18 +12,6 @@ import { randomArray } from "./utils.js"; import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback.js"; import Store, { clearDataAndRelaunch } from "./store.js"; -ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void { - if (process.platform !== "win32") { - // only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron - // has some Windows support too, and in some Windows environments this leads to two badges rendering atop - // each other. See https://github.com/vector-im/element-web/issues/16942 - app.badgeCount = count; - } - if (count === 0) { - global.mainWindow?.flashFrame(false); - } -}); - let focusHandlerAttached = false; ipcMain.on("loudNotification", function (): void { if (process.platform === "win32" || process.platform === "linux") { diff --git a/src/preload.cts b/src/preload.cts index f8922ab632..21d7593983 100644 --- a/src/preload.cts +++ b/src/preload.cts @@ -55,13 +55,17 @@ contextBridge.exposeInMainWorld("electron", { sessionId: string; config: IConfigOptions; supportedSettings: Record; + /** + * Do we need to render badge overlays for new notifications? + */ + supportsBadgeOverlay: boolean; }> { const [{ protocol, sessionId }, config, supportedSettings] = await Promise.all([ ipcRenderer.invoke("getProtocol"), ipcRenderer.invoke("getConfig"), ipcRenderer.invoke("getSupportedSettings"), ]); - return { protocol, sessionId, config, supportedSettings }; + return { protocol, sessionId, config, supportedSettings, supportsBadgeOverlay: process.platform === "win32" }; }, async setSettingValue(settingName: string, value: any): Promise { diff --git a/src/tray.ts b/src/tray.ts index bbf477bfe7..0e906fb65d 100644 --- a/src/tray.ts +++ b/src/tray.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024-2025 New Vector Ltd. Copyright 2017 Karl Glatz Copyright 2017 OpenMarket Ltd @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. */ import { app, Tray, Menu, nativeImage } from "electron"; +import { v5 as uuidv5 } from "uuid"; +import { writeFile } from "node:fs/promises"; import pngToIco from "png-to-ico"; import path from "node:path"; -import fs from "node:fs"; -import { v5 as uuidv5 } from "uuid"; import { _t } from "./language-helper.js"; @@ -71,6 +71,7 @@ export function create(config: IConfig): void { initApplicationMenu(); trayIcon.on("click", toggleWin); + // See also, badge.ts let lastFavicon: string | null = null; global.mainWindow?.webContents.on("page-favicon-updated", async function (ev, favicons) { if (!favicons || favicons.length <= 0 || !favicons[0].startsWith("data:")) { @@ -92,15 +93,17 @@ export function create(config: IConfig): void { if (process.platform === "win32") { try { const icoPath = path.join(app.getPath("temp"), "win32_element_icon.ico"); - fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG())); + await writeFile(icoPath, await pngToIco(newFavicon.toPNG())); newFavicon = nativeImage.createFromPath(icoPath); } catch (e) { console.error("Failed to make win32 ico", e); } + // Always update the tray icon for Windows. + trayIcon?.setImage(newFavicon); + } else { + trayIcon?.setImage(newFavicon); + global.mainWindow?.setIcon(newFavicon); } - - trayIcon?.setImage(newFavicon); - global.mainWindow?.setIcon(newFavicon); }); global.mainWindow?.webContents.on("page-title-updated", function (ev, title) { diff --git a/yarn.lock b/yarn.lock index d8bd03b458..61e14e8b9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6237,7 +6237,7 @@ pluralizers@^0.1.7: resolved "https://registry.yarnpkg.com/pluralizers/-/pluralizers-0.1.7.tgz#8d38dd0a1b660e739b10ab2eab10b684c9d50142" integrity sha512-mw6AejUiCaMQ6uPN9ObjJDTnR5AnBSmnHHy3uVTbxrSFSxO5scfwpTs8Dxyb6T2v7GSulhvOq+pm9y+hXUvtOA== -png-to-ico@^2.1.1: +png-to-ico@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/png-to-ico/-/png-to-ico-2.1.8.tgz#fdc2eda6f197df1d6c33400707e36c3b802ac6dd" integrity sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==