From 34fff73d37c07fff0bc626208246bf4095db1971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Fri, 4 Aug 2023 17:34:49 +0800 Subject: [PATCH] feat:Detect new upgrade on launch (#2783) * feat:Detect new upgrade on launch * refactor: Refactor update logic * feat: Add log for electron updater. * Fix: Add badge for unexpanded and add icon for update --------- Co-authored-by: Jun Ma --- .../GeneralSetting/generalSetting.module.scss | 20 ++++ .../src/components/GeneralSetting/hooks.ts | 46 +++++++++ .../src/components/GeneralSetting/index.tsx | 98 +++++++------------ .../neuron-ui/src/containers/Navbar/index.tsx | 57 +++++++++-- .../src/containers/Navbar/navbar.module.scss | 5 + packages/neuron-ui/src/locales/en.json | 2 +- packages/neuron-ui/src/locales/zh-tw.json | 2 +- packages/neuron-ui/src/locales/zh.json | 2 +- .../src/widgets/Badge/badge.module.scss | 16 +++ .../neuron-ui/src/widgets/Badge/index.tsx | 9 ++ .../neuron-ui/src/widgets/Icons/Update.svg | 3 + packages/neuron-wallet/src/controllers/api.ts | 2 +- .../neuron-wallet/src/controllers/update.ts | 77 +++++++++------ .../src/models/subjects/app-updater.ts | 6 +- 14 files changed, 239 insertions(+), 106 deletions(-) create mode 100644 packages/neuron-ui/src/components/GeneralSetting/hooks.ts create mode 100644 packages/neuron-ui/src/widgets/Badge/badge.module.scss create mode 100644 packages/neuron-ui/src/widgets/Badge/index.tsx create mode 100644 packages/neuron-ui/src/widgets/Icons/Update.svg diff --git a/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss b/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss index 37c98c5d7c..c9fb6d0603 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss +++ b/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss @@ -12,6 +12,7 @@ $action-button-width: 11.25rem; background: var(--input-disabled-color); border-radius: 8px; margin-right: 16px; + p { font-size: 14px; line-height: 20px; @@ -33,6 +34,25 @@ $action-button-width: 11.25rem; } } } + .showVersion { + position: relative; + + &::after { + content: attr(data-new-version-tip); + background-color: #ff1e1e; + border-top-right-radius: 8px; + border-bottom-left-radius: 8px; + position: absolute; + top: 0; + right: 0; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: normal; + padding: 2px 4px; + color: #fff; + } + } } .install { diff --git a/packages/neuron-ui/src/components/GeneralSetting/hooks.ts b/packages/neuron-ui/src/components/GeneralSetting/hooks.ts new file mode 100644 index 0000000000..0551a62c5c --- /dev/null +++ b/packages/neuron-ui/src/components/GeneralSetting/hooks.ts @@ -0,0 +1,46 @@ +import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react' +import { cancelCheckUpdates, cancelDownloadUpdate, checkForUpdates } from 'services/remote' + +export const useUpdateDownloadStatus = ({ + setShowCheckDialog, + downloadProgress, +}: { + setShowCheckDialog: Dispatch> + downloadProgress: number +}) => { + const [showUpdateDownloadStatus, setShowUpdateDownloadStatus] = useState(false) + const openShowUpdateDownloadStatus = useCallback(() => { + setShowUpdateDownloadStatus(true) + }, []) + const onCheckUpdate = useCallback(() => { + setShowCheckDialog(true) + checkForUpdates() + }, [setShowCheckDialog]) + const hasStartDownload = useMemo(() => downloadProgress >= 0, [downloadProgress]) + return { + showUpdateDownloadStatus, + openShowUpdateDownloadStatus, + onCheckUpdate, + onCancel: useCallback(() => { + if (hasStartDownload) { + cancelDownloadUpdate() + } + setShowUpdateDownloadStatus(false) + }, [hasStartDownload]), + } +} + +export const useCheckUpdate = () => { + const [showCheckDialog, setShowCheckDialog] = useState(false) + const onCancelCheckUpdates = useCallback(() => { + if (showCheckDialog) { + cancelCheckUpdates() + } + setShowCheckDialog(false) + }, [showCheckDialog]) + return { + showCheckDialog, + setShowCheckDialog, + onCancelCheckUpdates, + } +} diff --git a/packages/neuron-ui/src/components/GeneralSetting/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/index.tsx index 648e93a9aa..8819e4b5eb 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/index.tsx +++ b/packages/neuron-ui/src/components/GeneralSetting/index.tsx @@ -6,17 +6,12 @@ import LanguageDialog from 'components/LanguageDialog' import AlertDialog from 'widgets/AlertDialog' import { ReactComponent as VersionLogo } from 'widgets/Icons/VersionLogo.svg' import { ReactComponent as ArrowNext } from 'widgets/Icons/ArrowNext.svg' -import { - checkForUpdates, - cancelCheckUpdates, - downloadUpdate, - cancelDownloadUpdate, - installUpdate, - getVersion, -} from 'services/remote' -import { uniformTimeFormatter, bytesFormatter } from 'utils' +import { ReactComponent as Update } from 'widgets/Icons/Update.svg' +import { cancelCheckUpdates, downloadUpdate, installUpdate, getVersion } from 'services/remote' +import { uniformTimeFormatter, bytesFormatter, clsx } from 'utils' import { LanguageSelect } from 'widgets/Icons/icon' import styles from './generalSetting.module.scss' +import { useCheckUpdate, useUpdateDownloadStatus } from './hooks' interface UpdateDownloadStatusProps { show: boolean @@ -135,66 +130,47 @@ const GeneralSetting = ({ updater }: GeneralSettingProps) => { const [showLangDialog, setShowLangDialog] = useState(false) const [searchParams] = useSearchParams() const [errorMsg, setErrorMsg] = useState('') - const [dialogType, setDialogType] = useState<'' | 'checking' | 'updating' | 'updated'>('') + const { showCheckDialog, setShowCheckDialog, onCancelCheckUpdates } = useCheckUpdate() + const { version: newVersion, checking, downloadProgress } = updater + const { showUpdateDownloadStatus, openShowUpdateDownloadStatus, onCheckUpdate, onCancel } = useUpdateDownloadStatus({ + setShowCheckDialog, + downloadProgress, + }) - const version = useMemo(() => { + useEffect(() => { + if (showCheckDialog && newVersion) { + setShowCheckDialog(false) + openShowUpdateDownloadStatus() + } + }, [showCheckDialog, newVersion, openShowUpdateDownloadStatus, setShowCheckDialog]) + + const currentVersion = useMemo(() => { return getVersion() }, []) useEffect(() => { const checkUpdate = searchParams.get('checkUpdate') if (checkUpdate === '1') { - checkForUpdates() + onCheckUpdate() } - }, [searchParams, checkForUpdates]) + }, [searchParams, onCheckUpdate]) useEffect(() => { if (updater.errorMsg) { setErrorMsg(updater.errorMsg) cancelCheckUpdates() - return - } - if (updater.isUpdated) { - setDialogType('updated') - return - } - if (updater.checking) { - setDialogType('checking') - return - } - if (updater.version || updater.downloadProgress > 0) { - setDialogType('updating') - return } - setDialogType('') - }, [updater, setDialogType, setErrorMsg]) - - const handleUpdate = useCallback( - (e: React.SyntheticEvent) => { - const { - dataset: { method }, - } = e.target as HTMLElement - - if (method === 'cancelCheck') { - if (dialogType === 'checking') { - cancelCheckUpdates() - } - setDialogType('') - } else if (method === 'check') { - checkForUpdates() - } - }, - [dialogType, setDialogType, cancelCheckUpdates, checkForUpdates] - ) + }, [updater.errorMsg, setErrorMsg]) return (
-
+

- {t('settings.general.version')} v{version} + {t('settings.general.version')} v{newVersion || currentVersion}

-
@@ -216,32 +192,28 @@ const GeneralSetting = ({ updater }: GeneralSettingProps) => { title={t(`updates.check-updates`)} message={errorMsg} type="failed" - onCancel={() => setErrorMsg('')} + onCancel={() => { + setErrorMsg('') + }} />
-

{t(dialogType === 'checking' ? 'updates.checking-updates' : 'updates.update-not-available')}

+

{t(checking || newVersion ? 'updates.checking-updates' : 'updates.update-not-available')}

{ - cancelDownloadUpdate() - setDialogType('') - }} + show={showUpdateDownloadStatus} + onCancel={onCancel} progress={updater.downloadProgress} progressInfo={updater.progressInfo} newVersion={updater.version} diff --git a/packages/neuron-ui/src/containers/Navbar/index.tsx b/packages/neuron-ui/src/containers/Navbar/index.tsx index 7c45530421..c72c2624e6 100644 --- a/packages/neuron-ui/src/containers/Navbar/index.tsx +++ b/packages/neuron-ui/src/containers/Navbar/index.tsx @@ -2,7 +2,10 @@ import React, { useCallback, useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { useLocation, NavLink, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { useState as useGlobalState } from 'states' +import { NeuronWalletActions, useDispatch, useState as useGlobalState } from 'states' +import { checkForUpdates } from 'services/remote' +import { AppUpdater as AppUpdaterSubject } from 'services/subjects' +import Badge from 'widgets/Badge' import Logo from 'widgets/Icons/Logo.png' import { Overview, @@ -65,23 +68,48 @@ const MenuButton = ({ ) } +const ONE_DAY_MILLISECONDS = 24 * 3600 * 1000 + const Navbar = () => { const { pathname } = useLocation() + const dispatch = useDispatch() const neuronWallet = useGlobalState() const { wallet: { name }, settings: { wallets = [] }, + updater: { version }, } = neuronWallet const [t, i18n] = useTranslation() useOnLocaleChange(i18n) - const [selectedKey, setSelectedKey] = useState() - const computedKey = menuItems.find(item => item.key === pathname || item.children?.some(v => v.key === pathname))?.key + const [isClickedSetting, setIsClickedSetting] = useState(false) + const selectedKey = menuItems.find(item => item.key === pathname || item.children?.some(v => v.key === pathname))?.key useEffect(() => { - if (computedKey) { - setSelectedKey(computedKey) + const onAppUpdaterUpdates = (info: Subject.AppUpdater) => { + dispatch({ type: NeuronWalletActions.UpdateAppUpdaterStatus, payload: info }) + } + const appUpdaterSubscription = AppUpdaterSubject.subscribe(onAppUpdaterUpdates) + + return () => { + appUpdaterSubscription.unsubscribe() } - }, [computedKey]) + }, [dispatch]) + + useEffect(() => { + checkForUpdates() + const interval = setInterval(() => { + checkForUpdates() + }, ONE_DAY_MILLISECONDS) + return () => { + clearInterval(interval) + } + }, []) + + useEffect(() => { + if (pathname.includes(RoutePath.Settings)) { + setIsClickedSetting(true) + } + }, [pathname]) const [menuExpanded, setMenuExpanded] = useState(true) const onClickExpand = useCallback(() => { @@ -121,9 +149,18 @@ const Navbar = () => { {item.icon} - {t(item.name)} + + {!isClickedSetting && version && item.key === RoutePath.Settings ? ( + + {t(item.name)} + + ) : ( + {t(item.name)} + )} + {item.children?.length && } + {item.children?.length && item.key === selectedKey && (
@@ -162,7 +199,11 @@ const Navbar = () => { placement={item.children?.length ? 'right-bottom' : 'right'} > - {item.icon} + {!isClickedSetting && version && item.key === RoutePath.Settings ? ( + {item.icon} + ) : ( + item.icon + )} diff --git a/packages/neuron-ui/src/containers/Navbar/navbar.module.scss b/packages/neuron-ui/src/containers/Navbar/navbar.module.scss index 22571d635a..a6e4f79ffe 100644 --- a/packages/neuron-ui/src/containers/Navbar/navbar.module.scss +++ b/packages/neuron-ui/src/containers/Navbar/navbar.module.scss @@ -187,3 +187,8 @@ $hover-bg-color: #3cc68a4d; margin: unset; } } + +.unexpandedBadge::after { + top: 0; + right: 0; +} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 9450151e06..1c3cf8d443 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -740,7 +740,7 @@ "downloading-update": "Downloading update...", "update-not-available": "There are currently no updates available.", "updates-found-do-you-want-to-update": "An update ({{version}}) is available", - "install-update": "Install Update", + "install-update": "Immediate Update", "updates-downloaded-about-to-quit-and-install": "Update downloaded. Ready to install and relaunch.", "quit-and-install": "Install and relaunch" }, diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 4a437feb2e..5aef09ccab 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -711,7 +711,7 @@ "downloading-update": "正在下載更新…", "update-not-available": "當前是最新版本", "updates-found-do-you-want-to-update": "新版本 {{version}} 可供下載", - "install-update": "安裝更新", + "install-update": "立即更新", "updates-downloaded-about-to-quit-and-install": "下載完成。請安裝新版本。", "quit-and-install": "安裝並重啓應用" }, diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 2d00fcaf72..936882fae2 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -733,7 +733,7 @@ "downloading-update": "正在下载更新...", "update-not-available": "当前是最新版本", "updates-found-do-you-want-to-update": "新版本 {{version}} 可供下载", - "install-update": "安装更新", + "install-update": "立即更新", "updates-downloaded-about-to-quit-and-install": "下载完成。请安装新版本。", "quit-and-install": "安装并重启应用" }, diff --git a/packages/neuron-ui/src/widgets/Badge/badge.module.scss b/packages/neuron-ui/src/widgets/Badge/badge.module.scss new file mode 100644 index 0000000000..23ac5ba571 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Badge/badge.module.scss @@ -0,0 +1,16 @@ +@import '../../styles/theme.scss'; + +.badge { + position: relative; + + &::after { + content: ''; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #ff1e1e; + position: absolute; + top: 20%; + margin-left: 4px; + } +} diff --git a/packages/neuron-ui/src/widgets/Badge/index.tsx b/packages/neuron-ui/src/widgets/Badge/index.tsx new file mode 100644 index 0000000000..ee0798fee0 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Badge/index.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { clsx } from 'utils' +import styles from './badge.module.scss' + +const Badge = ({ children, className }: { children: React.ReactChild; className?: string }) => { + return
{children}
+} + +export default Badge diff --git a/packages/neuron-ui/src/widgets/Icons/Update.svg b/packages/neuron-ui/src/widgets/Icons/Update.svg new file mode 100644 index 0000000000..2562f66daa --- /dev/null +++ b/packages/neuron-ui/src/widgets/Icons/Update.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 8ccd7626ec..a169c79375 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -555,7 +555,7 @@ export default class ApiController { }) handle('download-update', async () => { - new UpdateController(false).downloadUpdate() + new UpdateController(true).downloadUpdate() }) handle('cancel-download-update', async () => { diff --git a/packages/neuron-wallet/src/controllers/update.ts b/packages/neuron-wallet/src/controllers/update.ts index d63301220c..2be6ea3c71 100644 --- a/packages/neuron-wallet/src/controllers/update.ts +++ b/packages/neuron-wallet/src/controllers/update.ts @@ -1,13 +1,19 @@ -import { autoUpdater, UpdateInfo, CancellationToken } from 'electron-updater' -import AppUpdaterSubject from '../models/subjects/app-updater' +import { autoUpdater, UpdateInfo, CancellationToken, ProgressInfo } from 'electron-updater' +import AppUpdaterSubject, { AppUpdater } from '../models/subjects/app-updater' +import logger from '../utils/logger' export default class UpdateController { static isChecking = false // One instance is already running and checking static downCancellationToken = new CancellationToken() + static lastNotifyInfo: AppUpdater + + static updatePackageSize: number = 0 + constructor(check: boolean = true) { autoUpdater.autoDownload = false + autoUpdater.logger = logger if (check && !UpdateController.isChecking) { this.bindEvents() @@ -34,14 +40,22 @@ export default class UpdateController { } public downloadUpdate() { - this.notify(0) + this.notify({ + ...UpdateController.lastNotifyInfo, + progressInfo: { + total: UpdateController.updatePackageSize, + percent: 0, + transferred: 0, + }, + downloadProgress: 0, + }) UpdateController.downCancellationToken = new CancellationToken() autoUpdater.downloadUpdate(UpdateController.downCancellationToken) } public cancelDownloadUpdate() { UpdateController.downCancellationToken.cancel() - this.notify() + this.notify({ ...UpdateController.lastNotifyInfo, progressInfo: null, downloadProgress: -1 }) autoUpdater.removeAllListeners() } @@ -50,54 +64,59 @@ export default class UpdateController { autoUpdater.on('error', error => { UpdateController.isChecking = false - this.notify(-1, null, false, '', '', '', error == null ? 'unknown' : (error.stack || error).toString()) + this.notify({ + version: '', + releaseDate: '', + releaseNotes: '', + errorMsg: error == null ? 'unknown' : (error.stack || error).toString(), + }) }) autoUpdater.on('update-available', (info: UpdateInfo) => { if (UpdateController.isChecking) { UpdateController.isChecking = false - this.notify(-1, null, false, info.version, info.releaseDate, info.releaseNotes as string) + UpdateController.updatePackageSize = info.files[0].size ?? 0 + this.notify({ + version: info.version, + releaseDate: info.releaseDate, + releaseNotes: info.releaseNotes as string, + }) } }) autoUpdater.on('update-not-available', () => { if (UpdateController.isChecking) { UpdateController.isChecking = false - this.notify(-1, null, true) + this.notify({ isUpdated: true }) } }) - autoUpdater.on('download-progress', progress => { - const progressPercent = progress.percent / 100 + autoUpdater.on('download-progress', (progressInfo: ProgressInfo) => { + const progressPercent = progressInfo.percent / 100 + UpdateController.updatePackageSize = progressInfo.total if (progressPercent !== 1) { - this.notify(progressPercent, progress) + this.notify({ ...UpdateController.lastNotifyInfo, downloadProgress: progressPercent, progressInfo }) } }) autoUpdater.on('update-downloaded', () => { UpdateController.isChecking = false - this.notify(1) + this.notify({ ...UpdateController.lastNotifyInfo, downloadProgress: 1 }) }) } - private notify( - downloadProgress: number = -1, - progressInfo = null, - isUpdated = false, - version = '', - releaseDate = '', - releaseNotes = '', - errorMsg = '' - ) { - AppUpdaterSubject.next({ - downloadProgress, - progressInfo, - isUpdated, - version, - releaseDate, - releaseNotes, - errorMsg, + private notify(appUpdater?: Partial>) { + UpdateController.lastNotifyInfo = { + downloadProgress: -1, + progressInfo: null, + isUpdated: false, + version: '', + releaseDate: '', + releaseNotes: '', + errorMsg: '', + ...appUpdater, checking: UpdateController.isChecking, - }) + } + AppUpdaterSubject.next(UpdateController.lastNotifyInfo) } } diff --git a/packages/neuron-wallet/src/models/subjects/app-updater.ts b/packages/neuron-wallet/src/models/subjects/app-updater.ts index 11a5aadac3..dc7d3af22f 100644 --- a/packages/neuron-wallet/src/models/subjects/app-updater.ts +++ b/packages/neuron-wallet/src/models/subjects/app-updater.ts @@ -1,6 +1,6 @@ import { Subject } from 'rxjs' -const AppUpdaterSubject = new Subject<{ +export interface AppUpdater { checking: boolean isUpdated: boolean downloadProgress: number // -1: not started, 1: finished, 0~1: downloading @@ -13,6 +13,8 @@ const AppUpdaterSubject = new Subject<{ releaseDate: string releaseNotes: string errorMsg: string -}>() +} + +const AppUpdaterSubject = new Subject() export default AppUpdaterSubject