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

View File

@@ -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',

View File

@@ -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}