From 88b3c7f526f28c2f0cc2e2f290440804113d73c8 Mon Sep 17 00:00:00 2001 From: Alice <53339016+AliceHincu@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:49:21 +0300 Subject: [PATCH] Desktop: Seamless-Updates - creation of update notification (#10791) --- packages/app-desktop/ElectronAppWrapper.ts | 2 +- .../gui/ConfigScreen/ConfigScreen.tsx | 1 + .../app-desktop/gui/MainScreen/MainScreen.tsx | 2 + .../UpdateNotification/UpdateNotification.tsx | 106 ++++++++++++++++++ .../gui/UpdateNotification/style.scss | 27 +++++ packages/app-desktop/style.scss | 1 + packages/lib/utils/processStartFlags.ts | 7 ++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx create mode 100644 packages/app-desktop/gui/UpdateNotification/style.scss diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 945565be5..aae7e5cbb 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -476,7 +476,7 @@ export default class ElectronAppWrapper { this.createWindow(); - if (!shim.isLinux) { + if (!shim.isLinux()) { this.updaterService_ = new AutoUpdaterService(); this.updaterService_.startPeriodicUpdateCheck(); } diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 572dfc88c..4645b8125 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -28,6 +28,7 @@ interface Font { declare global { interface Window { queryLocalFonts(): Promise; + openChangelogLink: ()=> void; } } diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index 6901f37e2..521d93f63 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -48,6 +48,7 @@ import NotePropertiesDialog from '../NotePropertiesDialog'; import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType'; import validateColumns from '../NoteListHeader/utils/validateColumns'; import TrashNotification from '../TrashNotification/TrashNotification'; +import UpdateNotification from '../UpdateNotification/UpdateNotification'; const PluginManager = require('@joplin/lib/services/PluginManager'); const ipcRenderer = require('electron').ipcRenderer; @@ -935,6 +936,7 @@ class MainScreenComponent extends React.Component { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied dispatch={this.props.dispatch as any} /> + {messageComp} {layoutComp} {pluginDialog} diff --git a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx new file mode 100644 index 000000000..d7d3fe0ed --- /dev/null +++ b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import { themeStyle } from '@joplin/lib/theme'; +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 { _ } from '@joplin/lib/locale'; +import { htmlentities } from '@joplin/utils/html'; + +interface UpdateNotificationProps { + themeId: number; +} + +export enum UpdateNotificationEvents { + ApplyUpdate = 'apply-update', + Dismiss = 'dismiss-update-notification', +} + +const changelogLink = 'https://github.com/laurent22/joplin/releases'; + +window.openChangelogLink = () => { + ipcRenderer.send('open-link', changelogLink); +}; + +const UpdateNotification = ({ themeId }: UpdateNotificationProps) => { + const notyfContext = useContext(NotyfContext); + const notificationRef = useRef(null); // Use ref to hold the current notification + + const theme = useMemo(() => themeStyle(themeId), [themeId]); + + const notyf = useMemo(() => { + const output = notyfContext; + output.options.types = notyfContext.options.types.map(type => { + if (type.type === 'success') { + type.background = theme.backgroundColor5; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + (type.icon as any).color = theme.backgroundColor5; + } + return type; + }); + return output; + }, [notyfContext, theme]); + + const handleDismissNotification = useCallback(() => { + notyf.dismiss(notificationRef.current); + notificationRef.current = null; + }, [notyf]); + + const handleApplyUpdate = useCallback(() => { + ipcRenderer.send('apply-update-now'); + handleDismissNotification(); + }, [handleDismissNotification]); + + + const handleUpdateDownloaded = useCallback((_event: IpcRendererEvent, info: UpdateInfo) => { + if (notificationRef.current) return; + + const updateAvailableHtml = htmlentities(_('A new update (%s) is available', info.version)); + const seeChangelogHtml = htmlentities(_('See changelog')); + const restartNowHtml = htmlentities(_('Restart now')); + const updateLaterHtml = htmlentities(_('Update later')); + + const messageHtml = ` +
+ ${updateAvailableHtml} ${seeChangelogHtml} +
+ + +
+
+ `; + + const notification: NotyfNotification = notyf.open({ + type: 'success', + message: messageHtml, + position: { + x: 'right', + y: 'bottom', + }, + duration: 0, + }); + + notificationRef.current = notification; + }, [notyf, theme]); + + useEffect(() => { + ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded); + document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate); + document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification); + + return () => { + ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded); + document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate); + document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification); + }; + }, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]); + + + return ( +
+ ); +}; + +export default UpdateNotification; diff --git a/packages/app-desktop/gui/UpdateNotification/style.scss b/packages/app-desktop/gui/UpdateNotification/style.scss new file mode 100644 index 000000000..65fbb36d0 --- /dev/null +++ b/packages/app-desktop/gui/UpdateNotification/style.scss @@ -0,0 +1,27 @@ +.update-notification { + display: flex; + flex-direction: column; + align-items: flex-start; + + .button-container { + display: flex; + gap: 10px; + margin-top: 8px; + } + + .notyf__button { + padding: 5px 10px; + border: 1px solid; + border-radius: 4px; + background-color: transparent; + cursor: pointer; + + &:hover { + background-color: rgba(255, 255, 255, 0.2); + } + } + + a { + text-decoration: underline; + } +} \ No newline at end of file diff --git a/packages/app-desktop/style.scss b/packages/app-desktop/style.scss index 55c2fcaf5..c42a35503 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -8,6 +8,7 @@ @use 'gui/NoteList/style.scss' as note-list; @use 'gui/JoplinCloudLoginScreen.scss' as joplin-cloud-login-screen; @use 'gui/NoteListHeader/style.scss' as note-list-header; +@use 'gui/UpdateNotification/style.scss' as update-notification; @use 'gui/TrashNotification/style.scss' as trash-notification; @use 'gui/Sidebar/style.scss' as sidebar-styles; @use 'gui/styles/index.scss'; diff --git a/packages/lib/utils/processStartFlags.ts b/packages/lib/utils/processStartFlags.ts index 975285189..18e2f3c71 100644 --- a/packages/lib/utils/processStartFlags.ts +++ b/packages/lib/utils/processStartFlags.ts @@ -174,6 +174,13 @@ const processStartFlags = async (argv: string[], setDefaults = true) => { continue; } + if (arg === '--updated') { + // Electron-specific flag - ignore it + // Allows to restart with the updated application after the update option is selected by the user + argv.splice(0, 1); + continue; + } + if (arg.length && arg[0] === '-') { throw new JoplinError(_('Unknown flag: %s', arg), 'flagError'); } else {