mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(core): add manual check for updates (#4957)
work for #4523 add `appBuildType` to `runtimeConfig` add `useAppUpdater` to manage client updates <!-- copilot:summary --> ### <samp>🤖[[deprecated]](https://githubnext.com/copilot-for-prs-sunset) Generated by Copilot at cdd012c</samp> This pull request refactors and enhances the update functionality for the frontend. It introduces a new custom hook `useAppUpdater` that simplifies the update logic and state management, and uses it in various components and commands. It also adds more options and feedback for the user to control and monitor the update process, such as manual download, auto-check, and auto-download toggles, and update status and progress indicators. It also updates the `AboutAffine` component to show the app icon, version, and build type. It also adds new translations, dependencies, types, and schemas related to the update functionality. <img width="1073" alt="image" src="https://github.com/toeverything/AFFiNE/assets/102217452/16ae7a6a-0035-4e57-902b-6b8f63169501">
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { updateReadyAtom } from '@affine/component/app-sidebar/app-updater-button';
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ResetIcon } from '@blocksuite/icons';
|
||||
import { updateReadyAtom } from '@toeverything/hooks/use-app-updater';
|
||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
|
||||
@@ -4,14 +4,39 @@ import { SettingRow } from '@affine/component/setting-components';
|
||||
import { SettingWrapper } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons';
|
||||
import { useAppUpdater } from '@toeverything/hooks/use-app-updater';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../hooks/affine/use-app-setting-helper';
|
||||
import { appIconMap, appNames } from '../../../../../pages/open-app';
|
||||
import { relatedLinks } from './config';
|
||||
import { communityItem, communityWrapper, link } from './style.css';
|
||||
import * as styles from './style.css';
|
||||
import { UpdateCheckSection } from './update-check-section';
|
||||
|
||||
export const AboutAffine = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { appSettings, updateSettings } = useAppSettingHelper();
|
||||
const { toggleAutoCheck, toggleAutoDownload } = useAppUpdater();
|
||||
const channel = runtimeConfig.appBuildType;
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
|
||||
const onSwitchAutoCheck = useCallback(
|
||||
(checked: boolean) => {
|
||||
toggleAutoCheck(checked);
|
||||
updateSettings('autoCheckUpdate', checked);
|
||||
},
|
||||
[toggleAutoCheck, updateSettings]
|
||||
);
|
||||
|
||||
const onSwitchAutoDownload = useCallback(
|
||||
(checked: boolean) => {
|
||||
toggleAutoDownload(checked);
|
||||
updateSettings('autoDownloadUpdate', checked);
|
||||
},
|
||||
[toggleAutoDownload, updateSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -21,26 +46,26 @@ export const AboutAffine = () => {
|
||||
/>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.version.title']()}>
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.version.app']()}
|
||||
name={appName}
|
||||
desc={runtimeConfig.appVersion}
|
||||
/>
|
||||
className={styles.appImageRow}
|
||||
>
|
||||
<img src={appIcon} alt={appName} width={56} height={56} />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.version.editor.title']()}
|
||||
desc={runtimeConfig.editorVersion}
|
||||
/>
|
||||
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
|
||||
{environment.isDesktop ? (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.checkUpdate.title']()}
|
||||
desc={t['com.affine.aboutAFFiNE.checkUpdate.description']()}
|
||||
/>
|
||||
<UpdateCheckSection />
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.autoCheckUpdate.title']()}
|
||||
desc={t['com.affine.aboutAFFiNE.autoCheckUpdate.description']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => updateSettings('autoCheckUpdate', checked)}
|
||||
onChange={onSwitchAutoCheck}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
@@ -50,8 +75,8 @@ export const AboutAffine = () => {
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => updateSettings('autoCheckUpdate', checked)}
|
||||
checked={appSettings.autoDownloadUpdate}
|
||||
onChange={onSwitchAutoDownload}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
@@ -69,7 +94,7 @@ export const AboutAffine = () => {
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.contact.title']()}>
|
||||
<a
|
||||
className={link}
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
@@ -78,7 +103,7 @@ export const AboutAffine = () => {
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<a
|
||||
className={link}
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://community.affine.pro"
|
||||
target="_blank"
|
||||
@@ -88,11 +113,11 @@ export const AboutAffine = () => {
|
||||
</a>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.community.title']()}>
|
||||
<div className={communityWrapper}>
|
||||
<div className={styles.communityWrapper}>
|
||||
{relatedLinks.map(({ icon, title, link }) => {
|
||||
return (
|
||||
<div
|
||||
className={communityItem}
|
||||
className={styles.communityItem}
|
||||
onClick={() => {
|
||||
window.open(link, '_blank');
|
||||
}}
|
||||
@@ -107,7 +132,7 @@ export const AboutAffine = () => {
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.legal.title']()}>
|
||||
<a
|
||||
className={link}
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://affine.pro/privacy"
|
||||
target="_blank"
|
||||
@@ -116,7 +141,7 @@ export const AboutAffine = () => {
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<a
|
||||
className={link}
|
||||
className={styles.link}
|
||||
rel="noreferrer"
|
||||
href="https://affine.pro/terms"
|
||||
target="_blank"
|
||||
|
||||
@@ -43,3 +43,37 @@ globalStyle(`${communityItem} p`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
export const checkUpdateDesc = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
selectors: {
|
||||
'&.active': {
|
||||
color: 'var(--affine-text-emphasis-color)',
|
||||
},
|
||||
'&.error': {
|
||||
color: 'var(--affine-error-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${checkUpdateDesc} svg`, {
|
||||
marginRight: '4px',
|
||||
});
|
||||
|
||||
export const appImageRow = style({
|
||||
flexDirection: 'row-reverse',
|
||||
selectors: {
|
||||
'&.two-col': {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${appImageRow} .right-col`, {
|
||||
paddingLeft: '0',
|
||||
paddingRight: '20px',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import {
|
||||
downloadProgressAtom,
|
||||
isCheckingForUpdatesAtom,
|
||||
updateAvailableAtom,
|
||||
updateReadyAtom,
|
||||
useAppUpdater,
|
||||
} from '@toeverything/hooks/use-app-updater';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Loading } from '../../../../pure/workspace-slider-bar/workspace-card/loading-icon';
|
||||
import * as styles from './style.css';
|
||||
|
||||
enum CheckUpdateStatus {
|
||||
UNCHECK = 'uncheck',
|
||||
LATEST = 'latest',
|
||||
UPDATE_AVAILABLE = 'update-available',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
const useUpdateStatusLabels = (checkUpdateStatus: CheckUpdateStatus) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const isCheckingForUpdates = useAtomValue(isCheckingForUpdatesAtom);
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
|
||||
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 (
|
||||
checkUpdateStatus === CheckUpdateStatus.LATEST ||
|
||||
checkUpdateStatus === CheckUpdateStatus.ERROR
|
||||
) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.button.retry']();
|
||||
}
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.button.check']();
|
||||
}, [checkUpdateStatus, downloadProgress, t, updateAvailable, updateReady]);
|
||||
|
||||
const subtitleLabel = useMemo(() => {
|
||||
if (updateAvailable && downloadProgress === null) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.update-available']({
|
||||
version: updateAvailable.version,
|
||||
});
|
||||
} else if (isCheckingForUpdates) {
|
||||
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) {
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.latest']();
|
||||
}
|
||||
return t['com.affine.aboutAFFiNE.checkUpdate.subtitle.check']();
|
||||
}, [
|
||||
checkUpdateStatus,
|
||||
downloadProgress,
|
||||
isCheckingForUpdates,
|
||||
t,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(styles.checkUpdateDesc, {
|
||||
active:
|
||||
updateReady ||
|
||||
(updateAvailable && downloadProgress === null) ||
|
||||
checkUpdateStatus === CheckUpdateStatus.LATEST,
|
||||
error: checkUpdateStatus === CheckUpdateStatus.ERROR,
|
||||
})}
|
||||
>
|
||||
{isCheckingForUpdates ? <Loading size={14} /> : null}
|
||||
{subtitleLabel}
|
||||
</span>
|
||||
);
|
||||
}, [
|
||||
checkUpdateStatus,
|
||||
downloadProgress,
|
||||
isCheckingForUpdates,
|
||||
subtitleLabel,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
return { subtitle, buttonLabel };
|
||||
};
|
||||
|
||||
export const UpdateCheckSection = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { checkForUpdates, downloadUpdate, quitAndInstall } = useAppUpdater();
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
const [checkUpdateStatus, setCheckUpdateStatus] = useState<CheckUpdateStatus>(
|
||||
CheckUpdateStatus.UNCHECK
|
||||
);
|
||||
|
||||
const { buttonLabel, subtitle } = useUpdateStatusLabels(checkUpdateStatus);
|
||||
|
||||
const asyncCheckForUpdates = useAsyncCallback(async () => {
|
||||
let statusCheck = CheckUpdateStatus.UNCHECK;
|
||||
try {
|
||||
const status = await checkForUpdates();
|
||||
|
||||
if (status === null) {
|
||||
statusCheck = CheckUpdateStatus.ERROR;
|
||||
} else if (status === false) {
|
||||
statusCheck = CheckUpdateStatus.LATEST;
|
||||
} else if (typeof status === 'string') {
|
||||
statusCheck = CheckUpdateStatus.UPDATE_AVAILABLE;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
statusCheck = CheckUpdateStatus.ERROR;
|
||||
} finally {
|
||||
setCheckUpdateStatus(statusCheck);
|
||||
}
|
||||
}, [checkForUpdates]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (updateAvailable && downloadProgress === null) {
|
||||
return downloadUpdate();
|
||||
}
|
||||
if (updateReady) {
|
||||
return quitAndInstall();
|
||||
}
|
||||
asyncCheckForUpdates();
|
||||
}, [
|
||||
asyncCheckForUpdates,
|
||||
downloadProgress,
|
||||
downloadUpdate,
|
||||
quitAndInstall,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['com.affine.aboutAFFiNE.checkUpdate.title']()}
|
||||
desc={subtitle}
|
||||
>
|
||||
<Button
|
||||
data-testid="check-update-button"
|
||||
onClick={handleClick}
|
||||
disabled={downloadProgress !== null && !updateReady}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,11 @@ import type { Page } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
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';
|
||||
@@ -102,6 +107,10 @@ 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(
|
||||
@@ -150,6 +159,26 @@ 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 {
|
||||
|
||||
@@ -37,14 +37,14 @@ const schemaToChanel = {
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Schema, Channel>;
|
||||
|
||||
const appIconMap = {
|
||||
export const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
const appNames = {
|
||||
export const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
|
||||
Reference in New Issue
Block a user