diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 764c2be52..b1e89b40c 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -27,7 +27,7 @@ import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; import { createApplicationMenu } from "./main/menu"; import { isDev } from "./main/util"; -import { setupAutoUpdater } from "./services/appUpdater"; +import { setupAutoUpdater } from "./services/app-update"; import { initWatcher } from "./services/chokidar"; let appIsQuitting = false; @@ -145,7 +145,7 @@ const attachEventHandlers = (mainWindow: BrowserWindow) => { // Let ipcRenderer know when mainWindow is in the foreground so that it can // in turn inform the renderer process. mainWindow.on("focus", () => - mainWindow.webContents.send("onMainWindowFocus"), + mainWindow.webContents.send("mainWindowFocus"), ); }; diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index b9ddb752b..ecb3e2010 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -12,14 +12,11 @@ import type { FSWatcher } from "chokidar"; import { ipcMain } from "electron/main"; import { appVersion, - muteUpdateNotification, skipAppUpdate, updateAndRestart, -} from "../services/appUpdater"; -import { - clipImageEmbedding, - clipTextEmbedding, -} from "../services/clip"; + updateOnNextRestart, +} from "../services/app-update"; +import { clipImageEmbedding, clipTextEmbedding } from "../services/clip"; import { runFFmpegCmd } from "../services/ffmpeg"; import { getDirFiles } from "../services/fs"; import { @@ -108,14 +105,14 @@ export const attachIPCHandlers = () => { // - App update - ipcMain.on("update-and-restart", () => updateAndRestart()); + ipcMain.on("updateAndRestart", () => updateAndRestart()); - ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version)); - - ipcMain.on("mute-update-notification", (_, version) => - muteUpdateNotification(version), + ipcMain.on("updateOnNextRestart", (_, version) => + updateOnNextRestart(version), ); + ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version)); + // - Conversion ipcMain.handle("convertToJPEG", (_, fileData, filename) => diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 658932961..c6ac1688a 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -6,7 +6,7 @@ import { shell, } from "electron"; import { setIsAppQuitting } from "../main"; -import { forceCheckForUpdateAndNotify } from "../services/appUpdater"; +import { forceCheckForAppUpdates } from "../services/app-update"; import autoLauncher from "../services/autoLauncher"; import { getHideDockIconPreference, @@ -26,8 +26,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { const macOSOnly = (options: MenuItemConstructorOptions[]) => process.platform == "darwin" ? options : []; - const handleCheckForUpdates = () => - forceCheckForUpdateAndNotify(mainWindow); + const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow); const handleViewChangelog = () => shell.openExternal( diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 5e0ed1b45..07736502b 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -70,8 +70,30 @@ const saveEncryptionKey = (encryptionKey: string): Promise => ipcRenderer.invoke("saveEncryptionKey", encryptionKey); const onMainWindowFocus = (cb?: () => void) => { - ipcRenderer.removeAllListeners("onMainWindowFocus"); - if (cb) ipcRenderer.on("onMainWindowFocus", cb); + ipcRenderer.removeAllListeners("mainWindowFocus"); + if (cb) ipcRenderer.on("mainWindowFocus", cb); +}; + +// - App update + +const onAppUpdateAvailable = ( + cb?: ((updateInfo: AppUpdateInfo) => void) | undefined, +) => { + ipcRenderer.removeAllListeners("appUpdateAvailable"); + if (cb) { + ipcRenderer.on("appUpdateAvailable", (_, updateInfo: AppUpdateInfo) => + cb(updateInfo), + ); + } +}; + +const updateAndRestart = () => ipcRenderer.send("updateAndRestart"); + +const updateOnNextRestart = (version: string) => + ipcRenderer.send("updateOnNextRestart", version); + +const skipAppUpdate = (version: string) => { + ipcRenderer.send("skipAppUpdate", version); }; const fsExists = (path: string): Promise => @@ -79,29 +101,6 @@ const fsExists = (path: string): Promise => // - AUDIT below this -// - App update - -const registerUpdateEventListener = ( - showUpdateDialog: (updateInfo: AppUpdateInfo) => void, -) => { - ipcRenderer.removeAllListeners("show-update-dialog"); - ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => { - showUpdateDialog(updateInfo); - }); -}; - -const updateAndRestart = () => { - ipcRenderer.send("update-and-restart"); -}; - -const skipAppUpdate = (version: string) => { - ipcRenderer.send("skip-app-update", version); -}; - -const muteUpdateNotification = (version: string) => { - ipcRenderer.send("mute-update-notification", version); -}; - // - Conversion const convertToJPEG = ( @@ -310,10 +309,10 @@ contextBridge.exposeInMainWorld("electron", { onMainWindowFocus, // - App update + onAppUpdateAvailable, updateAndRestart, + updateOnNextRestart, skipAppUpdate, - muteUpdateNotification, - registerUpdateEventListener, // - Conversion convertToJPEG, diff --git a/desktop/src/services/app-update.ts b/desktop/src/services/app-update.ts new file mode 100644 index 000000000..ec592095e --- /dev/null +++ b/desktop/src/services/app-update.ts @@ -0,0 +1,98 @@ +import { compareVersions } from "compare-versions"; +import { app, BrowserWindow } from "electron"; +import { default as electronLog } from "electron-log"; +import { autoUpdater } from "electron-updater"; +import { setIsAppQuitting, setIsUpdateAvailable } from "../main"; +import log from "../main/log"; +import { userPreferencesStore } from "../stores/user-preferences"; +import { AppUpdateInfo } from "../types/ipc"; + +export const setupAutoUpdater = (mainWindow: BrowserWindow) => { + autoUpdater.logger = electronLog; + autoUpdater.autoDownload = false; + + const oneDay = 1 * 24 * 60 * 60 * 1000; + setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay); + checkForUpdatesAndNotify(mainWindow); +}; + +/** + * Check for app update check ignoring any previously saved skips / mutes. + */ +export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => { + userPreferencesStore.delete("skipAppVersion"); + userPreferencesStore.delete("muteUpdateNotificationVersion"); + checkForUpdatesAndNotify(mainWindow); +}; + +const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { + try { + const { updateInfo } = await autoUpdater.checkForUpdates(); + const { version } = updateInfo; + + log.debug(() => `Checking for updates found version ${version}`); + + if (compareVersions(version, app.getVersion()) <= 0) { + log.debug(() => "Skipping update, already at latest version"); + return; + } + + if (version === userPreferencesStore.get("skipAppVersion")) { + log.info(`User chose to skip version ${version}`); + return; + } + + const mutedVersion = userPreferencesStore.get( + "muteUpdateNotificationVersion", + ); + if (version === mutedVersion) { + log.info( + `User has muted update notifications for version ${version}`, + ); + return; + } + + const showUpdateDialog = (updateInfo: AppUpdateInfo) => + mainWindow.webContents.send("appUpdateAvailable", updateInfo); + + log.debug(() => "Attempting auto update"); + autoUpdater.downloadUpdate(); + + let timeout: NodeJS.Timeout; + const fiveMinutes = 5 * 60 * 1000; + autoUpdater.on("update-downloaded", () => { + timeout = setTimeout( + () => showUpdateDialog({ autoUpdatable: true, version }), + fiveMinutes, + ); + }); + autoUpdater.on("error", (error) => { + clearTimeout(timeout); + log.error("Auto update failed", error); + showUpdateDialog({ autoUpdatable: false, version }); + }); + + setIsUpdateAvailable(true); + } catch (e) { + log.error("checkForUpdateAndNotify failed", e); + } +}; + +/** + * Return the version of the desktop app + * + * The return value is of the form `v1.2.3`. + */ +export const appVersion = () => `v${app.getVersion()}`; + +export const updateAndRestart = () => { + log.info("Restarting the app to apply update"); + setIsAppQuitting(true); + autoUpdater.quitAndInstall(); +}; + +export const updateOnNextRestart = (version: string) => + userPreferencesStore.set("muteUpdateNotificationVersion", version); + +export const skipAppUpdate = (version: string) => + userPreferencesStore.set("skipAppVersion", version); diff --git a/desktop/src/services/appUpdater.ts b/desktop/src/services/appUpdater.ts deleted file mode 100644 index 517fc98e9..000000000 --- a/desktop/src/services/appUpdater.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { compareVersions } from "compare-versions"; -import { app, BrowserWindow } from "electron"; -import { default as electronLog } from "electron-log"; -import { autoUpdater } from "electron-updater"; -import { setIsAppQuitting, setIsUpdateAvailable } from "../main"; -import log from "../main/log"; -import { AppUpdateInfo } from "../types/ipc"; -import { - clearMuteUpdateNotificationVersion, - clearSkipAppVersion, - getMuteUpdateNotificationVersion, - getSkipAppVersion, - setMuteUpdateNotificationVersion, - setSkipAppVersion, -} from "./userPreference"; - -const FIVE_MIN_IN_MICROSECOND = 5 * 60 * 1000; -const ONE_DAY_IN_MICROSECOND = 1 * 24 * 60 * 60 * 1000; - -export function setupAutoUpdater(mainWindow: BrowserWindow) { - autoUpdater.logger = electronLog; - autoUpdater.autoDownload = false; - checkForUpdateAndNotify(mainWindow); - setInterval( - () => checkForUpdateAndNotify(mainWindow), - ONE_DAY_IN_MICROSECOND, - ); -} - -export function forceCheckForUpdateAndNotify(mainWindow: BrowserWindow) { - try { - clearSkipAppVersion(); - clearMuteUpdateNotificationVersion(); - checkForUpdateAndNotify(mainWindow); - } catch (e) { - log.error("forceCheckForUpdateAndNotify failed", e); - } -} - -async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { - try { - log.debug(() => "checkForUpdateAndNotify"); - const { updateInfo } = await autoUpdater.checkForUpdates(); - log.debug(() => `Update version ${updateInfo.version}`); - if (compareVersions(updateInfo.version, app.getVersion()) <= 0) { - log.debug(() => "Skipping update, already at latest version"); - return; - } - const skipAppVersion = getSkipAppVersion(); - if (skipAppVersion && updateInfo.version === skipAppVersion) { - log.info(`User chose to skip version ${updateInfo.version}`); - return; - } - - let timeout: NodeJS.Timeout; - log.debug(() => "Attempting auto update"); - autoUpdater.downloadUpdate(); - const muteUpdateNotificationVersion = - getMuteUpdateNotificationVersion(); - if ( - muteUpdateNotificationVersion && - updateInfo.version === muteUpdateNotificationVersion - ) { - log.info( - `User has muted update notifications for version ${updateInfo.version}`, - ); - return; - } - autoUpdater.on("update-downloaded", () => { - timeout = setTimeout( - () => - showUpdateDialog(mainWindow, { - autoUpdatable: true, - version: updateInfo.version, - }), - FIVE_MIN_IN_MICROSECOND, - ); - }); - autoUpdater.on("error", (error) => { - clearTimeout(timeout); - log.error("Auto update failed", error); - showUpdateDialog(mainWindow, { - autoUpdatable: false, - version: updateInfo.version, - }); - }); - - setIsUpdateAvailable(true); - } catch (e) { - log.error("checkForUpdateAndNotify failed", e); - } -} - -export function updateAndRestart() { - log.info("user quit the app"); - setIsAppQuitting(true); - autoUpdater.quitAndInstall(); -} - -/** - * Return the version of the desktop app - * - * The return value is of the form `v1.2.3`. - */ -export const appVersion = () => `v${app.getVersion()}`; - -export function skipAppUpdate(version: string) { - setSkipAppVersion(version); -} - -export function muteUpdateNotification(version: string) { - setMuteUpdateNotificationVersion(version); -} - -function showUpdateDialog( - mainWindow: BrowserWindow, - updateInfo: AppUpdateInfo, -) { - mainWindow.webContents.send("show-update-dialog", updateInfo); -} diff --git a/desktop/src/services/userPreference.ts b/desktop/src/services/userPreference.ts index 8074ee4de..c20657aa9 100644 --- a/desktop/src/services/userPreference.ts +++ b/desktop/src/services/userPreference.ts @@ -1,4 +1,4 @@ -import { userPreferencesStore } from "../stores/userPreferences.store"; +import { userPreferencesStore } from "../stores/user-preferences"; export function getHideDockIconPreference() { return userPreferencesStore.get("hideDockIcon"); @@ -7,27 +7,3 @@ export function getHideDockIconPreference() { export function setHideDockIconPreference(shouldHideDockIcon: boolean) { userPreferencesStore.set("hideDockIcon", shouldHideDockIcon); } - -export function getSkipAppVersion() { - return userPreferencesStore.get("skipAppVersion"); -} - -export function setSkipAppVersion(version: string) { - userPreferencesStore.set("skipAppVersion", version); -} - -export function getMuteUpdateNotificationVersion() { - return userPreferencesStore.get("muteUpdateNotificationVersion"); -} - -export function setMuteUpdateNotificationVersion(version: string) { - userPreferencesStore.set("muteUpdateNotificationVersion", version); -} - -export function clearSkipAppVersion() { - userPreferencesStore.delete("skipAppVersion"); -} - -export function clearMuteUpdateNotificationVersion() { - userPreferencesStore.delete("muteUpdateNotificationVersion"); -} diff --git a/desktop/src/stores/userPreferences.store.ts b/desktop/src/stores/user-preferences.ts similarity index 63% rename from desktop/src/stores/userPreferences.store.ts rename to desktop/src/stores/user-preferences.ts index 9545b1261..396e7a86c 100644 --- a/desktop/src/stores/userPreferences.store.ts +++ b/desktop/src/stores/user-preferences.ts @@ -1,7 +1,12 @@ import Store, { Schema } from "electron-store"; -import type { UserPreferencesType } from "../types/main"; -const userPreferencesSchema: Schema = { +interface UserPreferencesSchema { + hideDockIcon: boolean; + skipAppVersion?: string; + muteUpdateNotificationVersion?: string; +} + +const userPreferencesSchema: Schema = { hideDockIcon: { type: "boolean", }, diff --git a/desktop/src/types/main.ts b/desktop/src/types/main.ts index c875db1ab..546749c54 100644 --- a/desktop/src/types/main.ts +++ b/desktop/src/types/main.ts @@ -29,9 +29,3 @@ export const FILE_PATH_KEYS: { export interface SafeStorageStoreType { encryptionKey: string; } - -export interface UserPreferencesType { - hideDockIcon: boolean; - skipAppVersion: string; - muteUpdateNotificationVersion: string; -} diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 4594b2e20..d191da9f6 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -154,28 +154,26 @@ export default function App({ Component, pageProps }: AppProps) { useEffect(() => { const electron = globalThis.electron; - if (electron) { - const showUpdateDialog = (updateInfo: AppUpdateInfo) => { - if (updateInfo.autoUpdatable) { - setDialogMessage( - getUpdateReadyToInstallMessage(updateInfo), - ); - } else { - setNotificationAttributes({ - endIcon: , - variant: "secondary", - message: t("UPDATE_AVAILABLE"), - onClick: () => - setDialogMessage( - getUpdateAvailableForDownloadMessage( - updateInfo, - ), - ), - }); - } - }; - electron.registerUpdateEventListener(showUpdateDialog); - } + if (!electron) return; + + const showUpdateDialog = (updateInfo: AppUpdateInfo) => { + if (updateInfo.autoUpdatable) { + setDialogMessage(getUpdateReadyToInstallMessage(updateInfo)); + } else { + setNotificationAttributes({ + endIcon: , + variant: "secondary", + message: t("UPDATE_AVAILABLE"), + onClick: () => + setDialogMessage( + getUpdateAvailableForDownloadMessage(updateInfo), + ), + }); + } + }; + electron.onAppUpdateAvailable(showUpdateDialog); + + return () => electron.onAppUpdateAvailable(undefined); }, []); useEffect(() => { diff --git a/web/apps/photos/src/utils/ui/index.tsx b/web/apps/photos/src/utils/ui/index.tsx index 9090c6917..1b01116d3 100644 --- a/web/apps/photos/src/utils/ui/index.tsx +++ b/web/apps/photos/src/utils/ui/index.tsx @@ -1,3 +1,4 @@ +import { ensureElectron } from "@/next/electron"; import { AppUpdateInfo } from "@/next/types/ipc"; import { logoutUser } from "@ente/accounts/services/user"; import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types"; @@ -52,35 +53,34 @@ export const getTrashFileMessage = (deleteFileHelper): DialogBoxAttributes => ({ close: { text: t("CANCEL") }, }); -export const getUpdateReadyToInstallMessage = ( - updateInfo: AppUpdateInfo, -): DialogBoxAttributes => ({ +export const getUpdateReadyToInstallMessage = ({ + version, +}: AppUpdateInfo): DialogBoxAttributes => ({ icon: , title: t("UPDATE_AVAILABLE"), content: t("UPDATE_INSTALLABLE_MESSAGE"), proceed: { - action: () => globalThis.electron?.updateAndRestart(), + action: () => ensureElectron().updateAndRestart(), text: t("INSTALL_NOW"), variant: "accent", }, close: { text: t("INSTALL_ON_NEXT_LAUNCH"), variant: "secondary", - action: () => - globalThis.electron?.muteUpdateNotification(updateInfo.version), + action: () => ensureElectron().updateOnNextRestart(version), }, }); -export const getUpdateAvailableForDownloadMessage = ( - updateInfo: AppUpdateInfo, -): DialogBoxAttributes => ({ +export const getUpdateAvailableForDownloadMessage = ({ + version, +}: AppUpdateInfo): DialogBoxAttributes => ({ icon: , title: t("UPDATE_AVAILABLE"), content: t("UPDATE_AVAILABLE_MESSAGE"), close: { text: t("IGNORE_THIS_VERSION"), variant: "secondary", - action: () => globalThis.electron?.skipAppUpdate(updateInfo.version), + action: () => ensureElectron().skipAppUpdate(version), }, proceed: { action: downloadApp, diff --git a/web/packages/accounts/api/user.ts b/web/packages/accounts/api/user.ts index ddd940755..7a072064e 100644 --- a/web/packages/accounts/api/user.ts +++ b/web/packages/accounts/api/user.ts @@ -1,4 +1,3 @@ -import log from "@/next/log"; import { RecoveryKey, TwoFactorRecoveryResponse, diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index f9656ad3e..a0bc07d9a 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -37,7 +37,11 @@ export enum PICKED_UPLOAD_TYPE { export interface Electron { // - General - /** Return the version of the desktop app. */ + /** + * Return the version of the desktop app. + * + * The return value is of the form `v1.2.3`. + */ appVersion: () => Promise; /** @@ -97,6 +101,43 @@ export interface Electron { */ onMainWindowFocus: (cb?: () => void) => void; + // - App update + + /** + * Set or clear the callback {@link cb} to invoke whenever a new + * (actionable) app update is available. This allows the Node.js layer to + * ask the renderer to show an "Update available" dialog to the user. + * + * Note: Setting a callback clears any previous callbacks. + */ + onAppUpdateAvailable: ( + cb?: ((updateInfo: AppUpdateInfo) => void) | undefined, + ) => void; + + /** + * Restart the app to apply the latest available update. + * + * This is expected to be called in response to {@link onAppUpdateAvailable} + * if the user so wishes. + */ + updateAndRestart: () => void; + + /** + * Mute update notifications for the given {@link version}. This allows us + * to implement the "Install on next launch" functionality in response to + * the {@link onAppUpdateAvailable} event. + */ + updateOnNextRestart: (version: string) => void; + + /** + * Skip the app update with the given {@link version}. + * + * This is expected to be called in response to {@link onAppUpdateAvailable} + * if the user so wishes. It will remember this {@link version} as having + * been marked as skipped so that we don't prompt the user again. + */ + skipAppUpdate: (version: string) => void; + /** * A subset of filesystem access APIs. * @@ -132,18 +173,6 @@ export interface Electron { * the dataflow. */ - // - App update - - updateAndRestart: () => void; - - skipAppUpdate: (version: string) => void; - - muteUpdateNotification: (version: string) => void; - - registerUpdateEventListener: ( - showUpdateDialog: (updateInfo: AppUpdateInfo) => void, - ) => void; - // - Conversion convertToJPEG: (