From a56f104fe88666acac740e175c3a5f22fdb0d94b Mon Sep 17 00:00:00 2001 From: Alice <53339016+AliceHincu@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:02:22 +0300 Subject: [PATCH] Desktop: Seamless-Updates: triggering updates (#11079) --- packages/app-desktop/ElectronAppWrapper.ts | 9 +++-- packages/app-desktop/app.ts | 8 ++--- packages/app-desktop/gui/MenuBar.tsx | 8 ++++- .../TrashNotification/TrashNotification.tsx | 10 ++++-- .../UpdateNotification/UpdateNotification.tsx | 36 +++++++++++++++++-- .../autoUpdater/AutoUpdaterService.ts | 13 +++++-- .../lib/models/settings/builtInMetadata.ts | 2 +- 7 files changed, 69 insertions(+), 17 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 25e421bcc..ba271a401 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -332,6 +332,10 @@ export default class ElectronAppWrapper { this.updaterService_.updateApp(); }); + ipcMain.on('check-for-updates', () => { + void this.updaterService_.checkForUpdates(true); + }); + // Let us register listeners on the window, so we can update the state // automatically (the listeners will be removed when the window is closed) // and restore the maximized or full screen state @@ -470,7 +474,7 @@ export default class ElectronAppWrapper { } // Electron's autoUpdater has to be init from the main process - public async initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) { + public initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) { if (shim.isWindows() || shim.isMac()) { if (!this.updaterService_) { this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases); @@ -482,7 +486,7 @@ export default class ElectronAppWrapper { private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => { this.stopPeriodicUpdateCheck(); this.updatePollInterval_ = setInterval(() => { - void this.updaterService_.checkForUpdates(); + void this.updaterService_.checkForUpdates(false); }, updateInterval); setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup); }; @@ -491,6 +495,7 @@ export default class ElectronAppWrapper { if (this.updatePollInterval_) { clearInterval(this.updatePollInterval_); this.updatePollInterval_ = null; + this.updaterService_ = null; } }; diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 9a56280a1..311095269 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -404,9 +404,9 @@ class Application extends BaseApplication { eventManager.on(EventName.ResourceChange, handleResourceChange); } - private async setupAutoUpdaterService() { + private setupAutoUpdaterService() { if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { - await bridge().electronApp().initializeAutoUpdaterService( + bridge().electronApp().initializeAutoUpdaterService( Logger.create('AutoUpdaterService'), Setting.value('env') === 'dev', Setting.value('autoUpdate.includePreReleases'), @@ -449,6 +449,8 @@ class Application extends BaseApplication { // Loads app-wide styles. (Markdown preview-specific styles loaded in app.js) await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP)); + this.setupAutoUpdaterService(); + AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId })); AlarmService.setLogger(reg.logger()); @@ -698,8 +700,6 @@ class Application extends BaseApplication { SearchEngine.instance().scheduleSyncTables(); }); - await this.setupAutoUpdaterService(); - // setTimeout(() => { // void populateDatabase(reg.db(), { // clearDatabase: true, diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 3abcdc436..5f39fc904 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -26,6 +26,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import { EventName } from '@joplin/lib/eventManager'; +import { ipcRenderer } from 'electron'; const packageInfo: PackageInfo = require('../packageInfo.js'); const { clipboard } = require('electron'); const Menu = bridge().Menu; @@ -575,7 +576,12 @@ function useMenu(props: Props) { toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled'])); function _checkForUpdates() { - void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); + if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { + ipcRenderer.send('check-for-updates'); + } else { + void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); + } + } function _showAbout() { diff --git a/packages/app-desktop/gui/TrashNotification/TrashNotification.tsx b/packages/app-desktop/gui/TrashNotification/TrashNotification.tsx index d3d7338fe..951e9965a 100644 --- a/packages/app-desktop/gui/TrashNotification/TrashNotification.tsx +++ b/packages/app-desktop/gui/TrashNotification/TrashNotification.tsx @@ -1,4 +1,4 @@ -import { useContext, useCallback, useMemo } from 'react'; +import { useContext, useCallback, useMemo, useRef } from 'react'; import { StateLastDeletion } from '@joplin/lib/reducer'; import { _, _n } from '@joplin/lib/locale'; import NotyfContext from '../NotyfContext'; @@ -9,6 +9,7 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems'; import { ModelType } from '@joplin/lib/BaseModel'; import { themeStyle } from '@joplin/lib/theme'; import { Dispatch } from 'redux'; +import { NotyfNotification } from 'notyf'; interface Props { lastDeletion: StateLastDeletion; @@ -19,6 +20,7 @@ interface Props { export default (props: Props) => { const notyfContext = useContext(NotyfContext); + const notificationRef = useRef(null); const theme = useMemo(() => { return themeStyle(props.themeId); @@ -39,7 +41,8 @@ export default (props: Props) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const onCancelClick = useCallback(async (event: any) => { - notyf.dismissAll(); + notyf.dismiss(notificationRef.current); + notificationRef.current = null; const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion')); @@ -70,7 +73,8 @@ export default (props: Props) => { const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`; const cancelLabel = _('Cancel'); - notyf.success(`${msg} ${cancelLabel}`); + const notification = notyf.success(`${msg} ${cancelLabel}`); + notificationRef.current = notification; const element: HTMLAnchorElement = await waitForElement(document, linkId); if (event.cancelled) return; diff --git a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx index 8029fda23..05ea6e502 100644 --- a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx +++ b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx @@ -5,7 +5,7 @@ import NotyfContext from '../NotyfContext'; import { UpdateInfo } from 'electron-updater'; import { ipcRenderer, IpcRendererEvent } from 'electron'; import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService'; -import { NotyfNotification } from 'notyf'; +import { NotyfEvent, NotyfNotification } from 'notyf'; import { _ } from '@joplin/lib/locale'; import { htmlentities } from '@joplin/utils/html'; import shim from '@joplin/lib/shim'; @@ -16,6 +16,7 @@ interface UpdateNotificationProps { export enum UpdateNotificationEvents { ApplyUpdate = 'apply-update', + UpdateNotAvailable = 'update-not-available', Dismiss = 'dismiss-update-notification', } @@ -86,17 +87,46 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => { notificationRef.current = notification; }, [notyf, theme]); + const handleUpdateNotAvailable = useCallback(() => { + if (notificationRef.current) return; + + const noUpdateMessageHtml = htmlentities(_('No updates available')); + + const messageHtml = ` +
+ ${noUpdateMessageHtml} +
+ `; + + const notification: NotyfNotification = notyf.open({ + type: 'success', + message: messageHtml, + position: { + x: 'right', + y: 'bottom', + }, + duration: 5000, + }); + + notification.on(NotyfEvent.Dismiss, () => { + notificationRef.current = null; + }); + + notificationRef.current = notification; + }, [notyf, theme]); + useEffect(() => { ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded); + ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable); document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate); document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification); return () => { ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded); + ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable); document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate); - document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification); }; - }, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]); + }, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]); return ( diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index 6fcedfd43..93105a08f 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -37,7 +37,7 @@ const supportedPlatformAssets: PlatformAssets = { }; export interface AutoUpdaterServiceInterface { - checkForUpdates(): void; + checkForUpdates(isManualCheck: boolean): void; updateApp(): void; fetchLatestRelease(includePreReleases: boolean): Promise; getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string; @@ -48,10 +48,11 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private logger_: LoggerWrapper; private devMode_: boolean; private enableDevMode = true; // force the updater to work in "dev" mode - private enableAutoDownload = false; // automatically download an update when it is found + private enableAutoDownload = true; // automatically download an update when it is found private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application private includePreReleases_ = false; private allowDowngrade = false; + private isManualCheckInProgress = false; public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) { this.window_ = mainWindow; @@ -61,8 +62,9 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { this.configureAutoUpdater(); } - public checkForUpdates = async (): Promise => { + public checkForUpdates = async (isManualCheck = false): Promise => { try { + this.isManualCheckInProgress = isManualCheck; await this.checkForLatestRelease(); } catch (error) { this.logger_.error('Failed to check for updates:', error); @@ -132,6 +134,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/')); autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl }); await autoUpdater.checkForUpdates(); + this.isManualCheckInProgress = false; } catch (error) { this.logger_.error(`Update download url failed: ${error.message}`); } @@ -167,6 +170,10 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { }; private onUpdateNotAvailable = (_info: UpdateInfo): void => { + if (this.isManualCheckInProgress) { + this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable); + } + this.logger_.info('Update not available.'); }; diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index e822d3689..d9ce70577 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1127,7 +1127,7 @@ const builtInMetadata = (Setting: typeof SettingType) => { }, - autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') }, + autoUpdateEnabled: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: false, appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') }, 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') }, 'autoUploadCrashDumps': {