Updater: show update dialog instead of forcing updates (#1184)

This commit is contained in:
V
2025-10-19 00:58:48 +00:00
committed by GitHub
parent eb3dae897d
commit 9f0af48355
19 changed files with 492 additions and 69 deletions
+1 -5
View File
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./updater";
import "./ipc";
import "./userAssets";
import { app, BrowserWindow, nativeTheme } from "electron";
import { autoUpdater } from "electron-updater";
import { checkCommandLineForHelpOrVersion } from "./cli";
import { DATA_DIR } from "./constants";
@@ -22,10 +22,6 @@ import { isDeckGameMode } from "./utils/steamOS";
checkCommandLineForHelpOrVersion();
if (!IS_DEV) {
autoUpdater.checkForUpdatesAndNotify();
}
console.log("Vesktop v" + app.getVersion());
// Make the Vencord files use our DATA_DIR
+81
View File
@@ -0,0 +1,81 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, ipcMain } from "electron";
import { autoUpdater, UpdateInfo } from "electron-updater";
import { join } from "path";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
import { VIEW_DIR } from "shared/paths";
import { Millis } from "shared/utils/millis";
import { State } from "./settings";
import { handle } from "./utils/ipcWrappers";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
let updaterWindow: BrowserWindow | null = null;
autoUpdater.on("update-available", update => {
if (State.store.updater?.ignoredVersion === update.version) return;
if ((State.store.updater?.snoozeUntil ?? 0) > Date.now()) return;
openUpdater(update);
});
autoUpdater.on("update-downloaded", () => setTimeout(() => autoUpdater.quitAndInstall(), 100));
autoUpdater.on("download-progress", p =>
updaterWindow?.webContents.send(UpdaterIpcEvents.DOWNLOAD_PROGRESS, p.percent)
);
autoUpdater.on("error", err => updaterWindow?.webContents.send(UpdaterIpcEvents.ERROR, err.message));
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.fullChangelog = true;
const isOutdated = autoUpdater.checkForUpdates().then(res => Boolean(res?.isUpdateAvailable));
handle(IpcEvents.UPDATER_IS_OUTDATED, () => isOutdated);
handle(IpcEvents.UPDATER_OPEN, async () => {
const res = await autoUpdater.checkForUpdates();
if (res?.isUpdateAvailable && res.updateInfo) openUpdater(res.updateInfo);
});
function openUpdater(update: UpdateInfo) {
updaterWindow = new BrowserWindow({
title: "Vesktop Updater",
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
},
minHeight: 400,
minWidth: 750
});
makeLinksOpenExternally(updaterWindow);
handle(UpdaterIpcEvents.GET_DATA, () => ({ update, version: app.getVersion() }));
handle(UpdaterIpcEvents.INSTALL, async () => {
await autoUpdater.downloadUpdate();
});
handle(UpdaterIpcEvents.SNOOZE_UPDATE, () => {
State.store.updater ??= {};
State.store.updater.snoozeUntil = Date.now() + 1 * Millis.DAY;
updaterWindow?.close();
});
handle(UpdaterIpcEvents.IGNORE_UPDATE, () => {
State.store.updater ??= {};
State.store.updater.ignoredVersion = update.version;
updaterWindow?.close();
});
updaterWindow.on("closed", () => {
ipcMain.removeHandler(UpdaterIpcEvents.GET_DATA);
ipcMain.removeHandler(UpdaterIpcEvents.INSTALL);
ipcMain.removeHandler(UpdaterIpcEvents.SNOOZE_UPDATE);
ipcMain.removeHandler(UpdaterIpcEvents.IGNORE_UPDATE);
updaterWindow = null;
});
updaterWindow.loadFile(join(VIEW_DIR, "updater.html"));
}
+3 -3
View File
@@ -6,7 +6,7 @@
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
import { DISCORD_HOSTNAMES } from "main/constants";
import { IpcEvents } from "shared/IpcEvents";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
export function validateSender(frame: WebFrameMain | null, event: string) {
if (!frame) throw new Error(`ipc[${event}]: No sender frame`);
@@ -25,14 +25,14 @@ export function validateSender(frame: WebFrameMain | null, event: string) {
}
}
export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
export function handleSync(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
ipcMain.on(event, (e, ...args) => {
validateSender(e.senderFrame, event);
e.returnValue = cb(e, ...args);
});
}
export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
export function handle(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(event, (e, ...args) => {
validateSender(e.senderFrame, event);
return cb(e, ...args);
+3 -1
View File
@@ -32,7 +32,9 @@ export const VesktopNative = {
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY),
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION)
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION),
isOutdated: () => invoke<boolean>(IpcEvents.UPDATER_IS_OUTDATED),
openUpdater: () => invoke<void>(IpcEvents.UPDATER_OPEN)
},
autostart: {
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
+3 -3
View File
@@ -5,12 +5,12 @@
*/
import { ipcRenderer } from "electron";
import { IpcEvents } from "shared/IpcEvents";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
export function invoke<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
}
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
export function sendSync<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { contextBridge, ipcRenderer } from "electron";
import type { UpdateInfo } from "electron-updater";
import { UpdaterIpcEvents } from "shared/IpcEvents";
import { invoke } from "./typedIpc";
contextBridge.exposeInMainWorld("VesktopUpdaterNative", {
getData: () => invoke<UpdateInfo>(UpdaterIpcEvents.GET_DATA),
installUpdate: () => invoke(UpdaterIpcEvents.INSTALL),
onProgress: (cb: (percent: number) => void) => {
ipcRenderer.on(UpdaterIpcEvents.DOWNLOAD_PROGRESS, (_, percent: number) => cb(percent));
},
onError: (cb: (message: string) => void) => {
ipcRenderer.on(UpdaterIpcEvents.ERROR, (_, message: string) => cb(message));
},
snoozeUpdate: () => invoke(UpdaterIpcEvents.SNOOZE_UPDATE),
ignoreUpdate: () => invoke(UpdaterIpcEvents.IGNORE_UPDATE)
});
@@ -6,6 +6,7 @@
import "./screenSharePicker.css";
import { classNameFactory } from "@vencord/types/api/Styles";
import { FormSwitch } from "@vencord/types/components";
import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
import { onceReady } from "@vencord/types/webpack";
@@ -15,7 +16,7 @@ import type { Dispatch, SetStateAction } from "react";
import { MediaEngineStore } from "renderer/common";
import { addPatch } from "renderer/patches/shared";
import { State, useSettings, useVesktopState } from "renderer/settings";
import { classNameFactory, isLinux, isWindows } from "renderer/utils";
import { isLinux, isWindows } from "renderer/utils";
const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const;
const StreamFps = ["15", "30", "60"] as const;
@@ -17,7 +17,7 @@ import {
import { Button, Forms, Text, Toasts } from "@vencord/types/webpack/common";
import { Settings } from "shared/settings";
import { SettingsComponent } from "./Settings";
import { cl, SettingsComponent } from "./Settings";
export const DeveloperOptionsButton: SettingsComponent = ({ settings }) => {
return <Button onClick={() => openDeveloperOptionsModal(settings)}>Open Developer Settings</Button>;
@@ -41,7 +41,7 @@ function openDeveloperOptionsModal(settings: Settings) {
<Forms.FormTitle tag="h5" className={Margins.top16}>
Debugging
</Forms.FormTitle>
<div className="vcd-settings-button-grid">
<div className={cl("button-grid")}>
<Button onClick={() => VesktopNative.debug.launchGpu()}>Open chrome://gpu</Button>
<Button onClick={() => VesktopNative.debug.launchWebrtcInternals()}>
Open chrome://webrtc-internals
@@ -75,7 +75,7 @@ const VencordLocationPicker: SettingsComponent = ({ settings }) => {
"the default location"
)}
</Forms.FormText>
<div className="vcd-settings-button-grid">
<div className={cl("button-grid")}>
<Button
size={Button.Sizes.SMALL}
onClick={async () => {
+10 -5
View File
@@ -6,6 +6,7 @@
import "./settings.css";
import { classNameFactory } from "@vencord/types/api/Styles";
import { ErrorBoundary } from "@vencord/types/components";
import { Forms, Text } from "@vencord/types/webpack/common";
import { ComponentType } from "react";
@@ -16,6 +17,7 @@ import { AutoStartToggle } from "./AutoStartToggle";
import { DeveloperOptionsButton } from "./DeveloperOptions";
import { DiscordBranchPicker } from "./DiscordBranchPicker";
import { NotificationBadgeToggle } from "./NotificationBadgeToggle";
import { Updater } from "./Updater";
import { UserAssetsButton } from "./UserAssets";
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
import { WindowsTransparencyControls } from "./WindowsTransparencyControls";
@@ -29,6 +31,8 @@ interface BooleanSetting {
invisible?(): boolean;
}
export const cl = classNameFactory("vcd-settings-");
export type SettingsComponent = ComponentType<{ settings: typeof Settings.store }>;
const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>> = {
@@ -144,12 +148,12 @@ function SettingsSections() {
const Settings = useSettings();
const sections = Object.entries(SettingsOptions).map(([title, settings], i, arr) => (
<div key={title} className="vcd-settings-category">
<Text variant="heading-lg/semibold" color="header-primary" className="vcd-settings-category-title">
<div key={title} className={cl("category")}>
<Text variant="heading-lg/semibold" color="header-primary" className={cl("category-title")}>
{title}
</Text>
<div className="vcd-settings-category-content">
<div className={cl("category-content")}>
{settings.map(Setting => {
if (typeof Setting === "function") return <Setting settings={Settings} />;
@@ -169,7 +173,7 @@ function SettingsSections() {
})}
</div>
{i < arr.length - 1 && <Forms.FormDivider className="vcd-settings-category-divider" />}
{i < arr.length - 1 && <Forms.FormDivider className={cl("category-divider")} />}
</div>
));
@@ -180,9 +184,10 @@ export default ErrorBoundary.wrap(
function SettingsUI() {
return (
<section>
<Text variant="heading-xl/semibold" color="header-primary" className="vcd-settings-title">
<Text variant="heading-xl/semibold" color="header-primary" className={cl("title")}>
Vesktop Settings
</Text>
<Updater />
<SettingsSections />
</section>
);
@@ -0,0 +1,31 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useAwaiter } from "@vencord/types/utils";
import { Button, Text } from "@vencord/types/webpack/common";
import { cl } from "./Settings";
export function Updater() {
const [isOutdated] = useAwaiter(VesktopNative.app.isOutdated);
if (!isOutdated) return null;
return (
<div className={cl("updater-card")}>
<Text variant="text-md/semibold">Your Vesktop is outdated!</Text>
<Text variant="text-sm/normal">Staying up to date is important for security and stability.</Text>
<Button
onClick={() => VesktopNative.app.openUpdater()}
size={Button.Sizes.SMALL}
color={Button.Colors.TRANSPARENT}
>
Open Updater
</Button>
</div>
);
}
@@ -7,6 +7,8 @@
import { FormSwitch } from "@vencord/types/components";
import { ComponentProps } from "react";
import { cl } from "./Settings";
export function VesktopSettingsSwitch(props: ComponentProps<typeof FormSwitch>) {
return <FormSwitch {...props} hideBorder className="vcd-settings-switch" />;
return <FormSwitch {...props} hideBorder className={cl("switch")} />;
}
@@ -31,4 +31,17 @@
.vcd-settings-switch {
margin-bottom: 0;
}
.vcd-settings-updater-card {
padding: 1em;
margin-bottom: 1em;
display: grid;
gap: 0.5em;
border-radius: 8px;
background-color: var(--bg-secondary);
background: var(--background-feedback-warning);
border: 1px solid var(--info-warning-foreground);
color: var(--text-feedback-warning);
}
-23
View File
@@ -19,26 +19,3 @@ const { platform } = navigator;
export const isWindows = platform.startsWith("Win");
export const isMac = platform.startsWith("Mac");
export const isLinux = platform.startsWith("Linux");
type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false | null | undefined | 0 | "";
/**
* @param prefix The prefix to add to each class, defaults to `""`
* @returns A classname generator function
* @example
* const cl = classNameFactory("plugin-");
*
* cl("base", ["item", "editable"], { selected: null, disabled: true })
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
*/
export const classNameFactory =
(prefix: string = "") =>
(...args: ClassNameFactoryArg[]) => {
const classNames = new Set<string>();
for (const arg of args) {
if (arg && typeof arg === "string") classNames.add(arg);
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
else if (arg && typeof arg === "object")
Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
}
return Array.from(classNames, name => prefix + name).join(" ");
};
+11 -3
View File
@@ -27,9 +27,8 @@ export const enum IpcEvents {
GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR",
SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",
UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA",
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
UPDATER_IS_OUTDATED = "VCD_UPDATER_IS_OUTDATED",
UPDATER_OPEN = "VCD_UPDATER_OPEN",
SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
@@ -62,6 +61,15 @@ export const enum IpcEvents {
CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET"
}
export const enum UpdaterIpcEvents {
GET_DATA = "VCD_UPDATER_GET_DATA",
INSTALL = "VCD_UPDATER_INSTALL",
DOWNLOAD_PROGRESS = "VCD_UPDATER_DOWNLOAD_PROGRESS",
ERROR = "VCD_UPDATER_ERROR",
SNOOZE_UPDATE = "VCD_UPDATER_SNOOZE_UPDATE",
IGNORE_UPDATE = "VCD_UPDATER_IGNORE_UPDATE"
}
export const enum IpcCommands {
RPC_ACTIVITY = "rpc:activity",
RPC_INVITE = "rpc:invite",
+5
View File
@@ -58,4 +58,9 @@ export interface State {
linuxAutoStartEnabled?: boolean;
vencordDir?: string;
updater?: {
ignoredVersion?: string;
snoozeUntil?: number;
};
}
+12
View File
@@ -0,0 +1,12 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const enum Millis {
SECOND = 1000,
MINUTE = 60 * SECOND,
HOUR = 60 * MINUTE,
DAY = 24 * HOUR
}