Show update UI on backup version mismatch
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
fece7b0e42
commit
9914fefe1c
15 changed files with 344 additions and 66 deletions
|
@ -6263,6 +6263,18 @@
|
||||||
"messageformat": "Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.",
|
"messageformat": "Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.",
|
||||||
"description": "The title of outage dialog during service outage."
|
"description": "The title of outage dialog during service outage."
|
||||||
},
|
},
|
||||||
|
"icu:InstallScreenUpdateDialog--update-required__title": {
|
||||||
|
"messageformat": "Update Required",
|
||||||
|
"description": "The title of update dialog on install screen when app update is required before proceeding with backup import"
|
||||||
|
},
|
||||||
|
"icu:InstallScreenUpdateDialog--update-required__body": {
|
||||||
|
"messageformat": "To complete syncing your messages, update Signal desktop now.",
|
||||||
|
"description": "The body of update dialog on install screen when app update is required before proceeding with backup import"
|
||||||
|
},
|
||||||
|
"icu:InstallScreenUpdateDialog--update-required__action-update": {
|
||||||
|
"messageformat": "Update",
|
||||||
|
"description": "The update action of update dialog on install screen when app update is required before proceeding with backup import"
|
||||||
|
},
|
||||||
"icu:InstallScreenUpdateDialog--unsupported-os__title": {
|
"icu:InstallScreenUpdateDialog--unsupported-os__title": {
|
||||||
"messageformat": "Update Required",
|
"messageformat": "Update Required",
|
||||||
"description": "The title of update dialog on install screen when user OS is unsupported"
|
"description": "The title of update dialog on install screen when user OS is unsupported"
|
||||||
|
|
|
@ -1,30 +1,77 @@
|
||||||
// Copyright 2024 Signal Messenger, LLC
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 type { Meta, StoryFn } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
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 enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { PropsType } from './InstallScreenBackupImportStep';
|
import type { PropsType } from './InstallScreenBackupImportStep';
|
||||||
import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep';
|
import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const DEFAULT_UPDATES = {
|
||||||
|
dialogType: DialogType.None,
|
||||||
|
didSnooze: false,
|
||||||
|
isCheckingForUpdates: false,
|
||||||
|
showEventsCount: 0,
|
||||||
|
downloadSize: 42 * 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/InstallScreenBackupImportStep',
|
title: 'Components/InstallScreenBackupImportStep',
|
||||||
} satisfies Meta<PropsType>;
|
} satisfies Meta<PropsType>;
|
||||||
|
|
||||||
// eslint-disable-next-line react/function-component-definition
|
// eslint-disable-next-line react/function-component-definition
|
||||||
const Template: StoryFn<PropsType> = (args: PropsType) => (
|
const Template: StoryFn<PropsType> = (args: PropsType) => {
|
||||||
<InstallScreenBackupImportStep
|
const [updates, setUpdates] = useState(DEFAULT_UPDATES);
|
||||||
{...args}
|
const forceUpdate = useCallback(async () => {
|
||||||
i18n={i18n}
|
setUpdates(state => ({
|
||||||
onCancel={action('onCancel')}
|
...state,
|
||||||
onRetry={action('onRetry')}
|
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({});
|
export const NoBytes = Template.bind({});
|
||||||
NoBytes.args = {
|
NoBytes.args = {
|
||||||
|
@ -52,7 +99,15 @@ Error.args = {
|
||||||
backupStep: InstallScreenBackupStep.Download,
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
currentBytes: 500 * 1024,
|
currentBytes: 500 * 1024,
|
||||||
totalBytes: 1024 * 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({});
|
export const Processing = Template.bind({});
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
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 { formatFileSize } from '../../util/formatFileSize';
|
||||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||||
import { ProgressBar } from '../ProgressBar';
|
import { ProgressBar } from '../ProgressBar';
|
||||||
|
@ -14,6 +19,7 @@ import { roundFractionForProgressBar } from '../../util/numbers';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { SYNCING_MESSAGES_SECURITY_URL } from '../../types/support';
|
import { SYNCING_MESSAGES_SECURITY_URL } from '../../types/support';
|
||||||
import { I18n } from '../I18n';
|
import { I18n } from '../I18n';
|
||||||
|
import { InstallScreenUpdateDialog } from './InstallScreenUpdateDialog';
|
||||||
|
|
||||||
// We can't always use destructuring assignment because of the complexity of this props
|
// We can't always use destructuring assignment because of the complexity of this props
|
||||||
// type.
|
// type.
|
||||||
|
@ -23,9 +29,16 @@ export type PropsType = Readonly<{
|
||||||
backupStep: InstallScreenBackupStep;
|
backupStep: InstallScreenBackupStep;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
hasError?: boolean;
|
error?: InstallScreenBackupError;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
|
|
||||||
|
// Updater UI
|
||||||
|
updates: UpdatesStateType;
|
||||||
|
currentVersion: string;
|
||||||
|
OS: string;
|
||||||
|
startUpdate: () => void;
|
||||||
|
forceUpdate: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function InstallScreenBackupImportStep({
|
export function InstallScreenBackupImportStep({
|
||||||
|
@ -33,9 +46,15 @@ export function InstallScreenBackupImportStep({
|
||||||
backupStep,
|
backupStep,
|
||||||
currentBytes,
|
currentBytes,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
hasError,
|
error,
|
||||||
onCancel,
|
onCancel,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
|
||||||
|
updates,
|
||||||
|
currentVersion,
|
||||||
|
OS,
|
||||||
|
startUpdate,
|
||||||
|
forceUpdate,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
|
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
|
||||||
const [isConfirmingSkip, setIsConfirmingSkip] = useState(false);
|
const [isConfirmingSkip, setIsConfirmingSkip] = useState(false);
|
||||||
|
@ -123,6 +142,47 @@ export function InstallScreenBackupImportStep({
|
||||||
</a>
|
</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 (
|
return (
|
||||||
<div className="InstallScreenBackupImportStep">
|
<div className="InstallScreenBackupImportStep">
|
||||||
<TitlebarDragArea />
|
<TitlebarDragArea />
|
||||||
|
@ -202,24 +262,7 @@ export function InstallScreenBackupImportStep({
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasError && !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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ const LOADED_URL = {
|
||||||
const DEFAULT_UPDATES = {
|
const DEFAULT_UPDATES = {
|
||||||
dialogType: DialogType.None,
|
dialogType: DialogType.None,
|
||||||
didSnooze: false,
|
didSnooze: false,
|
||||||
|
isCheckingForUpdates: false,
|
||||||
showEventsCount: 0,
|
showEventsCount: 0,
|
||||||
downloadSize: 67 * 1024 * 1024,
|
downloadSize: 67 * 1024 * 1024,
|
||||||
downloadedSize: 15 * 1024 * 1024,
|
downloadedSize: 15 * 1024 * 1024,
|
||||||
|
@ -63,6 +64,7 @@ function Simulation({
|
||||||
updates={DEFAULT_UPDATES}
|
updates={DEFAULT_UPDATES}
|
||||||
OS="macOS"
|
OS="macOS"
|
||||||
startUpdate={action('startUpdate')}
|
startUpdate={action('startUpdate')}
|
||||||
|
forceUpdate={action('forceUpdate')}
|
||||||
currentVersion="v6.0.0"
|
currentVersion="v6.0.0"
|
||||||
retryGetQrCode={action('retryGetQrCode')}
|
retryGetQrCode={action('retryGetQrCode')}
|
||||||
/>
|
/>
|
||||||
|
@ -80,6 +82,7 @@ export function QrCodeLoading(): JSX.Element {
|
||||||
updates={DEFAULT_UPDATES}
|
updates={DEFAULT_UPDATES}
|
||||||
OS="macOS"
|
OS="macOS"
|
||||||
startUpdate={action('startUpdate')}
|
startUpdate={action('startUpdate')}
|
||||||
|
forceUpdate={action('forceUpdate')}
|
||||||
currentVersion="v6.0.0"
|
currentVersion="v6.0.0"
|
||||||
retryGetQrCode={action('retryGetQrCode')}
|
retryGetQrCode={action('retryGetQrCode')}
|
||||||
/>
|
/>
|
||||||
|
@ -98,6 +101,7 @@ export function QrCodeFailedToLoad(): JSX.Element {
|
||||||
updates={DEFAULT_UPDATES}
|
updates={DEFAULT_UPDATES}
|
||||||
OS="macOS"
|
OS="macOS"
|
||||||
startUpdate={action('startUpdate')}
|
startUpdate={action('startUpdate')}
|
||||||
|
forceUpdate={action('forceUpdate')}
|
||||||
currentVersion="v6.0.0"
|
currentVersion="v6.0.0"
|
||||||
retryGetQrCode={action('retryGetQrCode')}
|
retryGetQrCode={action('retryGetQrCode')}
|
||||||
/>
|
/>
|
||||||
|
@ -113,6 +117,7 @@ export function QrCodeLoaded(): JSX.Element {
|
||||||
updates={DEFAULT_UPDATES}
|
updates={DEFAULT_UPDATES}
|
||||||
OS="macOS"
|
OS="macOS"
|
||||||
startUpdate={action('startUpdate')}
|
startUpdate={action('startUpdate')}
|
||||||
|
forceUpdate={action('forceUpdate')}
|
||||||
currentVersion="v6.0.0"
|
currentVersion="v6.0.0"
|
||||||
retryGetQrCode={action('retryGetQrCode')}
|
retryGetQrCode={action('retryGetQrCode')}
|
||||||
/>
|
/>
|
||||||
|
@ -177,6 +182,7 @@ export const WithUpdateKnobs: StoryFn<PropsType & { dialogType: DialogType }> =
|
||||||
}}
|
}}
|
||||||
OS="macOS"
|
OS="macOS"
|
||||||
startUpdate={action('startUpdate')}
|
startUpdate={action('startUpdate')}
|
||||||
|
forceUpdate={action('forceUpdate')}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
retryGetQrCode={action('retryGetQrCode')}
|
retryGetQrCode={action('retryGetQrCode')}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,7 +6,10 @@ import React, { useCallback } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import { InstallScreenQRCodeError } from '../../types/InstallScreen';
|
import {
|
||||||
|
InstallScreenStep,
|
||||||
|
InstallScreenQRCodeError,
|
||||||
|
} from '../../types/InstallScreen';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import type { Loadable } from '../../util/loadable';
|
import type { Loadable } from '../../util/loadable';
|
||||||
import { LoadingState } from '../../util/loadable';
|
import { LoadingState } from '../../util/loadable';
|
||||||
|
@ -33,6 +36,7 @@ export type PropsType = Readonly<{
|
||||||
isStaging: boolean;
|
isStaging: boolean;
|
||||||
retryGetQrCode: () => void;
|
retryGetQrCode: () => void;
|
||||||
startUpdate: () => void;
|
startUpdate: () => void;
|
||||||
|
forceUpdate: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const getQrCodeClassName = getClassNamesFor(
|
const getQrCodeClassName = getClassNamesFor(
|
||||||
|
@ -51,6 +55,7 @@ export function InstallScreenQrCodeNotScannedStep({
|
||||||
provisioningUrl,
|
provisioningUrl,
|
||||||
retryGetQrCode,
|
retryGetQrCode,
|
||||||
startUpdate,
|
startUpdate,
|
||||||
|
forceUpdate,
|
||||||
updates,
|
updates,
|
||||||
}: Readonly<PropsType>): ReactElement {
|
}: Readonly<PropsType>): ReactElement {
|
||||||
return (
|
return (
|
||||||
|
@ -63,7 +68,9 @@ export function InstallScreenQrCodeNotScannedStep({
|
||||||
<InstallScreenUpdateDialog
|
<InstallScreenUpdateDialog
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
{...updates}
|
{...updates}
|
||||||
|
step={InstallScreenStep.QrCodeNotScanned}
|
||||||
startUpdate={startUpdate}
|
startUpdate={startUpdate}
|
||||||
|
forceUpdate={forceUpdate}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
OS={OS}
|
OS={OS}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React from 'react';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
import { DialogType } from '../../types/Dialogs';
|
import { DialogType } from '../../types/Dialogs';
|
||||||
|
import { InstallScreenStep } from '../../types/InstallScreen';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import {
|
import {
|
||||||
PRODUCTION_DOWNLOAD_URL,
|
PRODUCTION_DOWNLOAD_URL,
|
||||||
|
@ -13,6 +14,8 @@ import {
|
||||||
} from '../../types/support';
|
} from '../../types/support';
|
||||||
import type { UpdatesStateType } from '../../state/ducks/updates';
|
import type { UpdatesStateType } from '../../state/ducks/updates';
|
||||||
import { isBeta } from '../../util/version';
|
import { isBeta } from '../../util/version';
|
||||||
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { roundFractionForProgressBar } from '../../util/numbers';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
import { I18n } from '../I18n';
|
import { I18n } from '../I18n';
|
||||||
|
@ -21,19 +24,26 @@ import { formatFileSize } from '../../util/formatFileSize';
|
||||||
export type PropsType = UpdatesStateType &
|
export type PropsType = UpdatesStateType &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
step: InstallScreenStep;
|
||||||
|
forceUpdate: () => void;
|
||||||
startUpdate: () => void;
|
startUpdate: () => void;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
OS: string;
|
OS: string;
|
||||||
|
onClose?: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function InstallScreenUpdateDialog({
|
export function InstallScreenUpdateDialog({
|
||||||
i18n,
|
i18n,
|
||||||
|
step,
|
||||||
dialogType,
|
dialogType,
|
||||||
|
isCheckingForUpdates,
|
||||||
downloadSize,
|
downloadSize,
|
||||||
downloadedSize,
|
downloadedSize,
|
||||||
|
forceUpdate,
|
||||||
startUpdate,
|
startUpdate,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
OS,
|
OS,
|
||||||
|
onClose = noop,
|
||||||
}: PropsType): JSX.Element | null {
|
}: PropsType): JSX.Element | null {
|
||||||
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
||||||
<a
|
<a
|
||||||
|
@ -48,6 +58,40 @@ export function InstallScreenUpdateDialog({
|
||||||
|
|
||||||
const dialogName = `InstallScreenUpdateDialog.${dialogType}`;
|
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) {
|
if (dialogType === DialogType.UnsupportedOS) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -109,6 +153,7 @@ export function InstallScreenUpdateDialog({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
dialogName={dialogName}
|
dialogName={dialogName}
|
||||||
title={title}
|
title={title}
|
||||||
|
noMouseClose
|
||||||
noDefaultCancelButton
|
noDefaultCancelButton
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
|
@ -119,7 +164,7 @@ export function InstallScreenUpdateDialog({
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onClose={noop}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
{bodyText}
|
{bodyText}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
|
@ -127,27 +172,10 @@ export function InstallScreenUpdateDialog({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogType === DialogType.Downloading) {
|
if (dialogType === DialogType.Downloading) {
|
||||||
// Focus trap can't be used because there are no elements that can be
|
const fractionComplete = roundFractionForProgressBar(
|
||||||
// focused within the modal.
|
(downloadedSize || 0) / (downloadSize || 1)
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
return <DownloadingModal i18n={i18n} width={fractionComplete * 100} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -179,6 +207,7 @@ export function InstallScreenUpdateDialog({
|
||||||
dialogName={dialogName}
|
dialogName={dialogName}
|
||||||
moduleClassName="InstallScreenUpdateDialog"
|
moduleClassName="InstallScreenUpdateDialog"
|
||||||
title={title}
|
title={title}
|
||||||
|
noMouseClose
|
||||||
noDefaultCancelButton
|
noDefaultCancelButton
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
|
@ -188,7 +217,7 @@ export function InstallScreenUpdateDialog({
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onClose={noop}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</ConfirmationDialog>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
10
ts/services/backups/errors.ts
Normal file
10
ts/services/backups/errors.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type Long from 'long';
|
||||||
|
|
||||||
|
export class UnsupportedBackupVersion extends Error {
|
||||||
|
constructor(version: Long) {
|
||||||
|
super(`Unsupported backup version: ${version}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,7 @@ import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
import { constantTimeEqual } from '../../Crypto';
|
import { constantTimeEqual } from '../../Crypto';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
import { BACKUP_VERSION, WALLPAPER_TO_BUBBLE_COLOR } from './constants';
|
import { BACKUP_VERSION, WALLPAPER_TO_BUBBLE_COLOR } from './constants';
|
||||||
|
import { UnsupportedBackupVersion } from './errors';
|
||||||
import type { AboutMe, LocalChatStyle } from './types';
|
import type { AboutMe, LocalChatStyle } from './types';
|
||||||
import { BackupType } from './types';
|
import { BackupType } from './types';
|
||||||
import { getBackupMediaRootKey } from './crypto';
|
import { getBackupMediaRootKey } from './crypto';
|
||||||
|
@ -253,7 +254,7 @@ export class BackupImportStream extends Writable {
|
||||||
log.info(`${this.logId}: got BackupInfo`);
|
log.info(`${this.logId}: got BackupInfo`);
|
||||||
|
|
||||||
if (info.version?.toNumber() !== BACKUP_VERSION) {
|
if (info.version?.toNumber() !== BACKUP_VERSION) {
|
||||||
throw new Error(`Unsupported backup version: ${info.version}`);
|
throw new UnsupportedBackupVersion(info.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Bytes.isEmpty(info.mediaRootBackupKey)) {
|
if (Bytes.isEmpty(info.mediaRootBackupKey)) {
|
||||||
|
|
|
@ -31,7 +31,10 @@ import type { ExplodePromiseResultType } from '../../util/explodePromise';
|
||||||
import { explodePromise } from '../../util/explodePromise';
|
import { explodePromise } from '../../util/explodePromise';
|
||||||
import type { RetryBackupImportValue } from '../../state/ducks/installer';
|
import type { RetryBackupImportValue } from '../../state/ducks/installer';
|
||||||
import { CipherType, HashType } from '../../types/Crypto';
|
import { CipherType, HashType } from '../../types/Crypto';
|
||||||
import { InstallScreenBackupStep } from '../../types/InstallScreen';
|
import {
|
||||||
|
InstallScreenBackupStep,
|
||||||
|
InstallScreenBackupError,
|
||||||
|
} from '../../types/InstallScreen';
|
||||||
import * as Errors from '../../types/errors';
|
import * as Errors from '../../types/errors';
|
||||||
import { BackupCredentialType } from '../../types/backups';
|
import { BackupCredentialType } from '../../types/backups';
|
||||||
import { HTTPError } from '../../textsecure/Errors';
|
import { HTTPError } from '../../textsecure/Errors';
|
||||||
|
@ -46,6 +49,7 @@ import { BackupCredentials } from './credentials';
|
||||||
import { BackupAPI } from './api';
|
import { BackupAPI } from './api';
|
||||||
import { validateBackup } from './validator';
|
import { validateBackup } from './validator';
|
||||||
import { BackupType } from './types';
|
import { BackupType } from './types';
|
||||||
|
import { UnsupportedBackupVersion } from './errors';
|
||||||
|
|
||||||
export { BackupType };
|
export { BackupType };
|
||||||
|
|
||||||
|
@ -142,7 +146,10 @@ export class BackupsService {
|
||||||
);
|
);
|
||||||
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
||||||
window.reduxActions.installer.updateBackupImportProgress({
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
hasError: true,
|
error:
|
||||||
|
error instanceof UnsupportedBackupVersion
|
||||||
|
? InstallScreenBackupError.UnsupportedVersion
|
||||||
|
: InstallScreenBackupError.Unknown,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
|
|
@ -6,3 +6,7 @@ import { ipcRenderer } from 'electron';
|
||||||
export function startUpdate(): Promise<void> {
|
export function startUpdate(): Promise<void> {
|
||||||
return ipcRenderer.invoke('start-update');
|
return ipcRenderer.invoke('start-update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function forceUpdate(): Promise<void> {
|
||||||
|
return ipcRenderer.invoke('updater/force-update');
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import pTimeout, { TimeoutError } from 'p-timeout';
|
||||||
|
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import {
|
import {
|
||||||
|
type InstallScreenBackupError,
|
||||||
InstallScreenBackupStep,
|
InstallScreenBackupStep,
|
||||||
InstallScreenStep,
|
InstallScreenStep,
|
||||||
InstallScreenError,
|
InstallScreenError,
|
||||||
|
@ -67,7 +68,7 @@ export type InstallerStateType = ReadonlyDeep<
|
||||||
backupStep: InstallScreenBackupStep;
|
backupStep: InstallScreenBackupStep;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
hasError?: boolean;
|
error?: InstallScreenBackupError;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -132,7 +133,7 @@ type UpdateBackupImportProgressActionType = ReadonlyDeep<{
|
||||||
totalBytes: number;
|
totalBytes: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
hasError: boolean;
|
error: InstallScreenBackupError;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -600,10 +601,10 @@ export function reducer(
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('hasError' in action.payload) {
|
if ('error' in action.payload) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
hasError: action.payload.hasError,
|
error: action.payload.error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,7 +627,7 @@ export function reducer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
hasError: false,
|
error: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ export type UpdatesStateType = ReadonlyDeep<{
|
||||||
downloadSize?: number;
|
downloadSize?: number;
|
||||||
downloadedSize?: number;
|
downloadedSize?: number;
|
||||||
showEventsCount: number;
|
showEventsCount: number;
|
||||||
|
isCheckingForUpdates: boolean;
|
||||||
version?: string;
|
version?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -27,6 +28,8 @@ const DISMISS_DIALOG = 'updates/DISMISS_DIALOG';
|
||||||
const SHOW_UPDATE_DIALOG = 'updates/SHOW_UPDATE_DIALOG';
|
const SHOW_UPDATE_DIALOG = 'updates/SHOW_UPDATE_DIALOG';
|
||||||
const SNOOZE_UPDATE = 'updates/SNOOZE_UPDATE';
|
const SNOOZE_UPDATE = 'updates/SNOOZE_UPDATE';
|
||||||
const START_UPDATE = 'updates/START_UPDATE';
|
const START_UPDATE = 'updates/START_UPDATE';
|
||||||
|
const CHECK_FOR_UPDATES = 'updates/CHECK_FOR_UPDATES';
|
||||||
|
const CHECK_FOR_UPDATES_FINISHED = 'updates/CHECK_FOR_UPDATES_FINISHED';
|
||||||
const UNSNOOZE_UPDATE = 'updates/UNSNOOZE_UPDATE';
|
const UNSNOOZE_UPDATE = 'updates/UNSNOOZE_UPDATE';
|
||||||
|
|
||||||
export type UpdateDialogOptionsType = ReadonlyDeep<{
|
export type UpdateDialogOptionsType = ReadonlyDeep<{
|
||||||
|
@ -55,6 +58,14 @@ type StartUpdateActionType = ReadonlyDeep<{
|
||||||
type: typeof START_UPDATE;
|
type: typeof START_UPDATE;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type CheckForUpdatesActionType = ReadonlyDeep<{
|
||||||
|
type: typeof CHECK_FOR_UPDATES;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type CheckForUpdatesFinishedActionType = ReadonlyDeep<{
|
||||||
|
type: typeof CHECK_FOR_UPDATES_FINISHED;
|
||||||
|
}>;
|
||||||
|
|
||||||
type UnsnoozeUpdateActionType = ReadonlyDeep<{
|
type UnsnoozeUpdateActionType = ReadonlyDeep<{
|
||||||
type: typeof UNSNOOZE_UPDATE;
|
type: typeof UNSNOOZE_UPDATE;
|
||||||
payload: DialogType;
|
payload: DialogType;
|
||||||
|
@ -65,6 +76,8 @@ export type UpdatesActionType = ReadonlyDeep<
|
||||||
| ShowUpdateDialogActionType
|
| ShowUpdateDialogActionType
|
||||||
| SnoozeUpdateActionType
|
| SnoozeUpdateActionType
|
||||||
| StartUpdateActionType
|
| StartUpdateActionType
|
||||||
|
| CheckForUpdatesActionType
|
||||||
|
| CheckForUpdatesFinishedActionType
|
||||||
| UnsnoozeUpdateActionType
|
| UnsnoozeUpdateActionType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -135,11 +148,43 @@ function startUpdate(): ThunkAction<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function forceUpdate(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
| CheckForUpdatesActionType
|
||||||
|
| CheckForUpdatesFinishedActionType
|
||||||
|
| ShowUpdateDialogActionType
|
||||||
|
> {
|
||||||
|
return async dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: CHECK_FOR_UPDATES,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateIpc.forceUpdate();
|
||||||
|
} catch {
|
||||||
|
dispatch({
|
||||||
|
type: SHOW_UPDATE_DIALOG,
|
||||||
|
payload: {
|
||||||
|
dialogType: DialogType.Cannot_Update,
|
||||||
|
otherState: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
dispatch({
|
||||||
|
type: CHECK_FOR_UPDATES_FINISHED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
dismissDialog,
|
dismissDialog,
|
||||||
showUpdateDialog,
|
showUpdateDialog,
|
||||||
snoozeUpdate,
|
snoozeUpdate,
|
||||||
startUpdate,
|
startUpdate,
|
||||||
|
forceUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdatesActions = (): BoundActionCreatorsMapObject<
|
export const useUpdatesActions = (): BoundActionCreatorsMapObject<
|
||||||
|
@ -152,6 +197,7 @@ export function getEmptyState(): UpdatesStateType {
|
||||||
return {
|
return {
|
||||||
dialogType: DialogType.None,
|
dialogType: DialogType.None,
|
||||||
didSnooze: false,
|
didSnooze: false,
|
||||||
|
isCheckingForUpdates: false,
|
||||||
showEventsCount: 0,
|
showEventsCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -187,6 +233,22 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === CHECK_FOR_UPDATES) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dialogType: DialogType.None,
|
||||||
|
didSnooze: false,
|
||||||
|
isCheckingForUpdates: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === CHECK_FOR_UPDATES_FINISHED) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isCheckingForUpdates: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
action.type === DISMISS_DIALOG &&
|
action.type === DISMISS_DIALOG &&
|
||||||
state.dialogType === DialogType.MacOS_Read_Only
|
state.dialogType === DialogType.MacOS_Read_Only
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
const { openInbox } = useAppActions();
|
const { openInbox } = useAppActions();
|
||||||
const { startInstaller, finishInstall, retryBackupImport } =
|
const { startInstaller, finishInstall, retryBackupImport } =
|
||||||
useInstallerActions();
|
useInstallerActions();
|
||||||
const { startUpdate } = useUpdatesActions();
|
const { startUpdate, forceUpdate } = useUpdatesActions();
|
||||||
const hasExpired = useSelector(hasExpiredSelector);
|
const hasExpired = useSelector(hasExpiredSelector);
|
||||||
|
|
||||||
const [deviceName, setDeviceName] = useState<string>('');
|
const [deviceName, setDeviceName] = useState<string>('');
|
||||||
|
@ -80,6 +80,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
updates,
|
updates,
|
||||||
currentVersion: window.getVersion(),
|
currentVersion: window.getVersion(),
|
||||||
startUpdate,
|
startUpdate,
|
||||||
|
forceUpdate,
|
||||||
retryGetQrCode: startInstaller,
|
retryGetQrCode: startInstaller,
|
||||||
OS: OS.getName(),
|
OS: OS.getName(),
|
||||||
isStaging: isStagingServer(),
|
isStaging: isStagingServer(),
|
||||||
|
@ -112,9 +113,15 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
backupStep: installerState.backupStep,
|
backupStep: installerState.backupStep,
|
||||||
currentBytes: installerState.currentBytes,
|
currentBytes: installerState.currentBytes,
|
||||||
totalBytes: installerState.totalBytes,
|
totalBytes: installerState.totalBytes,
|
||||||
hasError: installerState.hasError,
|
error: installerState.error,
|
||||||
onCancel: onCancelBackupImport,
|
onCancel: onCancelBackupImport,
|
||||||
onRetry: retryBackupImport,
|
onRetry: retryBackupImport,
|
||||||
|
|
||||||
|
updates,
|
||||||
|
currentVersion: window.getVersion(),
|
||||||
|
forceUpdate,
|
||||||
|
startUpdate,
|
||||||
|
OS: OS.getName(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -17,6 +17,11 @@ export enum InstallScreenBackupStep {
|
||||||
Process = 'Process',
|
Process = 'Process',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum InstallScreenBackupError {
|
||||||
|
Unknown = 'Unknown',
|
||||||
|
UnsupportedVersion = 'UnsupportedVersion',
|
||||||
|
}
|
||||||
|
|
||||||
export enum InstallScreenError {
|
export enum InstallScreenError {
|
||||||
TooManyDevices = 'TooManyDevices',
|
TooManyDevices = 'TooManyDevices',
|
||||||
TooOld = 'TooOld',
|
TooOld = 'TooOld',
|
||||||
|
|
|
@ -155,6 +155,8 @@ export abstract class Updater {
|
||||||
},
|
},
|
||||||
50
|
50
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('updater/force-update', () => this.force());
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
Loading…
Reference in a new issue