Resumable backup import

This commit is contained in:
Fedor Indutny 2024-08-27 17:00:41 -04:00 committed by GitHub
parent 3d8aaf0a5a
commit 8ef149e3a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 498 additions and 33 deletions

View file

@ -8,14 +8,18 @@ import classNames from 'classnames';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
import type { VerificationTransport } from '../types/VerificationTransport';
import { ThemeType } from '../types/Util';
import { AppViewType } from '../state/ducks/app';
import type { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError';
import { type AppStateType, AppViewType } from '../state/ducks/app';
import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration';
import { BackupImportScreen } from './BackupImportScreen';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { useReducedMotion } from '../hooks/useReducedMotion';
type PropsType = {
appView: AppViewType;
i18n: LocalizerType;
state: AppStateType;
openInbox: () => void;
getCaptchaToken: () => Promise<string>;
registerSingleDevice: (
@ -49,7 +53,8 @@ type PropsType = {
};
export function App({
appView,
i18n,
state,
getCaptchaToken,
hasSelectedStoryData,
isFullScreen,
@ -70,9 +75,9 @@ export function App({
}: PropsType): JSX.Element {
let contents;
if (appView === AppViewType.Installer) {
if (state.appView === AppViewType.Installer) {
contents = <SmartInstallScreen />;
} else if (appView === AppViewType.Standalone) {
} else if (state.appView === AppViewType.Standalone) {
const onComplete = () => {
window.IPC.removeSetupMenuItems();
openInbox();
@ -87,8 +92,14 @@ export function App({
uploadProfile={uploadProfile}
/>
);
} else if (appView === AppViewType.Inbox) {
} else if (state.appView === AppViewType.Inbox) {
contents = renderInbox();
} else if (state.appView === AppViewType.Blank) {
contents = undefined;
} else if (state.appView === AppViewType.BackupImport) {
contents = <BackupImportScreen i18n={i18n} {...state} />;
} else {
throw missingCaseError(state);
}
// This are here so that themes are properly applied to anything that is

View file

@ -0,0 +1,32 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import type { PropsType } from './BackupImportScreen';
import { BackupImportScreen } from './BackupImportScreen';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/BackupImportScreen',
} satisfies Meta<PropsType>;
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn<PropsType> = (args: PropsType) => (
<BackupImportScreen {...args} i18n={i18n} />
);
export const NoBytes = Template.bind({});
NoBytes.args = {
currentBytes: undefined,
totalBytes: undefined,
};
export const Bytes = Template.bind({});
Bytes.args = {
currentBytes: 500,
totalBytes: 1024,
};

View file

@ -0,0 +1,85 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { formatFileSize } from '../util/formatFileSize';
import { TitlebarDragArea } from './TitlebarDragArea';
import { InstallScreenSignalLogo } from './installScreen/InstallScreenSignalLogo';
// We can't always use destructuring assignment because of the complexity of this props
// type.
export type PropsType = Readonly<{
i18n: LocalizerType;
currentBytes?: number;
totalBytes?: number;
}>;
export function BackupImportScreen({
i18n,
currentBytes,
totalBytes,
}: PropsType): JSX.Element {
let percentage = 0;
let progress: JSX.Element;
if (currentBytes != null && totalBytes != null) {
percentage = Math.max(0, Math.min(1, currentBytes / totalBytes));
if (percentage > 0 && percentage <= 0.01) {
percentage = 0.01;
} else if (percentage >= 0.99 && percentage < 1) {
percentage = 0.99;
} else {
percentage = Math.round(percentage * 100) / 100;
}
progress = (
<>
<div className="BackupImportScreen__progressbar">
<div
className="BackupImportScreen__progressbar__fill"
style={{ transform: `translateX(${(percentage - 1) * 100}%)` }}
/>
</div>
<div className="BackupImportScreen__progressbar-hint">
{i18n('icu:BackupImportScreen__progressbar-hint', {
currentSize: formatFileSize(currentBytes),
totalSize: formatFileSize(totalBytes),
fractionComplete: percentage,
})}
</div>
</>
);
} else {
progress = (
<>
<div className="BackupImportScreen__progressbar" />
<div className="BackupImportScreen__progressbar-hint BackupImportScreen__progressbar-hint--hidden">
{i18n('icu:BackupImportScreen__progressbar-hint', {
currentSize: '',
totalSize: '',
fractionComplete: 0,
})}
</div>
</>
);
}
return (
<div className="BackupImportScreen">
<TitlebarDragArea />
<InstallScreenSignalLogo />
<div className="BackupImportScreen__content">
<h3 className="BackupImportScreen__title">
{i18n('icu:BackupImportScreen__title')}
</h3>
{progress}
<div className="BackupImportScreen__description">
{i18n('icu:BackupImportScreen__description')}
</div>
</div>
</div>
);
}