From d69256fb4d6b78a62ad8fcf09d2eb01d7a68e767 Mon Sep 17 00:00:00 2001 From: prism Date: Tue, 27 Jan 2026 23:50:46 +0100 Subject: [PATCH] add equicord tray menu for desktop discord (#694) --- browser/VencordNativeStub.ts | 5 ++ src/Vencord.ts | 39 +++++++++++++++- src/VencordNative.ts | 6 +++ src/main/patcher.ts | 4 ++ src/main/trayMenu.ts | 90 ++++++++++++++++++++++++++++++++++++ src/shared/IpcEvents.ts | 4 ++ 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/main/trayMenu.ts diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 861aa00ca..46cba9d0c 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -135,4 +135,9 @@ window.VencordNative = { pluginHelpers: {} as any, csp: {} as any, + tray: { + setUpdateState: NOOP, + onCheckUpdates: NOOP, + onRepair: NOOP, + }, }; diff --git a/src/Vencord.ts b/src/Vencord.ts index a1ae66b74..f1bbb2bf1 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -36,14 +36,14 @@ import { createAndAppendStyle } from "@utils/css"; import { StartAt } from "@utils/types"; import { get as dsGet } from "./api/DataStore"; -import { showNotice } from "./api/Notices"; +import { popNotice, showNotice } from "./api/Notices"; import { NotificationData, showNotification } from "./api/Notifications"; import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager"; import { PlainSettings, Settings, SettingsStore } from "./api/Settings"; import { getCloudSettings, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync"; import { localStorage } from "./utils/localStorage"; import { relaunch } from "./utils/native"; -import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; +import { checkForUpdates, isOutdated as getIsOutdated, update, UpdateLogger } from "./utils/updater"; import { onceReady } from "./webpack"; import { openUserSettingsPanel } from "./webpack/common"; import { patches } from "./webpack/patchWebpack"; @@ -141,6 +141,7 @@ async function runUpdateCheck() { try { const isOutdated = await checkForUpdates(); + if (IS_DISCORD_DESKTOP) VencordNative.tray.setUpdateState(isOutdated); if (!isOutdated) return; if (Settings.autoUpdate) { @@ -171,11 +172,45 @@ async function runUpdateCheck() { } } +function initTrayIpc() { + if (IS_WEB || IS_UPDATER_DISABLED) return; + + VencordNative.tray.onCheckUpdates(async () => { + try { + const isOutdated = await checkForUpdates(); + VencordNative.tray.setUpdateState(isOutdated); + + if (isOutdated) { + showNotice("An Equicord update is available!", "View Update", () => openSettingsTabModal(UpdaterTab!)); + } else { + showNotice("No updates available, you're on the latest version!", "OK", popNotice); + } + } catch (err) { + UpdateLogger.error("Failed to check for updates from tray", err); + showNotice("Failed to check for updates, check the console for more info", "OK", popNotice); + } + }); + + VencordNative.tray.onRepair(async () => { + try { + const res = await VencordNative.updater.rebuild(); + if (!res.ok) throw res.error; + + showNotice("Equicord has been repaired!", "Restart", relaunch); + } catch (err) { + UpdateLogger.error("Failed to repair Equicord", err); + } + }); + + VencordNative.tray.setUpdateState(getIsOutdated); +} + async function init() { await onceReady; startAllPlugins(StartAt.WebpackReady); syncSettings(); + initTrayIpc(); if (!IS_DEV && !IS_WEB && !IS_UPDATER_DISABLED) { runUpdateCheck(); diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 4c2232ff9..9e3a3aefb 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -98,5 +98,11 @@ export default { invoke(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName), }, + tray: { + setUpdateState: (available: boolean) => ipcRenderer.send(IpcEvents.SET_TRAY_UPDATE_STATE, available), + onCheckUpdates: (cb: () => void) => { ipcRenderer.on(IpcEvents.TRAY_CHECK_UPDATES, cb); }, + onRepair: (cb: () => void) => { ipcRenderer.on(IpcEvents.TRAY_REPAIR_EQUICORD, cb); }, + }, + pluginHelpers: PluginHelpers }; diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 6b042400e..60cc515f4 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -21,6 +21,7 @@ import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import { dirname, join } from "path"; import { RendererSettings } from "./settings"; +import { patchTrayMenu } from "./trayMenu"; import { IS_VANILLA } from "./utils/constants"; console.log("[Equicord] Starting up..."); @@ -40,6 +41,9 @@ app.setAppPath(asarPath); if (!IS_VANILLA) { const settings = RendererSettings.store; + + patchTrayMenu(); + // Repatch after host updates on Windows if (process.platform === "win32") { require("./patchWin32Updater"); diff --git a/src/main/trayMenu.ts b/src/main/trayMenu.ts new file mode 100644 index 000000000..4a8a503a1 --- /dev/null +++ b/src/main/trayMenu.ts @@ -0,0 +1,90 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { IpcEvents } from "@shared/IpcEvents"; +import { BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, shell } from "electron"; + +import { SETTINGS_DIR } from "./utils/constants"; + +let cachedUpdateAvailable = false; + +ipcMain.on(IpcEvents.SET_TRAY_UPDATE_STATE, (_, available: boolean) => { + cachedUpdateAvailable = available; +}); + +function getMainWindow(): BrowserWindow | undefined { + return BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; +} + +function sendToRenderer(event: IpcEvents): void { + getMainWindow()?.webContents.send(event); +} + +function findInsertIndex(template: MenuItemConstructorOptions[]): number { + const openIndex = template.findIndex(item => { + const label = item.label?.toLowerCase() ?? ""; + return label.includes("open") || label.includes("show"); + }); + return openIndex !== -1 ? openIndex + 1 : 0; +} + +function isTrayMenu(template: MenuItemConstructorOptions[]): boolean { + if (!template.length) return false; + + const hasOpenOrShow = template.some(item => { + const label = item.label?.toLowerCase() ?? ""; + return label.includes("open") || label.includes("show"); + }); + + const hasQuit = template.some(item => + item.label?.toLowerCase().includes("quit") || item.role === "quit" + ); + + const isNotAppMenu = !template.some(item => + item.label === "&File" || item.label === "File" || + item.label === "&Edit" || item.label === "Edit" + ); + + return hasOpenOrShow && hasQuit && isNotAppMenu; +} + +function createEquicordMenuItems(): MenuItemConstructorOptions[] { + return [ + { + label: "Equicord", + submenu: [ + { + label: cachedUpdateAvailable ? "Update Equicord" : "Check for Updates", + click: () => sendToRenderer(IpcEvents.TRAY_CHECK_UPDATES) + }, + { + label: "Repair Equicord", + click: () => sendToRenderer(IpcEvents.TRAY_REPAIR_EQUICORD) + }, + { type: "separator" }, + { + label: "Open Settings Folder", + click: () => shell.openPath(SETTINGS_DIR) + } + ] + }, + { type: "separator" } + ]; +} + +export function patchTrayMenu(): void { + const originalBuildFromTemplate = Menu.buildFromTemplate; + + Menu.buildFromTemplate = function (template: MenuItemConstructorOptions[]) { + if (isTrayMenu(template)) { + const insertIndex = findInsertIndex(template); + const equicordItems = createEquicordMenuItems(); + template.splice(insertIndex, 0, ...equicordItems); + } + + return originalBuildFromTemplate.call(this, template); + }; +} diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 974fe0b5f..e5dfeffb1 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -51,4 +51,8 @@ export const enum IpcEvents { GET_RENDERER_CSS = "VencordGetRendererCss", RENDERER_CSS_UPDATE = "VencordRendererCssUpdate", PRELOAD_GET_RENDERER_JS = "VencordPreloadGetRendererJs", + + SET_TRAY_UPDATE_STATE = "EquicordSetTrayUpdateState", + TRAY_REPAIR_EQUICORD = "EquicordTrayRepairEquicord", + TRAY_CHECK_UPDATES = "EquicordTrayCheckUpdates", }