From fcd43033feabf7f599a3632dd4ca67239bf6240d Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Fri, 8 Dec 2023 03:20:02 +0000 Subject: [PATCH] fix(electron): sync settings from localStorage -> atom -> electron (#5020) - moved `appSettingAtom` to infra since we now have different packages that depends on it. There is no better place to fit in for now - use atomEffect to sync setting changes to updater related configs to Electron side - refactored how Electron reacts to updater config changes. --- packages/common/env/package.json | 3 +- packages/common/env/tsconfig.json | 3 - packages/common/infra/package.json | 1 + packages/common/infra/src/atom/index.ts | 8 + .../infra/src/{atom.ts => atom/layout.ts} | 31 +- packages/common/infra/src/atom/root-store.ts | 15 + .../infra/src/atom}/settings.ts | 34 +- packages/common/infra/src/atom/workspace.ts | 14 + packages/common/infra/src/type.ts | 2 +- packages/common/infra/tsconfig.json | 3 + packages/common/infra/vite.config.ts | 2 +- .../app-updater-button/index.css.ts | 3 +- .../app-sidebar/app-updater-button/index.tsx | 341 +++++++++++------- packages/frontend/core/package.json | 1 + .../core/src/commands/affine-settings.tsx | 2 +- .../about/update-check-section.tsx | 47 ++- .../appearance/date-format-setting.tsx | 5 +- .../general-setting/appearance/index.tsx | 8 +- .../src/components/page-detail-editor.tsx | 2 +- .../src/components/root-app-sidebar/index.tsx | 29 -- .../hooks/affine/use-app-setting-helper.ts | 3 +- .../workspace/detail-page/detail-page.tsx | 2 +- .../src/main/application-menu/create.ts | 2 +- .../src/main/updater/electron-updater.ts | 62 ++-- .../electron/src/main/updater/index.ts | 4 +- packages/frontend/hooks/package.json | 2 + .../frontend/hooks/src/use-app-updater.ts | 146 ++++---- packages/frontend/hooks/tsconfig.json | 3 +- packages/frontend/i18n/src/resources/en.json | 1 + yarn.lock | 5 +- 30 files changed, 441 insertions(+), 343 deletions(-) create mode 100644 packages/common/infra/src/atom/index.ts rename packages/common/infra/src/{atom.ts => atom/layout.ts} (50%) create mode 100644 packages/common/infra/src/atom/root-store.ts rename packages/{frontend/core/src/atoms => common/infra/src/atom}/settings.ts (68%) create mode 100644 packages/common/infra/src/atom/workspace.ts diff --git a/packages/common/env/package.json b/packages/common/env/package.json index 439d80bf89..8c44d85e05 100644 --- a/packages/common/env/package.json +++ b/packages/common/env/package.json @@ -21,8 +21,7 @@ }, "peerDependencies": { "@affine/templates": "workspace:*", - "@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly", - "@toeverything/infra": "workspace:*" + "@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly" }, "dependencies": { "lit": "^3.0.2" diff --git a/packages/common/env/tsconfig.json b/packages/common/env/tsconfig.json index d3368cce03..60bef5cacc 100644 --- a/packages/common/env/tsconfig.json +++ b/packages/common/env/tsconfig.json @@ -7,9 +7,6 @@ "outDir": "lib" }, "references": [ - { - "path": "../infra" - }, { "path": "../../../tests/fixtures" } diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index e783cc5504..7da35fcbf9 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -54,6 +54,7 @@ "dev": "vite build --watch" }, "dependencies": { + "@affine/env": "workspace:*", "@affine/sdk": "workspace:*", "@blocksuite/blocks": "0.11.0-nightly-202312070955-2b5bb47", "@blocksuite/global": "0.11.0-nightly-202312070955-2b5bb47", diff --git a/packages/common/infra/src/atom/index.ts b/packages/common/infra/src/atom/index.ts new file mode 100644 index 0000000000..8b5f2a3104 --- /dev/null +++ b/packages/common/infra/src/atom/index.ts @@ -0,0 +1,8 @@ +import { atom } from 'jotai'; + +export const loadedPluginNameAtom = atom([]); + +export * from './layout'; +export * from './root-store'; +export * from './settings'; +export * from './workspace'; diff --git a/packages/common/infra/src/atom.ts b/packages/common/infra/src/atom/layout.ts similarity index 50% rename from packages/common/infra/src/atom.ts rename to packages/common/infra/src/atom/layout.ts index 4957094507..ed6ec20fe7 100644 --- a/packages/common/infra/src/atom.ts +++ b/packages/common/infra/src/atom/layout.ts @@ -1,34 +1,5 @@ import type { ExpectedLayout } from '@affine/sdk/entry'; -import { assertExists } from '@blocksuite/global/utils'; -import type { Workspace } from '@blocksuite/store'; -import { atom, createStore } from 'jotai/vanilla'; - -import { getBlockSuiteWorkspaceAtom } from './__internal__/workspace'; - -// global store -let rootStore = createStore(); - -export function getCurrentStore() { - return rootStore; -} - -/** - * @internal do not use this function unless you know what you are doing - */ -export function _setCurrentStore(store: ReturnType) { - rootStore = store; -} - -export const loadedPluginNameAtom = atom([]); - -export const currentWorkspaceIdAtom = atom(null); -export const currentPageIdAtom = atom(null); -export const currentWorkspaceAtom = atom>(async get => { - const workspaceId = get(currentWorkspaceIdAtom); - assertExists(workspaceId); - const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId); - return get(currentWorkspaceAtom); -}); +import { atom } from 'jotai'; const contentLayoutBaseAtom = atom('editor'); diff --git a/packages/common/infra/src/atom/root-store.ts b/packages/common/infra/src/atom/root-store.ts new file mode 100644 index 0000000000..7bbb1ae73e --- /dev/null +++ b/packages/common/infra/src/atom/root-store.ts @@ -0,0 +1,15 @@ +import { createStore } from 'jotai'; + +// global store +let rootStore = createStore(); + +export function getCurrentStore() { + return rootStore; +} + +/** + * @internal do not use this function unless you know what you are doing + */ +export function _setCurrentStore(store: ReturnType) { + rootStore = store; +} diff --git a/packages/frontend/core/src/atoms/settings.ts b/packages/common/infra/src/atom/settings.ts similarity index 68% rename from packages/frontend/core/src/atoms/settings.ts rename to packages/common/infra/src/atom/settings.ts index 835dd16ce1..229335ce48 100644 --- a/packages/frontend/core/src/atoms/settings.ts +++ b/packages/common/infra/src/atom/settings.ts @@ -1,5 +1,9 @@ +import { setupGlobal } from '@affine/env/global'; import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; +import { atomEffect } from 'jotai-effect'; + +setupGlobal(); export type DateFormats = | 'MM/dd/YYYY' @@ -63,15 +67,35 @@ const appSettingBaseAtom = atomWithStorage('affine-settings', { type SetStateAction = Value | ((prev: Value) => Value); +const appSettingEffect = atomEffect(get => { + const settings = get(appSettingBaseAtom); + // some values in settings should be synced into electron side + if (environment.isDesktop) { + console.log('set config', settings); + window.apis?.updater + .setConfig({ + autoCheckUpdate: settings.autoCheckUpdate, + autoDownloadUpdate: settings.autoDownloadUpdate, + }) + .catch(err => { + console.error(err); + }); + } +}); + export const appSettingAtom = atom< AppSetting, [SetStateAction>], void >( - get => get(appSettingBaseAtom), - (get, set, apply) => { - const prev = get(appSettingBaseAtom); - const next = typeof apply === 'function' ? apply(prev) : apply; - set(appSettingBaseAtom, { ...prev, ...next }); + get => { + get(appSettingEffect); + return get(appSettingBaseAtom); + }, + (_get, set, apply) => { + set(appSettingBaseAtom, prev => { + const next = typeof apply === 'function' ? apply(prev) : apply; + return { ...prev, ...next }; + }); } ); diff --git a/packages/common/infra/src/atom/workspace.ts b/packages/common/infra/src/atom/workspace.ts new file mode 100644 index 0000000000..c73ab5a5e7 --- /dev/null +++ b/packages/common/infra/src/atom/workspace.ts @@ -0,0 +1,14 @@ +import { assertExists } from '@blocksuite/global/utils'; +import type { Workspace } from '@blocksuite/store'; +import { atom } from 'jotai'; + +import { getBlockSuiteWorkspaceAtom } from '../__internal__/workspace'; + +export const currentWorkspaceIdAtom = atom(null); +export const currentPageIdAtom = atom(null); +export const currentWorkspaceAtom = atom>(async get => { + const workspaceId = get(currentWorkspaceIdAtom); + assertExists(workspaceId); + const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId); + return get(currentWorkspaceAtom); +}); diff --git a/packages/common/infra/src/type.ts b/packages/common/infra/src/type.ts index 4467e27734..732c8d1d4d 100644 --- a/packages/common/infra/src/type.ts +++ b/packages/common/infra/src/type.ts @@ -201,7 +201,7 @@ export type UpdaterHandlers = { downloadUpdate: () => Promise; getConfig: () => Promise; setConfig: (newConfig: Partial) => Promise; - checkForUpdatesAndNotify: () => Promise<{ version: string } | null>; + checkForUpdates: () => Promise<{ version: string } | null>; }; export type WorkspaceHandlers = { diff --git a/packages/common/infra/tsconfig.json b/packages/common/infra/tsconfig.json index dadee14e7d..0941f63e4d 100644 --- a/packages/common/infra/tsconfig.json +++ b/packages/common/infra/tsconfig.json @@ -11,6 +11,9 @@ { "path": "../sdk" }, + { + "path": "../env" + }, { "path": "./tsconfig.node.json" } diff --git a/packages/common/infra/vite.config.ts b/packages/common/infra/vite.config.ts index ca5dc954e7..efd6f41b66 100644 --- a/packages/common/infra/vite.config.ts +++ b/packages/common/infra/vite.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ entry: { blocksuite: resolve(root, 'src/blocksuite/index.ts'), index: resolve(root, 'src/index.ts'), - atom: resolve(root, 'src/atom.ts'), + atom: resolve(root, 'src/atom/index.ts'), command: resolve(root, 'src/command/index.ts'), type: resolve(root, 'src/type.ts'), 'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'), diff --git a/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.css.ts b/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.css.ts index a2d56ad2ed..8e1aa32d92 100644 --- a/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.css.ts +++ b/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.css.ts @@ -85,12 +85,12 @@ export const installLabel = style({ flex: 1, fontSize: 'var(--affine-font-sm)', whiteSpace: 'nowrap', + justifyContent: 'space-between', }); export const installLabelNormal = style([ installLabel, { - justifyContent: 'space-between', selectors: { [`${root}:hover &, ${root}[data-updating=true] &`]: { display: 'none', @@ -103,6 +103,7 @@ export const installLabelHover = style([ installLabel, { display: 'none', + justifyContent: 'flex-start', selectors: { [`${root}:hover &, ${root}[data-updating=true] &`]: { display: 'flex', diff --git a/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.tsx b/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.tsx index d1c51a3b62..bf9025d78f 100644 --- a/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.tsx +++ b/packages/frontend/component/src/components/app-sidebar/app-updater-button/index.tsx @@ -1,18 +1,9 @@ import { Unreachable } from '@affine/env/constant'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons'; -import { - changelogCheckedAtom, - currentChangelogUnreadAtom, - currentVersionAtom, - downloadProgressAtom, - updateAvailableAtom, - updateReadyAtom, - useAppUpdater, -} from '@toeverything/hooks/use-app-updater'; +import { useAppUpdater } from '@toeverything/hooks/use-app-updater'; import clsx from 'clsx'; -import { useAtomValue, useSetAtom } from 'jotai'; -import { startTransition, useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { Tooltip } from '../../../ui/tooltip'; import * as styles from './index.css'; @@ -26,36 +17,178 @@ export interface AddPageButtonPureProps { version: string; allowAutoUpdate: boolean; } | null; + autoDownload: boolean; downloadProgress: number | null; appQuitting: boolean; className?: string; style?: React.CSSProperties; } +interface ButtonContentProps { + updateReady: boolean; + updateAvailable: { + version: string; + allowAutoUpdate: boolean; + } | null; + autoDownload: boolean; + downloadProgress: number | null; + appQuitting: boolean; + currentChangelogUnread: boolean; + onDismissCurrentChangelog: () => void; +} + +function DownloadUpdate({ updateAvailable }: ButtonContentProps) { + const t = useAFFiNEI18N(); + return ( +
+ + {t['com.affine.appUpdater.downloadUpdate']()} + + {updateAvailable?.version} +
+ ); +} + +function UpdateReady({ updateAvailable, appQuitting }: ButtonContentProps) { + const t = useAFFiNEI18N(); + return ( +
+
+ + {t['com.affine.appUpdater.updateAvailable']()} + + {updateAvailable?.version} +
+ +
+ + + {t[appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate']()} + +
+
+ ); +} + +function DownloadingUpdate({ + updateAvailable, + downloadProgress, +}: ButtonContentProps) { + const t = useAFFiNEI18N(); + return ( +
+
+ + {t['com.affine.appUpdater.downloading']()} + + {updateAvailable?.version} +
+ +
+
+
+
+ ); +} + +function OpenDownloadPage({ updateAvailable }: ButtonContentProps) { + const t = useAFFiNEI18N(); + return ( + <> +
+ + {t['com.affine.appUpdater.updateAvailable']()} + + {updateAvailable?.version} +
+ +
+ + {t['com.affine.appUpdater.openDownloadPage']()} + +
+ + ); +} + +function WhatsNew({ onDismissCurrentChangelog }: ButtonContentProps) { + const t = useAFFiNEI18N(); + const onClickClose: React.MouseEventHandler = useCallback( + e => { + onDismissCurrentChangelog(); + e.stopPropagation(); + }, + [onDismissCurrentChangelog] + ); + return ( + <> +
+ + + {t['com.affine.appUpdater.whatsNew']()} + +
+
+ +
+ + ); +} + +const getButtonContentRenderer = (props: ButtonContentProps) => { + if (props.updateReady) { + return UpdateReady; + } else if (props.updateAvailable?.allowAutoUpdate) { + if (props.autoDownload && props.updateAvailable.allowAutoUpdate) { + return DownloadingUpdate; + } else { + return DownloadUpdate; + } + } else if (props.updateAvailable && !props.updateAvailable?.allowAutoUpdate) { + return OpenDownloadPage; + } else if (props.currentChangelogUnread) { + return WhatsNew; + } + return null; +}; + export function AppUpdaterButtonPure({ updateReady, onClickUpdate, onDismissCurrentChangelog, currentChangelogUnread, updateAvailable, + autoDownload, downloadProgress, appQuitting, className, style, }: AddPageButtonPureProps) { - const t = useAFFiNEI18N(); + const contentProps = useMemo( + () => ({ + updateReady, + updateAvailable, + currentChangelogUnread, + autoDownload, + downloadProgress, + appQuitting, + onDismissCurrentChangelog, + }), + [ + updateReady, + updateAvailable, + currentChangelogUnread, + autoDownload, + downloadProgress, + appQuitting, + onDismissCurrentChangelog, + ] + ); - if (!updateAvailable && !currentChangelogUnread) { - return null; - } - - const updateAvailableNode = updateAvailable - ? updateAvailable.allowAutoUpdate - ? renderUpdateAvailableAllowAutoUpdate() - : renderUpdateAvailableNotAllowAutoUpdate() - : null; - const whatsNew = - !updateAvailable && currentChangelogUnread ? renderWhatsNew() : null; + const ContentComponent = getButtonContentRenderer(contentProps); const wrapWithTooltip = ( node: React.ReactElement, @@ -72,102 +205,38 @@ export function AppUpdaterButtonPure({ ); }; + const disabled = useMemo(() => { + if (appQuitting) { + return true; + } + + if (updateAvailable?.allowAutoUpdate) { + return !updateReady && autoDownload; + } + + return false; + }, [ + appQuitting, + autoDownload, + updateAvailable?.allowAutoUpdate, + updateReady, + ]); + return wrapWithTooltip( , updateAvailable?.version ); - - function renderUpdateAvailableAllowAutoUpdate() { - return ( -
-
- - {!updateReady - ? t['com.affine.appUpdater.downloading']() - : t['com.affine.appUpdater.updateAvailable']()} - - - {updateAvailable?.version} - -
- - {updateReady ? ( -
- - - {t[ - appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate' - ]()} - -
- ) : ( -
-
-
- )} -
- ); - } - - function renderUpdateAvailableNotAllowAutoUpdate() { - return ( - <> -
- - {t['com.affine.appUpdater.updateAvailable']()} - - - {updateAvailable?.version} - -
- -
- - {t['com.affine.appUpdater.openDownloadPage']()} - -
- - ); - } - - function renderWhatsNew() { - return ( - <> -
- - - {t['com.affine.appUpdater.whatsNew']()} - -
-
{ - onDismissCurrentChangelog(); - e.stopPropagation(); - }} - > - -
- - ); - } } // Although it is called an input, it is actually a button. @@ -178,62 +247,64 @@ export function AppUpdaterButton({ className?: string; style?: React.CSSProperties; }) { - const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom); - const updateReady = useAtomValue(updateReadyAtom); - const updateAvailable = useAtomValue(updateAvailableAtom); - const downloadProgress = useAtomValue(downloadProgressAtom); - const currentVersion = useAtomValue(currentVersionAtom); - const { quitAndInstall, appQuitting } = useAppUpdater(); - const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom); - - const dismissCurrentChangelog = useCallback(() => { - if (!currentVersion) { - return; - } - startTransition(() => - setChangelogCheckAtom(mapping => { - return { - ...mapping, - [currentVersion]: true, - }; - }) - ); - }, [currentVersion, setChangelogCheckAtom]); + const { + quitAndInstall, + appQuitting, + autoDownload, + downloadUpdate, + readChangelog, + changelogUnread, + updateReady, + updateAvailable, + downloadProgress, + currentVersion, + } = useAppUpdater(); const handleClickUpdate = useCallback(() => { if (updateReady) { quitAndInstall(); } else if (updateAvailable) { if (updateAvailable.allowAutoUpdate) { - // wait for download to finish + if (autoDownload) { + // wait for download to finish + } else { + downloadUpdate(); + } } else { window.open( `https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`, '_blank' ); } - } else if (currentChangelogUnread) { + } else if (changelogUnread) { window.open(runtimeConfig.changelogUrl, '_blank'); - dismissCurrentChangelog(); + readChangelog(); } else { throw new Unreachable(); } }, [ updateReady, - quitAndInstall, updateAvailable, - currentChangelogUnread, - dismissCurrentChangelog, + changelogUnread, + quitAndInstall, + autoDownload, + downloadUpdate, currentVersion, + readChangelog, ]); + if (!updateAvailable && !changelogUnread) { + return null; + } + return ( { const t = useAFFiNEI18N(); - const isCheckingForUpdates = useAtomValue(isCheckingForUpdatesAtom); - const updateAvailable = useAtomValue(updateAvailableAtom); - const updateReady = useAtomValue(updateReadyAtom); - const downloadProgress = useAtomValue(downloadProgressAtom); + const { updateAvailable, downloadProgress, updateReady, checkingForUpdates } = + useAppUpdater(); const buttonLabel = useMemo(() => { - if (updateAvailable && downloadProgress === null) { - return t['com.affine.aboutAFFiNE.checkUpdate.button.download'](); - } if (updateReady) { return t['com.affine.aboutAFFiNE.checkUpdate.button.restart'](); } + if (updateAvailable && downloadProgress === null) { + return t['com.affine.aboutAFFiNE.checkUpdate.button.download'](); + } if ( checkUpdateStatus === CheckUpdateStatus.LATEST || checkUpdateStatus === CheckUpdateStatus.ERROR @@ -47,16 +38,16 @@ const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => { }, [checkUpdateStatus, downloadProgress, t, updateAvailable, updateReady]); const subtitleLabel = useMemo(() => { - if (updateAvailable && downloadProgress === null) { + if (updateReady) { + return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.restart'](); + } else if (updateAvailable && downloadProgress === null) { return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.update-available']({ version: updateAvailable.version, }); - } else if (isCheckingForUpdates) { + } else if (checkingForUpdates) { return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.checking'](); } else if (updateAvailable && downloadProgress !== null) { return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.downloading'](); - } else if (updateReady) { - return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.restart'](); } else if (checkUpdateStatus === CheckUpdateStatus.ERROR) { return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.error'](); } else if (checkUpdateStatus === CheckUpdateStatus.LATEST) { @@ -66,7 +57,7 @@ const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => { }, [ checkUpdateStatus, downloadProgress, - isCheckingForUpdates, + checkingForUpdates, t, updateAvailable, updateReady, @@ -83,14 +74,14 @@ const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => { error: checkUpdateStatus === CheckUpdateStatus.ERROR, })} > - {isCheckingForUpdates ? : null} + {checkingForUpdates ? : null} {subtitleLabel} ); }, [ checkUpdateStatus, downloadProgress, - isCheckingForUpdates, + checkingForUpdates, subtitleLabel, updateAvailable, updateReady, @@ -101,10 +92,14 @@ const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => { export const UpdateCheckSection = () => { const t = useAFFiNEI18N(); - const { checkForUpdates, downloadUpdate, quitAndInstall } = useAppUpdater(); - const updateAvailable = useAtomValue(updateAvailableAtom); - const updateReady = useAtomValue(updateReadyAtom); - const downloadProgress = useAtomValue(downloadProgressAtom); + const { + checkForUpdates, + downloadUpdate, + quitAndInstall, + updateAvailable, + downloadProgress, + updateReady, + } = useAppUpdater(); const [checkUpdateStatus, setCheckUpdateStatus] = useState( CheckUpdateStatus.UNCHECK ); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/date-format-setting.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/date-format-setting.tsx index fb1be173a9..06ee1aa6d4 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/date-format-setting.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/date-format-setting.tsx @@ -1,11 +1,8 @@ import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu'; +import { dateFormatOptions, type DateFormats } from '@toeverything/infra/atom'; import dayjs from 'dayjs'; import { useCallback } from 'react'; -import { - dateFormatOptions, - type DateFormats, -} from '../../../../../atoms/settings'; import { useAppSettingHelper } from '../../../../../hooks/affine/use-app-setting-helper'; interface DateFormatMenuContentProps { diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx index 076a0fad14..5988c8e15b 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/appearance/index.tsx @@ -3,14 +3,14 @@ import { SettingHeader } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components'; import { SettingWrapper } from '@affine/component/setting-components'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useTheme } from 'next-themes'; -import { useCallback } from 'react'; - import { type AppSetting, fontStyleOptions, windowFrameStyleOptions, -} from '../../../../../atoms/settings'; +} from '@toeverything/infra/atom'; +import { useTheme } from 'next-themes'; +import { useCallback } from 'react'; + import { useAppSettingHelper } from '../../../../../hooks/affine/use-app-setting-helper'; import { LanguageMenu } from '../../../language-menu'; import { DateFormatSetting } from './date-format-setting'; diff --git a/packages/frontend/core/src/components/page-detail-editor.tsx b/packages/frontend/core/src/components/page-detail-editor.tsx index 5ae209a5ea..1d0a13615a 100644 --- a/packages/frontend/core/src/components/page-detail-editor.tsx +++ b/packages/frontend/core/src/components/page-detail-editor.tsx @@ -8,6 +8,7 @@ import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page- import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page'; import { pluginEditorAtom } from '@toeverything/infra/__internal__/plugin'; import { getCurrentStore } from '@toeverything/infra/atom'; +import { fontStyleOptions } from '@toeverything/infra/atom'; import clsx from 'clsx'; import { useAtomValue } from 'jotai'; import type { CSSProperties } from 'react'; @@ -15,7 +16,6 @@ import { memo, Suspense, useCallback, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { type PageMode, pageSettingFamily } from '../atoms'; -import { fontStyleOptions } from '../atoms/settings'; import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper'; import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index 9696b991be..5956f64a1f 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -20,11 +20,6 @@ import { FolderIcon, SettingsIcon } from '@blocksuite/icons'; import type { Page } from '@blocksuite/store'; import { useDroppable } from '@dnd-kit/core'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; -import { - isAutoCheckUpdateAtom, - isAutoDownloadUpdateAtom, - useAppUpdater, -} from '@toeverything/hooks/use-app-updater'; import { useAtom, useAtomValue } from 'jotai'; import type { HTMLAttributes, ReactElement } from 'react'; import { forwardRef, useCallback, useEffect, useMemo } from 'react'; @@ -107,10 +102,6 @@ export const RootAppSidebar = ({ }: RootAppSidebarProps): ReactElement => { const currentWorkspaceId = currentWorkspace.id; const { appSettings } = useAppSettingHelper(); - const { toggleAutoCheck, toggleAutoDownload } = useAppUpdater(); - const { autoCheckUpdate, autoDownloadUpdate } = appSettings; - const isAutoDownload = useAtomValue(isAutoDownloadUpdateAtom); - const isAutoCheck = useAtomValue(isAutoCheckUpdateAtom); const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; const t = useAFFiNEI18N(); const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom( @@ -159,26 +150,6 @@ export const RootAppSidebar = ({ } }, [sidebarOpen]); - useEffect(() => { - if (!environment.isDesktop) { - return; - } - - if (isAutoCheck !== autoCheckUpdate) { - toggleAutoCheck(autoCheckUpdate); - } - if (isAutoDownload !== autoDownloadUpdate) { - toggleAutoDownload(autoDownloadUpdate); - } - }, [ - autoCheckUpdate, - autoDownloadUpdate, - isAutoCheck, - isAutoDownload, - toggleAutoCheck, - toggleAutoDownload, - ]); - const [history, setHistory] = useHistoryAtom(); const router = useMemo(() => { return { diff --git a/packages/frontend/core/src/hooks/affine/use-app-setting-helper.ts b/packages/frontend/core/src/hooks/affine/use-app-setting-helper.ts index f918f8be83..1be32c0b8f 100644 --- a/packages/frontend/core/src/hooks/affine/use-app-setting-helper.ts +++ b/packages/frontend/core/src/hooks/affine/use-app-setting-helper.ts @@ -1,8 +1,7 @@ +import { type AppSetting, appSettingAtom } from '@toeverything/infra/atom'; import { useAtom } from 'jotai'; import { useCallback, useMemo } from 'react'; -import { type AppSetting, appSettingAtom } from '../../atoms/settings'; - export function useAppSettingHelper() { const [appSettings, setAppSettings] = useAtom(appSettingAtom); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index a4aa1c82dd..62fd0b0838 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -11,6 +11,7 @@ import type { EditorContainer } from '@blocksuite/presets'; import type { Page, Workspace } from '@blocksuite/store'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { + appSettingAtom, currentPageIdAtom, currentWorkspaceIdAtom, } from '@toeverything/infra/atom'; @@ -36,7 +37,6 @@ import type { Map as YMap } from 'yjs'; import { setPageModeAtom } from '../../../atoms'; import { collectionsCRUDAtom } from '../../../atoms/collections'; import { currentModeAtom } from '../../../atoms/mode'; -import { appSettingAtom } from '../../../atoms/settings'; import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; import { HubIsland } from '../../../components/affine/hub-island'; import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; diff --git a/packages/frontend/electron/src/main/application-menu/create.ts b/packages/frontend/electron/src/main/application-menu/create.ts index c57ee266f9..dd16bc3b6c 100644 --- a/packages/frontend/electron/src/main/application-menu/create.ts +++ b/packages/frontend/electron/src/main/application-menu/create.ts @@ -125,7 +125,7 @@ export function createApplicationMenu() { { label: 'Check for Updates', click: async () => { - await checkForUpdates(true); + await checkForUpdates(); }, }, ], diff --git a/packages/frontend/electron/src/main/updater/electron-updater.ts b/packages/frontend/electron/src/main/updater/electron-updater.ts index 7594664107..0cbba4d980 100644 --- a/packages/frontend/electron/src/main/updater/electron-updater.ts +++ b/packages/frontend/electron/src/main/updater/electron-updater.ts @@ -17,9 +17,9 @@ export const quitAndInstall = async () => { autoUpdater.quitAndInstall(); }; -let lastCheckTime = 0; - let downloading = false; +let configured = false; +let checkingUpdate = false; export type UpdaterConfig = { autoCheckUpdate: boolean; @@ -36,29 +36,39 @@ export const getConfig = (): UpdaterConfig => { }; export const setConfig = (newConfig: Partial = {}): void => { + configured = true; + Object.assign(config, newConfig); + + logger.info('Updater configured!', config); + + // if config.autoCheckUpdate is true, trigger a check + if (config.autoCheckUpdate) { + checkForUpdates().catch(err => { + logger.error('Error checking for updates', err); + }); + } }; -export const checkForUpdates = async (force = false) => { - if (disabled) { +export const checkForUpdates = async () => { + if (disabled || checkingUpdate) { return; } - - if ( - force || - (config.autoCheckUpdate && lastCheckTime + 1000 * 1800 < Date.now()) - ) { - lastCheckTime = Date.now(); - return await autoUpdater.checkForUpdates(); + checkingUpdate = true; + try { + const info = await autoUpdater.checkForUpdates(); + return info; + } finally { + checkingUpdate = false; } - return; }; export const downloadUpdate = async () => { - if (disabled) { + if (disabled || downloading) { return; } downloading = true; + updaterSubjects.downloadProgress.next(0); autoUpdater.downloadUpdate().catch(e => { downloading = false; logger.error('Failed to download update', e); @@ -96,19 +106,16 @@ export const registerUpdater = async () => { autoUpdater.setFeedURL(feedUrl); - // register events for checkForUpdatesAndNotify + // register events for checkForUpdates autoUpdater.on('checking-for-update', () => { logger.info('Checking for update'); }); autoUpdater.on('update-available', info => { logger.info('Update available', info); - if (config.autoDownloadUpdate && allowAutoUpdate && !downloading) { - downloading = true; - autoUpdater?.downloadUpdate().catch(e => { - downloading = false; - logger.error('Failed to download update', e); + if (config.autoDownloadUpdate && allowAutoUpdate) { + downloadUpdate().catch(err => { + console.error(err); }); - logger.info('Update available, downloading...', info); } updaterSubjects.updateAvailable.next({ version: info.version, @@ -137,9 +144,20 @@ export const registerUpdater = async () => { }); autoUpdater.forceDevUpdateConfig = isDev; + // check update whenever the window is activated + let lastCheckTime = 0; app.on('activate', () => { - checkForUpdates(false).catch(err => { - console.error(err); + (async () => { + if ( + configured && + config.autoCheckUpdate && + lastCheckTime + 1000 * 1800 < Date.now() + ) { + lastCheckTime = Date.now(); + await checkForUpdates(); + } + })().catch(err => { + logger.error('Error checking for updates', err); }); }); }; diff --git a/packages/frontend/electron/src/main/updater/index.ts b/packages/frontend/electron/src/main/updater/index.ts index 19d24aaaf3..0265db1e71 100644 --- a/packages/frontend/electron/src/main/updater/index.ts +++ b/packages/frontend/electron/src/main/updater/index.ts @@ -29,8 +29,8 @@ export const updaterHandlers = { ): Promise => { return setConfig(newConfig); }, - checkForUpdatesAndNotify: async () => { - const res = await checkForUpdates(true); + checkForUpdates: async () => { + const res = await checkForUpdates(); if (res) { const { updateInfo } = res; return { diff --git a/packages/frontend/hooks/package.json b/packages/frontend/hooks/package.json index 1a8d4f128d..de23d82f87 100644 --- a/packages/frontend/hooks/package.json +++ b/packages/frontend/hooks/package.json @@ -9,6 +9,7 @@ "foxact": "^0.2.20", "image-blob-reduce": "^4.1.0", "jotai": "^2.5.1", + "jotai-effect": "^0.2.3", "lodash.debounce": "^4.0.8", "p-queue": "^7.4.1", "react": "18.2.0", @@ -26,6 +27,7 @@ "@blocksuite/presets": "0.11.0-nightly-202312070955-2b5bb47", "@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47", "@testing-library/react": "^14.0.0", + "@toeverything/infra": "workspace:*", "@types/image-blob-reduce": "^4.1.3", "@types/lodash.debounce": "^4.0.7", "fake-indexeddb": "^5.0.0", diff --git a/packages/frontend/hooks/src/use-app-updater.ts b/packages/frontend/hooks/src/use-app-updater.ts index adf0c8e7f0..d4e28c1dc6 100644 --- a/packages/frontend/hooks/src/use-app-updater.ts +++ b/packages/frontend/hooks/src/use-app-updater.ts @@ -1,10 +1,13 @@ import { isBrowser } from '@affine/env/constant'; +import { appSettingAtom } from '@toeverything/infra/atom'; import type { UpdateMeta } from '@toeverything/infra/type'; -import { atom, useAtomValue, useSetAtom } from 'jotai'; +import { atom, useAtom, useAtomValue } from 'jotai'; import { atomWithObservable, atomWithStorage } from 'jotai/utils'; import { useCallback, useState } from 'react'; import { Observable } from 'rxjs'; +import { useAsyncCallback } from './affine-async-hooks'; + function rpcToObservable< T, H extends () => Promise, @@ -41,25 +44,21 @@ function rpcToObservable< }); } +// download complete, ready to install export const updateReadyAtom = atomWithObservable(() => { return rpcToObservable(null as UpdateMeta | null, { event: window.events?.updater.onUpdateReady, }); }); -export const updateAvailableStateAtom = atom(null); - -export const updateAvailableAtom = atomWithObservable(get => { - return rpcToObservable(get(updateAvailableStateAtom), { +// update available, but not downloaded yet +export const updateAvailableAtom = atomWithObservable(() => { + return rpcToObservable(null as UpdateMeta | null, { event: window.events?.updater.onUpdateAvailable, - onSubscribe: () => { - window.apis?.updater.checkForUpdatesAndNotify().catch(err => { - console.error(err); - }); - }, }); }); +// downloading new update export const downloadProgressAtom = atomWithObservable(() => { return rpcToObservable(null as number | null, { event: window.events?.updater.onDownloadProgress, @@ -71,6 +70,8 @@ export const changelogCheckedAtom = atomWithStorage>( {} ); +export const checkingForUpdatesAtom = atom(false); + export const currentVersionAtom = atom(async () => { if (!isBrowser) { return null; @@ -79,29 +80,43 @@ export const currentVersionAtom = atom(async () => { return currentVersion; }); -export const currentChangelogUnreadAtom = atom(async get => { - if (!isBrowser) { +const currentChangelogUnreadAtom = atom( + async get => { + if (!isBrowser) { + return false; + } + const mapping = get(changelogCheckedAtom); + const currentVersion = await get(currentVersionAtom); + if (currentVersion) { + return !mapping[currentVersion]; + } return false; + }, + async (get, set, v: boolean) => { + const currentVersion = await get(currentVersionAtom); + if (currentVersion) { + set(changelogCheckedAtom, mapping => { + return { + ...mapping, + [currentVersion]: v, + }; + }); + } } - const mapping = get(changelogCheckedAtom); - const currentVersion = await get(currentVersionAtom); - if (currentVersion) { - return !mapping[currentVersion]; - } - return false; -}); - -export const isCheckingForUpdatesAtom = atom(false); -export const isAutoDownloadUpdateAtom = atom(true); -export const isAutoCheckUpdateAtom = atom(true); +); export const useAppUpdater = () => { const [appQuitting, setAppQuitting] = useState(false); const updateReady = useAtomValue(updateReadyAtom); - const setUpdateAvailableState = useSetAtom(updateAvailableStateAtom); - const setIsCheckingForUpdates = useSetAtom(isCheckingForUpdatesAtom); - const setIsAutoCheckUpdate = useSetAtom(isAutoCheckUpdateAtom); - const setIsAutoDownloadUpdate = useSetAtom(isAutoDownloadUpdateAtom); + const [setting, setSetting] = useAtom(appSettingAtom); + const downloadProgress = useAtomValue(downloadProgressAtom); + const [changelogUnread, setChangelogUnread] = useAtom( + currentChangelogUnreadAtom + ); + + const [checkingForUpdates, setCheckingForUpdates] = useAtom( + checkingForUpdatesAtom + ); const quitAndInstall = useCallback(() => { if (updateReady) { @@ -114,73 +129,64 @@ export const useAppUpdater = () => { }, [updateReady]); const checkForUpdates = useCallback(async () => { - setIsCheckingForUpdates(true); + if (checkingForUpdates) { + return; + } + setCheckingForUpdates(true); try { - const updateInfo = await window.apis?.updater.checkForUpdatesAndNotify(); - setIsCheckingForUpdates(false); - if (updateInfo) { - const updateMeta: UpdateMeta = { - version: updateInfo.version, - allowAutoUpdate: false, - }; - setUpdateAvailableState(updateMeta); - return updateInfo.version; - } - return false; + const updateInfo = await window.apis?.updater.checkForUpdates(); + return updateInfo?.version ?? false; } catch (err) { - setIsCheckingForUpdates(false); console.error('Error checking for updates:', err); return null; + } finally { + setCheckingForUpdates(false); } - }, [setIsCheckingForUpdates, setUpdateAvailableState]); + }, [checkingForUpdates, setCheckingForUpdates]); const downloadUpdate = useCallback(() => { - window.apis?.updater - .downloadUpdate() - .then(() => {}) - .catch(err => { - console.error('Error downloading update:', err); - }); + window.apis?.updater.downloadUpdate().catch(err => { + console.error('Error downloading update:', err); + }); }, []); const toggleAutoDownload = useCallback( (enable: boolean) => { - window.apis?.updater - .setConfig({ - autoDownloadUpdate: enable, - }) - .then(() => { - setIsAutoDownloadUpdate(enable); - }) - .catch(err => { - console.error('Error setting auto download:', err); - }); + setSetting({ + autoDownloadUpdate: enable, + }); }, - [setIsAutoDownloadUpdate] + [setSetting] ); const toggleAutoCheck = useCallback( (enable: boolean) => { - window.apis?.updater - .setConfig({ - autoCheckUpdate: enable, - }) - .then(() => { - setIsAutoCheckUpdate(enable); - }) - .catch(err => { - console.error('Error setting auto check:', err); - }); + setSetting({ + autoCheckUpdate: enable, + }); }, - [setIsAutoCheckUpdate] + [setSetting] ); + const readChangelog = useAsyncCallback(async () => { + await setChangelogUnread(true); + }, [setChangelogUnread]); + return { quitAndInstall, - appQuitting, checkForUpdates, downloadUpdate, toggleAutoDownload, toggleAutoCheck, + appQuitting, + checkingForUpdates, + autoCheck: setting.autoCheckUpdate, + autoDownload: setting.autoDownloadUpdate, + changelogUnread, + readChangelog, + updateReady, + updateAvailable: useAtomValue(updateAvailableAtom), + downloadProgress, + currentVersion: useAtomValue(currentVersionAtom), }; }; diff --git a/packages/frontend/hooks/tsconfig.json b/packages/frontend/hooks/tsconfig.json index f0040e1558..9b142bb210 100644 --- a/packages/frontend/hooks/tsconfig.json +++ b/packages/frontend/hooks/tsconfig.json @@ -9,6 +9,7 @@ "references": [ { "path": "../../common/env" }, { "path": "../../common/y-indexeddb" }, - { "path": "../../common/debug" } + { "path": "../../common/debug" }, + { "path": "../../common/infra" } ] } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 4aa27db82a..3492354538 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -373,6 +373,7 @@ "com.affine.appUpdater.downloading": "Downloading", "com.affine.appUpdater.installUpdate": "Restart to install update", "com.affine.appUpdater.openDownloadPage": "Open download page", + "com.affine.appUpdater.downloadUpdate": "Download update", "com.affine.appUpdater.updateAvailable": "Update available", "com.affine.appUpdater.whatsNew": "Discover what's new!", "com.affine.appearanceSettings.clientBorder.description": "Customise the appearance of the client.", diff --git a/yarn.lock b/yarn.lock index 5f4dc4e6a6..444dedbeb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -402,6 +402,7 @@ __metadata: intl-segmenter-polyfill-rs: "npm:^0.1.6" jotai: "npm:^2.5.1" jotai-devtools: "npm:^0.7.0" + jotai-effect: "npm:^0.2.3" lit: "npm:^3.0.2" lodash-es: "npm:^4.17.21" lottie-web: "npm:^5.12.2" @@ -524,7 +525,6 @@ __metadata: peerDependencies: "@affine/templates": "workspace:*" "@blocksuite/global": 0.0.0-20230409084303-221991d4-nightly - "@toeverything/infra": "workspace:*" languageName: unknown linkType: soft @@ -13665,12 +13665,14 @@ __metadata: "@blocksuite/presets": "npm:0.11.0-nightly-202312070955-2b5bb47" "@blocksuite/store": "npm:0.11.0-nightly-202312070955-2b5bb47" "@testing-library/react": "npm:^14.0.0" + "@toeverything/infra": "workspace:*" "@types/image-blob-reduce": "npm:^4.1.3" "@types/lodash.debounce": "npm:^4.0.7" fake-indexeddb: "npm:^5.0.0" foxact: "npm:^0.2.20" image-blob-reduce: "npm:^4.1.0" jotai: "npm:^2.5.1" + jotai-effect: "npm:^0.2.3" lodash.debounce: "npm:^4.0.8" p-queue: "npm:^7.4.1" react: "npm:18.2.0" @@ -13709,6 +13711,7 @@ __metadata: resolution: "@toeverything/infra@workspace:packages/common/infra" dependencies: "@affine-test/fixtures": "workspace:*" + "@affine/env": "workspace:*" "@affine/sdk": "workspace:*" "@affine/templates": "workspace:*" "@blocksuite/blocks": "npm:0.11.0-nightly-202312070955-2b5bb47"