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.
This commit is contained in:
Peng Xiao
2023-12-08 03:20:02 +00:00
parent 453d4db713
commit fcd43033fe
30 changed files with 441 additions and 343 deletions
+1 -2
View File
@@ -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"
-3
View File
@@ -7,9 +7,6 @@
"outDir": "lib"
},
"references": [
{
"path": "../infra"
},
{
"path": "../../../tests/fixtures"
}
+1
View File
@@ -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",
+8
View File
@@ -0,0 +1,8 @@
import { atom } from 'jotai';
export const loadedPluginNameAtom = atom<string[]>([]);
export * from './layout';
export * from './root-store';
export * from './settings';
export * from './workspace';
@@ -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<typeof createStore>) {
rootStore = store;
}
export const loadedPluginNameAtom = atom<string[]>([]);
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
const workspaceId = get(currentWorkspaceIdAtom);
assertExists(workspaceId);
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
return get(currentWorkspaceAtom);
});
import { atom } from 'jotai';
const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
@@ -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<typeof createStore>) {
rootStore = store;
}
@@ -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<AppSetting>('affine-settings', {
type SetStateAction<Value> = 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<Partial<AppSetting>>],
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 };
});
}
);
@@ -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<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
const workspaceId = get(currentWorkspaceIdAtom);
assertExists(workspaceId);
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
return get(currentWorkspaceAtom);
});
+1 -1
View File
@@ -201,7 +201,7 @@ export type UpdaterHandlers = {
downloadUpdate: () => Promise<void>;
getConfig: () => Promise<UpdaterConfig>;
setConfig: (newConfig: Partial<UpdaterConfig>) => Promise<void>;
checkForUpdatesAndNotify: () => Promise<{ version: string } | null>;
checkForUpdates: () => Promise<{ version: string } | null>;
};
export type WorkspaceHandlers = {
+3
View File
@@ -11,6 +11,9 @@
{
"path": "../sdk"
},
{
"path": "../env"
},
{
"path": "./tsconfig.node.json"
}
+1 -1
View File
@@ -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'),
@@ -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',
@@ -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 (
<div className={styles.installLabel}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.downloadUpdate']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
);
}
function UpdateReady({ updateAvailable, appQuitting }: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<div className={styles.updateAvailableWrapper}>
<div className={styles.installLabelNormal}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
<div className={styles.installLabelHover}>
<ResetIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t[appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate']()}
</span>
</div>
</div>
);
}
function DownloadingUpdate({
updateAvailable,
downloadProgress,
}: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<div className={clsx([styles.updateAvailableWrapper])}>
<div className={clsx([styles.installLabelNormal])}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.downloading']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
<div className={styles.progress}>
<div
className={styles.progressInner}
style={{ width: `${downloadProgress}%` }}
/>
</div>
</div>
);
}
function OpenDownloadPage({ updateAvailable }: ButtonContentProps) {
const t = useAFFiNEI18N();
return (
<>
<div className={styles.installLabelNormal}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
</div>
<div className={styles.installLabelHover}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.openDownloadPage']()}
</span>
</div>
</>
);
}
function WhatsNew({ onDismissCurrentChangelog }: ButtonContentProps) {
const t = useAFFiNEI18N();
const onClickClose: React.MouseEventHandler = useCallback(
e => {
onDismissCurrentChangelog();
e.stopPropagation();
},
[onDismissCurrentChangelog]
);
return (
<>
<div className={clsx([styles.whatsNewLabel])}>
<NewIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.whatsNew']()}
</span>
</div>
<div className={styles.closeIcon} onClick={onClickClose}>
<CloseIcon />
</div>
</>
);
}
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(
<button
style={style}
className={clsx([styles.root, className])}
data-has-update={!!updateAvailable}
data-updating={appQuitting}
data-disabled={
(updateAvailable?.allowAutoUpdate && !updateReady) || appQuitting
}
data-disabled={disabled}
onClick={onClickUpdate}
>
{updateAvailableNode}
{whatsNew}
{ContentComponent ? <ContentComponent {...contentProps} /> : null}
<div className={styles.particles} aria-hidden="true"></div>
<span className={styles.halo} aria-hidden="true"></span>
</button>,
updateAvailable?.version
);
function renderUpdateAvailableAllowAutoUpdate() {
return (
<div className={clsx([styles.updateAvailableWrapper])}>
<div className={clsx([styles.installLabelNormal])}>
<span className={styles.ellipsisTextOverflow}>
{!updateReady
? t['com.affine.appUpdater.downloading']()
: t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>
{updateAvailable?.version}
</span>
</div>
{updateReady ? (
<div className={clsx([styles.installLabelHover])}>
<ResetIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t[
appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate'
]()}
</span>
</div>
) : (
<div className={styles.progress}>
<div
className={styles.progressInner}
style={{ width: `${downloadProgress}%` }}
/>
</div>
)}
</div>
);
}
function renderUpdateAvailableNotAllowAutoUpdate() {
return (
<>
<div className={clsx([styles.installLabelNormal])}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.updateAvailable']()}
</span>
<span className={styles.versionLabel}>
{updateAvailable?.version}
</span>
</div>
<div className={clsx([styles.installLabelHover])}>
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.openDownloadPage']()}
</span>
</div>
</>
);
}
function renderWhatsNew() {
return (
<>
<div className={clsx([styles.whatsNewLabel])}>
<NewIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>
{t['com.affine.appUpdater.whatsNew']()}
</span>
</div>
<div
className={styles.closeIcon}
onClick={e => {
onDismissCurrentChangelog();
e.stopPropagation();
}}
>
<CloseIcon />
</div>
</>
);
}
}
// 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 (
<AppUpdaterButtonPure
appQuitting={appQuitting}
autoDownload={autoDownload}
updateReady={!!updateReady}
onClickUpdate={handleClickUpdate}
onDismissCurrentChangelog={dismissCurrentChangelog}
currentChangelogUnread={currentChangelogUnread}
onDismissCurrentChangelog={readChangelog}
currentChangelogUnread={changelogUnread}
updateAvailable={updateAvailable}
downloadProgress={downloadProgress}
className={className}
+1
View File
@@ -62,6 +62,7 @@
"intl-segmenter-polyfill-rs": "^0.1.6",
"jotai": "^2.5.1",
"jotai-devtools": "^0.7.0",
"jotai-effect": "^0.2.3",
"lit": "^3.0.2",
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.7.6",
@@ -1,5 +1,6 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SettingsIcon } from '@blocksuite/icons';
import { appSettingAtom } from '@toeverything/infra/atom';
import {
PreconditionStrategy,
registerAffineCommand,
@@ -8,7 +9,6 @@ import { type createStore } from 'jotai';
import type { useTheme } from 'next-themes';
import { openQuickSearchModalAtom } from '../atoms';
import { appSettingAtom } from '../atoms/settings';
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
export function registerAffineSettingsCommands({
@@ -3,15 +3,8 @@ import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import {
downloadProgressAtom,
isCheckingForUpdatesAtom,
updateAvailableAtom,
updateReadyAtom,
useAppUpdater,
} from '@toeverything/hooks/use-app-updater';
import { useAppUpdater } from '@toeverything/hooks/use-app-updater';
import clsx from 'clsx';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo, useState } from 'react';
import * as styles from './style.css';
@@ -25,18 +18,16 @@ enum CheckUpdateStatus {
const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => {
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 ? <Loading size={14} /> : null}
{checkingForUpdates ? <Loading size={14} /> : null}
{subtitleLabel}
</span>
);
}, [
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>(
CheckUpdateStatus.UNCHECK
);
@@ -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 {
@@ -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';
@@ -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';
@@ -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 {
@@ -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);
@@ -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';
@@ -125,7 +125,7 @@ export function createApplicationMenu() {
{
label: 'Check for Updates',
click: async () => {
await checkForUpdates(true);
await checkForUpdates();
},
},
],
@@ -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<UpdaterConfig> = {}): 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);
});
});
};
@@ -29,8 +29,8 @@ export const updaterHandlers = {
): Promise<void> => {
return setConfig(newConfig);
},
checkForUpdatesAndNotify: async () => {
const res = await checkForUpdates(true);
checkForUpdates: async () => {
const res = await checkForUpdates();
if (res) {
const { updateInfo } = res;
return {
+2
View File
@@ -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",
+76 -70
View File
@@ -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<T>,
@@ -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<UpdateMeta | null>(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<Record<string, boolean>>(
{}
);
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),
};
};
+2 -1
View File
@@ -9,6 +9,7 @@
"references": [
{ "path": "../../common/env" },
{ "path": "../../common/y-indexeddb" },
{ "path": "../../common/debug" }
{ "path": "../../common/debug" },
{ "path": "../../common/infra" }
]
}
@@ -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.",
+4 -1
View File
@@ -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"