Show update UI on backup version mismatch
This commit is contained in:
parent
8557de20c2
commit
230ecdf7c9
15 changed files with 344 additions and 66 deletions
|
@ -1,30 +1,77 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { InstallScreenBackupStep } from '../../types/InstallScreen';
|
||||
import { sleep } from '../../util/sleep';
|
||||
import {
|
||||
InstallScreenBackupStep,
|
||||
InstallScreenBackupError,
|
||||
} from '../../types/InstallScreen';
|
||||
import { DialogType } from '../../types/Dialogs';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { PropsType } from './InstallScreenBackupImportStep';
|
||||
import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const DEFAULT_UPDATES = {
|
||||
dialogType: DialogType.None,
|
||||
didSnooze: false,
|
||||
isCheckingForUpdates: false,
|
||||
showEventsCount: 0,
|
||||
downloadSize: 42 * 1024 * 1024,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/InstallScreenBackupImportStep',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = (args: PropsType) => (
|
||||
<InstallScreenBackupImportStep
|
||||
{...args}
|
||||
i18n={i18n}
|
||||
onCancel={action('onCancel')}
|
||||
onRetry={action('onRetry')}
|
||||
/>
|
||||
);
|
||||
const Template: StoryFn<PropsType> = (args: PropsType) => {
|
||||
const [updates, setUpdates] = useState(DEFAULT_UPDATES);
|
||||
const forceUpdate = useCallback(async () => {
|
||||
setUpdates(state => ({
|
||||
...state,
|
||||
isCheckingForUpdates: true,
|
||||
}));
|
||||
await sleep(500);
|
||||
setUpdates(state => ({
|
||||
...state,
|
||||
isCheckingForUpdates: false,
|
||||
dialogType: DialogType.Downloading,
|
||||
downloadSize: 100,
|
||||
downloadedSize: 0,
|
||||
version: 'v7.7.7',
|
||||
}));
|
||||
await sleep(500);
|
||||
setUpdates(state => ({
|
||||
...state,
|
||||
downloadedSize: 50,
|
||||
}));
|
||||
await sleep(500);
|
||||
setUpdates(state => ({
|
||||
...state,
|
||||
downloadedSize: 100,
|
||||
}));
|
||||
}, [setUpdates]);
|
||||
|
||||
return (
|
||||
<InstallScreenBackupImportStep
|
||||
{...args}
|
||||
i18n={i18n}
|
||||
updates={updates}
|
||||
currentVersion="v6.0.0"
|
||||
OS="macOS"
|
||||
startUpdate={action('startUpdate')}
|
||||
forceUpdate={forceUpdate}
|
||||
onCancel={action('onCancel')}
|
||||
onRetry={action('onRetry')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoBytes = Template.bind({});
|
||||
NoBytes.args = {
|
||||
|
@ -52,7 +99,15 @@ Error.args = {
|
|||
backupStep: InstallScreenBackupStep.Download,
|
||||
currentBytes: 500 * 1024,
|
||||
totalBytes: 1024 * 1024,
|
||||
hasError: true,
|
||||
error: InstallScreenBackupError.Unknown,
|
||||
};
|
||||
|
||||
export const UnsupportedVersion = Template.bind({});
|
||||
UnsupportedVersion.args = {
|
||||
backupStep: InstallScreenBackupStep.Process,
|
||||
currentBytes: 1,
|
||||
totalBytes: 1024 * 1024,
|
||||
error: InstallScreenBackupError.UnsupportedVersion,
|
||||
};
|
||||
|
||||
export const Processing = Template.bind({});
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { InstallScreenBackupStep } from '../../types/InstallScreen';
|
||||
import type { UpdatesStateType } from '../../state/ducks/updates';
|
||||
import {
|
||||
InstallScreenStep,
|
||||
InstallScreenBackupStep,
|
||||
InstallScreenBackupError,
|
||||
} from '../../types/InstallScreen';
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { ProgressBar } from '../ProgressBar';
|
||||
|
@ -14,6 +19,7 @@ import { roundFractionForProgressBar } from '../../util/numbers';
|
|||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { SYNCING_MESSAGES_SECURITY_URL } from '../../types/support';
|
||||
import { I18n } from '../I18n';
|
||||
import { InstallScreenUpdateDialog } from './InstallScreenUpdateDialog';
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
|
@ -23,9 +29,16 @@ export type PropsType = Readonly<{
|
|||
backupStep: InstallScreenBackupStep;
|
||||
currentBytes?: number;
|
||||
totalBytes?: number;
|
||||
hasError?: boolean;
|
||||
error?: InstallScreenBackupError;
|
||||
onCancel: () => void;
|
||||
onRetry: () => void;
|
||||
|
||||
// Updater UI
|
||||
updates: UpdatesStateType;
|
||||
currentVersion: string;
|
||||
OS: string;
|
||||
startUpdate: () => void;
|
||||
forceUpdate: () => void;
|
||||
}>;
|
||||
|
||||
export function InstallScreenBackupImportStep({
|
||||
|
@ -33,9 +46,15 @@ export function InstallScreenBackupImportStep({
|
|||
backupStep,
|
||||
currentBytes,
|
||||
totalBytes,
|
||||
hasError,
|
||||
error,
|
||||
onCancel,
|
||||
onRetry,
|
||||
|
||||
updates,
|
||||
currentVersion,
|
||||
OS,
|
||||
startUpdate,
|
||||
forceUpdate,
|
||||
}: PropsType): JSX.Element {
|
||||
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
|
||||
const [isConfirmingSkip, setIsConfirmingSkip] = useState(false);
|
||||
|
@ -123,6 +142,47 @@ export function InstallScreenBackupImportStep({
|
|||
</a>
|
||||
);
|
||||
|
||||
let errorElem: JSX.Element | undefined;
|
||||
if (error == null) {
|
||||
// no-op
|
||||
} else if (error === InstallScreenBackupError.UnsupportedVersion) {
|
||||
errorElem = (
|
||||
<InstallScreenUpdateDialog
|
||||
i18n={i18n}
|
||||
{...updates}
|
||||
step={InstallScreenStep.BackupImport}
|
||||
startUpdate={startUpdate}
|
||||
forceUpdate={forceUpdate}
|
||||
currentVersion={currentVersion}
|
||||
onClose={confirmSkip}
|
||||
OS={OS}
|
||||
/>
|
||||
);
|
||||
} else if (error === InstallScreenBackupError.Unknown) {
|
||||
if (!isConfirmingSkip) {
|
||||
errorElem = (
|
||||
<ConfirmationDialog
|
||||
dialogName="InstallScreenBackupImportStep.error"
|
||||
title={i18n('icu:BackupImportScreen__error__title')}
|
||||
cancelText={i18n('icu:BackupImportScreen__skip')}
|
||||
actions={[
|
||||
{
|
||||
action: onRetryWrap,
|
||||
style: 'affirmative',
|
||||
text: i18n('icu:BackupImportScreen__error__confirm'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={confirmSkip}
|
||||
>
|
||||
{i18n('icu:BackupImportScreen__error__body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw missingCaseError(error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="InstallScreenBackupImportStep">
|
||||
<TitlebarDragArea />
|
||||
|
@ -202,24 +262,7 @@ export function InstallScreenBackupImportStep({
|
|||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{hasError && !isConfirmingSkip && (
|
||||
<ConfirmationDialog
|
||||
dialogName="InstallScreenBackupImportStep.error"
|
||||
title={i18n('icu:BackupImportScreen__error__title')}
|
||||
cancelText={i18n('icu:BackupImportScreen__skip')}
|
||||
actions={[
|
||||
{
|
||||
action: onRetryWrap,
|
||||
style: 'affirmative',
|
||||
text: i18n('icu:BackupImportScreen__error__confirm'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={confirmSkip}
|
||||
>
|
||||
{i18n('icu:BackupImportScreen__error__body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
{errorElem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ const LOADED_URL = {
|
|||
const DEFAULT_UPDATES = {
|
||||
dialogType: DialogType.None,
|
||||
didSnooze: false,
|
||||
isCheckingForUpdates: false,
|
||||
showEventsCount: 0,
|
||||
downloadSize: 67 * 1024 * 1024,
|
||||
downloadedSize: 15 * 1024 * 1024,
|
||||
|
@ -63,6 +64,7 @@ function Simulation({
|
|||
updates={DEFAULT_UPDATES}
|
||||
OS="macOS"
|
||||
startUpdate={action('startUpdate')}
|
||||
forceUpdate={action('forceUpdate')}
|
||||
currentVersion="v6.0.0"
|
||||
retryGetQrCode={action('retryGetQrCode')}
|
||||
/>
|
||||
|
@ -80,6 +82,7 @@ export function QrCodeLoading(): JSX.Element {
|
|||
updates={DEFAULT_UPDATES}
|
||||
OS="macOS"
|
||||
startUpdate={action('startUpdate')}
|
||||
forceUpdate={action('forceUpdate')}
|
||||
currentVersion="v6.0.0"
|
||||
retryGetQrCode={action('retryGetQrCode')}
|
||||
/>
|
||||
|
@ -98,6 +101,7 @@ export function QrCodeFailedToLoad(): JSX.Element {
|
|||
updates={DEFAULT_UPDATES}
|
||||
OS="macOS"
|
||||
startUpdate={action('startUpdate')}
|
||||
forceUpdate={action('forceUpdate')}
|
||||
currentVersion="v6.0.0"
|
||||
retryGetQrCode={action('retryGetQrCode')}
|
||||
/>
|
||||
|
@ -113,6 +117,7 @@ export function QrCodeLoaded(): JSX.Element {
|
|||
updates={DEFAULT_UPDATES}
|
||||
OS="macOS"
|
||||
startUpdate={action('startUpdate')}
|
||||
forceUpdate={action('forceUpdate')}
|
||||
currentVersion="v6.0.0"
|
||||
retryGetQrCode={action('retryGetQrCode')}
|
||||
/>
|
||||
|
@ -177,6 +182,7 @@ export const WithUpdateKnobs: StoryFn<PropsType & { dialogType: DialogType }> =
|
|||
}}
|
||||
OS="macOS"
|
||||
startUpdate={action('startUpdate')}
|
||||
forceUpdate={action('forceUpdate')}
|
||||
currentVersion={currentVersion}
|
||||
retryGetQrCode={action('retryGetQrCode')}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,10 @@ import React, { useCallback } from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { InstallScreenQRCodeError } from '../../types/InstallScreen';
|
||||
import {
|
||||
InstallScreenStep,
|
||||
InstallScreenQRCodeError,
|
||||
} from '../../types/InstallScreen';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
|
@ -33,6 +36,7 @@ export type PropsType = Readonly<{
|
|||
isStaging: boolean;
|
||||
retryGetQrCode: () => void;
|
||||
startUpdate: () => void;
|
||||
forceUpdate: () => void;
|
||||
}>;
|
||||
|
||||
const getQrCodeClassName = getClassNamesFor(
|
||||
|
@ -51,6 +55,7 @@ export function InstallScreenQrCodeNotScannedStep({
|
|||
provisioningUrl,
|
||||
retryGetQrCode,
|
||||
startUpdate,
|
||||
forceUpdate,
|
||||
updates,
|
||||
}: Readonly<PropsType>): ReactElement {
|
||||
return (
|
||||
|
@ -63,7 +68,9 @@ export function InstallScreenQrCodeNotScannedStep({
|
|||
<InstallScreenUpdateDialog
|
||||
i18n={i18n}
|
||||
{...updates}
|
||||
step={InstallScreenStep.QrCodeNotScanned}
|
||||
startUpdate={startUpdate}
|
||||
forceUpdate={forceUpdate}
|
||||
currentVersion={currentVersion}
|
||||
OS={OS}
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@ import React from 'react';
|
|||
import { noop } from 'lodash';
|
||||
|
||||
import { DialogType } from '../../types/Dialogs';
|
||||
import { InstallScreenStep } from '../../types/InstallScreen';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
PRODUCTION_DOWNLOAD_URL,
|
||||
|
@ -13,6 +14,8 @@ import {
|
|||
} from '../../types/support';
|
||||
import type { UpdatesStateType } from '../../state/ducks/updates';
|
||||
import { isBeta } from '../../util/version';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { roundFractionForProgressBar } from '../../util/numbers';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Modal } from '../Modal';
|
||||
import { I18n } from '../I18n';
|
||||
|
@ -21,19 +24,26 @@ import { formatFileSize } from '../../util/formatFileSize';
|
|||
export type PropsType = UpdatesStateType &
|
||||
Readonly<{
|
||||
i18n: LocalizerType;
|
||||
step: InstallScreenStep;
|
||||
forceUpdate: () => void;
|
||||
startUpdate: () => void;
|
||||
currentVersion: string;
|
||||
OS: string;
|
||||
onClose?: () => void;
|
||||
}>;
|
||||
|
||||
export function InstallScreenUpdateDialog({
|
||||
i18n,
|
||||
step,
|
||||
dialogType,
|
||||
isCheckingForUpdates,
|
||||
downloadSize,
|
||||
downloadedSize,
|
||||
forceUpdate,
|
||||
startUpdate,
|
||||
currentVersion,
|
||||
OS,
|
||||
onClose = noop,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
||||
<a
|
||||
|
@ -48,6 +58,40 @@ export function InstallScreenUpdateDialog({
|
|||
|
||||
const dialogName = `InstallScreenUpdateDialog.${dialogType}`;
|
||||
|
||||
if (dialogType === DialogType.None) {
|
||||
if (step === InstallScreenStep.BackupImport) {
|
||||
if (isCheckingForUpdates) {
|
||||
return <DownloadingModal i18n={i18n} width={0} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName={dialogName}
|
||||
noMouseClose
|
||||
onClose={onClose}
|
||||
noDefaultCancelButton
|
||||
actions={[
|
||||
{
|
||||
id: 'ok',
|
||||
text: i18n(
|
||||
'icu:InstallScreenUpdateDialog--update-required__action-update'
|
||||
),
|
||||
action: forceUpdate,
|
||||
style: 'affirmative',
|
||||
autoClose: false,
|
||||
},
|
||||
]}
|
||||
title={i18n('icu:InstallScreenUpdateDialog--update-required__title')}
|
||||
>
|
||||
{i18n('icu:InstallScreenUpdateDialog--update-required__body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.UnsupportedOS) {
|
||||
return (
|
||||
<Modal
|
||||
|
@ -109,6 +153,7 @@ export function InstallScreenUpdateDialog({
|
|||
i18n={i18n}
|
||||
dialogName={dialogName}
|
||||
title={title}
|
||||
noMouseClose
|
||||
noDefaultCancelButton
|
||||
actions={[
|
||||
{
|
||||
|
@ -119,7 +164,7 @@ export function InstallScreenUpdateDialog({
|
|||
autoClose: false,
|
||||
},
|
||||
]}
|
||||
onClose={noop}
|
||||
onClose={onClose}
|
||||
>
|
||||
{bodyText}
|
||||
</ConfirmationDialog>
|
||||
|
@ -127,27 +172,10 @@ export function InstallScreenUpdateDialog({
|
|||
}
|
||||
|
||||
if (dialogType === DialogType.Downloading) {
|
||||
// Focus trap can't be used because there are no elements that can be
|
||||
// focused within the modal.
|
||||
const width = Math.ceil(
|
||||
((downloadedSize || 1) / (downloadSize || 1)) * 100
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
modalName={dialogName}
|
||||
noMouseClose
|
||||
useFocusTrap={false}
|
||||
title={i18n('icu:DialogUpdate__downloading')}
|
||||
>
|
||||
<div className="InstallScreenUpdateDialog__progress--container">
|
||||
<div
|
||||
className="InstallScreenUpdateDialog__progress--bar"
|
||||
style={{ transform: `translateX(${width - 100}%)` }}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
const fractionComplete = roundFractionForProgressBar(
|
||||
(downloadedSize || 0) / (downloadSize || 1)
|
||||
);
|
||||
return <DownloadingModal i18n={i18n} width={fractionComplete * 100} />;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -179,6 +207,7 @@ export function InstallScreenUpdateDialog({
|
|||
dialogName={dialogName}
|
||||
moduleClassName="InstallScreenUpdateDialog"
|
||||
title={title}
|
||||
noMouseClose
|
||||
noDefaultCancelButton
|
||||
actions={[
|
||||
{
|
||||
|
@ -188,7 +217,7 @@ export function InstallScreenUpdateDialog({
|
|||
autoClose: false,
|
||||
},
|
||||
]}
|
||||
onClose={noop}
|
||||
onClose={onClose}
|
||||
>
|
||||
{body}
|
||||
</ConfirmationDialog>
|
||||
|
@ -230,5 +259,32 @@ export function InstallScreenUpdateDialog({
|
|||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
throw missingCaseError(dialogType);
|
||||
}
|
||||
|
||||
export function DownloadingModal({
|
||||
i18n,
|
||||
width,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
width: number;
|
||||
}): JSX.Element {
|
||||
// Focus trap can't be used because there are no elements that can be
|
||||
// focused within the modal.
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
modalName="InstallScreenUpdateDialog.Downloading"
|
||||
noMouseClose
|
||||
useFocusTrap={false}
|
||||
title={i18n('icu:DialogUpdate__downloading')}
|
||||
>
|
||||
<div className="InstallScreenUpdateDialog__progress--container">
|
||||
<div
|
||||
className="InstallScreenUpdateDialog__progress--bar"
|
||||
style={{ transform: `translateX(${width - 100}%)` }}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue