Backup import cancel UI
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
7700953777
commit
d2c02b1246
9 changed files with 183 additions and 25 deletions
|
@ -4687,10 +4687,34 @@
|
||||||
"messageformat": "Downloading {currentSize} of {totalSize} ({fractionComplete, number, percent})...",
|
"messageformat": "Downloading {currentSize} of {totalSize} ({fractionComplete, number, percent})...",
|
||||||
"description": "Hint under the progressbar in the backup import screen"
|
"description": "Hint under the progressbar in the backup import screen"
|
||||||
},
|
},
|
||||||
|
"icu:BackupImportScreen__progressbar-hint--preparing": {
|
||||||
|
"messageformat": "Preparing to download...",
|
||||||
|
"description": "Hint under the progressbar in the backup import screen when download size is not yet known"
|
||||||
|
},
|
||||||
"icu:BackupImportScreen__description": {
|
"icu:BackupImportScreen__description": {
|
||||||
"messageformat": "This may take a few minutes depending on the size of your backup",
|
"messageformat": "This may take a few minutes depending on the size of your backup",
|
||||||
"description": "Description at the bottom of backup import screen"
|
"description": "Description at the bottom of backup import screen"
|
||||||
},
|
},
|
||||||
|
"icu:BackupImportScreen__cancel": {
|
||||||
|
"messageformat": "Cancel transfer",
|
||||||
|
"description": "Text of the cancel button at the bottom of backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__cancel-confirmation__title": {
|
||||||
|
"messageformat": "Cancel transfer?",
|
||||||
|
"description": "Title of the cancel confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__cancel-confirmation__body": {
|
||||||
|
"messageformat": "Your messages and media have not completed restoring. If you choose to cancel, you can transfer again from Settings.",
|
||||||
|
"description": "Body of the cancel confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__cancel-confirmation__cancel": {
|
||||||
|
"messageformat": "Continue transfer",
|
||||||
|
"description": "Text of the continue button of the cancel confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__cancel-confirmation__confirm": {
|
||||||
|
"messageformat": "Cancel transfer",
|
||||||
|
"description": "Text of the confirmation button of the cancel confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
"icu:BackupMediaDownloadProgress__title": {
|
"icu:BackupMediaDownloadProgress__title": {
|
||||||
"messageformat": "Restoring media",
|
"messageformat": "Restoring media",
|
||||||
"description": "Label above a progress bar showing media (attachment) download progress after restoring from backup"
|
"description": "Label above a progress bar showing media (attachment) download progress after restoring from backup"
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
// Copyright 2024 Signal Messenger, LLC
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
.BackupImportScreen {
|
.InstallScreenBackupImportStep {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
|
@ -10,20 +12,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.BackupImportScreen__content {
|
.InstallScreenBackupImportStep__content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.BackupImportScreen__title {
|
.InstallScreenBackupImportStep__title {
|
||||||
@include font-title-2;
|
@include font-title-2;
|
||||||
margin-block: 0 20px;
|
margin-block: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.BackupImportScreen .ProgressBar {
|
.InstallScreenBackupImportStep .ProgressBar {
|
||||||
margin-block-end: 14px;
|
margin-block-end: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.BackupImportScreen__progressbar-hint {
|
.InstallScreenBackupImportStep__progressbar-hint {
|
||||||
@include font-caption;
|
@include font-caption;
|
||||||
margin-block-end: 22px;
|
margin-block-end: 22px;
|
||||||
|
|
||||||
|
@ -36,11 +38,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.BackupImportScreen__progressbar-hint--hidden {
|
.InstallScreenBackupImportStep__description {
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.BackupImportScreen__description {
|
|
||||||
@include font-body-1;
|
@include font-body-1;
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
|
@ -51,3 +49,20 @@
|
||||||
color: $color-gray-25;
|
color: $color-gray-25;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.InstallScreenBackupImportStep__cancel {
|
||||||
|
@include button-reset();
|
||||||
|
@include button-focus-outline;
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 48px;
|
||||||
|
|
||||||
|
@include light-theme() {
|
||||||
|
color: $color-ultramarine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme() {
|
||||||
|
color: $color-ultramarine-light;
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,6 @@
|
||||||
@import './components/AvatarModalButtons.scss';
|
@import './components/AvatarModalButtons.scss';
|
||||||
@import './components/AvatarPreview.scss';
|
@import './components/AvatarPreview.scss';
|
||||||
@import './components/AvatarTextEditor.scss';
|
@import './components/AvatarTextEditor.scss';
|
||||||
@import './components/BackupImportScreen.scss';
|
|
||||||
@import './components/BackupMediaDownloadProgress.scss';
|
@import './components/BackupMediaDownloadProgress.scss';
|
||||||
@import './components/BadgeCarouselIndex.scss';
|
@import './components/BadgeCarouselIndex.scss';
|
||||||
@import './components/BadgeDialog.scss';
|
@import './components/BadgeDialog.scss';
|
||||||
|
@ -106,6 +105,7 @@
|
||||||
@import './components/Inbox.scss';
|
@import './components/Inbox.scss';
|
||||||
@import './components/IncomingCallBar.scss';
|
@import './components/IncomingCallBar.scss';
|
||||||
@import './components/Input.scss';
|
@import './components/Input.scss';
|
||||||
|
@import './components/InstallScreenBackupImportStep.scss';
|
||||||
@import './components/InstallScreenChoosingDeviceNameStep.scss';
|
@import './components/InstallScreenChoosingDeviceNameStep.scss';
|
||||||
@import './components/InstallScreenErrorStep.scss';
|
@import './components/InstallScreenErrorStep.scss';
|
||||||
@import './components/InstallScreenLinkInProgressStep.scss';
|
@import './components/InstallScreenLinkInProgressStep.scss';
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { PropsType } from './InstallScreenBackupImportStep';
|
import type { PropsType } from './InstallScreenBackupImportStep';
|
||||||
|
@ -16,7 +17,11 @@ export default {
|
||||||
|
|
||||||
// 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 {...args} i18n={i18n} />
|
<InstallScreenBackupImportStep
|
||||||
|
{...args}
|
||||||
|
i18n={i18n}
|
||||||
|
onCancel={action('onCancel')}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NoBytes = Template.bind({});
|
export const NoBytes = Template.bind({});
|
||||||
|
@ -27,6 +32,12 @@ NoBytes.args = {
|
||||||
|
|
||||||
export const Bytes = Template.bind({});
|
export const Bytes = Template.bind({});
|
||||||
Bytes.args = {
|
Bytes.args = {
|
||||||
currentBytes: 500,
|
currentBytes: 500 * 1024,
|
||||||
|
totalBytes: 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Full = Template.bind({});
|
||||||
|
Full.args = {
|
||||||
|
currentBytes: 1024,
|
||||||
totalBytes: 1024,
|
totalBytes: 1024,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
// 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 { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
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';
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -16,16 +17,36 @@ export type PropsType = Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
|
onCancel: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function InstallScreenBackupImportStep({
|
export function InstallScreenBackupImportStep({
|
||||||
i18n,
|
i18n,
|
||||||
currentBytes,
|
currentBytes,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
|
onCancel,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
|
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
|
||||||
|
|
||||||
|
const confirmCancel = useCallback(() => {
|
||||||
|
setIsConfirmingCancel(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const abortCancel = useCallback(() => {
|
||||||
|
setIsConfirmingCancel(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCancelWrap = useCallback(() => {
|
||||||
|
onCancel();
|
||||||
|
setIsConfirmingCancel(false);
|
||||||
|
}, [onCancel]);
|
||||||
|
|
||||||
let percentage = 0;
|
let percentage = 0;
|
||||||
let progress: JSX.Element;
|
let progress: JSX.Element;
|
||||||
|
let isCancelPossible = true;
|
||||||
if (currentBytes != null && totalBytes != null) {
|
if (currentBytes != null && totalBytes != null) {
|
||||||
|
isCancelPossible = currentBytes !== totalBytes;
|
||||||
|
|
||||||
percentage = Math.max(0, Math.min(1, currentBytes / totalBytes));
|
percentage = Math.max(0, Math.min(1, currentBytes / totalBytes));
|
||||||
if (percentage > 0 && percentage <= 0.01) {
|
if (percentage > 0 && percentage <= 0.01) {
|
||||||
percentage = 0.01;
|
percentage = 0.01;
|
||||||
|
@ -38,7 +59,7 @@ export function InstallScreenBackupImportStep({
|
||||||
progress = (
|
progress = (
|
||||||
<>
|
<>
|
||||||
<ProgressBar fractionComplete={percentage} />
|
<ProgressBar fractionComplete={percentage} />
|
||||||
<div className="BackupImportScreen__progressbar-hint">
|
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
||||||
{i18n('icu:BackupImportScreen__progressbar-hint', {
|
{i18n('icu:BackupImportScreen__progressbar-hint', {
|
||||||
currentSize: formatFileSize(currentBytes),
|
currentSize: formatFileSize(currentBytes),
|
||||||
totalSize: formatFileSize(totalBytes),
|
totalSize: formatFileSize(totalBytes),
|
||||||
|
@ -51,31 +72,60 @@ export function InstallScreenBackupImportStep({
|
||||||
progress = (
|
progress = (
|
||||||
<>
|
<>
|
||||||
<ProgressBar fractionComplete={0} />
|
<ProgressBar fractionComplete={0} />
|
||||||
<div className="BackupImportScreen__progressbar-hint BackupImportScreen__progressbar-hint--hidden">
|
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
||||||
{i18n('icu:BackupImportScreen__progressbar-hint', {
|
{i18n('icu:BackupImportScreen__progressbar-hint--preparing')}
|
||||||
currentSize: '',
|
|
||||||
totalSize: '',
|
|
||||||
fractionComplete: 0,
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="BackupImportScreen">
|
<div className="InstallScreenBackupImportStep">
|
||||||
<TitlebarDragArea />
|
<TitlebarDragArea />
|
||||||
|
|
||||||
<InstallScreenSignalLogo />
|
<InstallScreenSignalLogo />
|
||||||
|
|
||||||
<div className="BackupImportScreen__content">
|
<div className="InstallScreenBackupImportStep__content">
|
||||||
<h3 className="BackupImportScreen__title">
|
<h3 className="InstallScreenBackupImportStep__title">
|
||||||
{i18n('icu:BackupImportScreen__title')}
|
{i18n('icu:BackupImportScreen__title')}
|
||||||
</h3>
|
</h3>
|
||||||
{progress}
|
{progress}
|
||||||
<div className="BackupImportScreen__description">
|
<div className="InstallScreenBackupImportStep__description">
|
||||||
{i18n('icu:BackupImportScreen__description')}
|
{i18n('icu:BackupImportScreen__description')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isCancelPossible && (
|
||||||
|
<button
|
||||||
|
className="InstallScreenBackupImportStep__cancel"
|
||||||
|
type="button"
|
||||||
|
onClick={confirmCancel}
|
||||||
|
>
|
||||||
|
{i18n('icu:BackupImportScreen__cancel')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isConfirmingCancel && (
|
||||||
|
<ConfirmationDialog
|
||||||
|
dialogName="InstallScreenBackupImportStep.confirmCancel"
|
||||||
|
title={i18n('icu:BackupImportScreen__cancel-confirmation__title')}
|
||||||
|
cancelText={i18n(
|
||||||
|
'icu:BackupImportScreen__cancel-confirmation__cancel'
|
||||||
|
)}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: onCancelWrap,
|
||||||
|
style: 'negative',
|
||||||
|
text: i18n(
|
||||||
|
'icu:BackupImportScreen__cancel-confirmation__confirm'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={abortCancel}
|
||||||
|
>
|
||||||
|
{i18n('icu:BackupImportScreen__cancel-confirmation__body')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { uploadFile } from '../../util/uploadAttachment';
|
||||||
export type DownloadOptionsType = Readonly<{
|
export type DownloadOptionsType = Readonly<{
|
||||||
downloadOffset: number;
|
downloadOffset: number;
|
||||||
onProgress: (currentBytes: number, totalBytes: number) => void;
|
onProgress: (currentBytes: number, totalBytes: number) => void;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class BackupAPI {
|
export class BackupAPI {
|
||||||
|
@ -75,6 +76,7 @@ export class BackupAPI {
|
||||||
public async download({
|
public async download({
|
||||||
downloadOffset,
|
downloadOffset,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
abortSignal,
|
||||||
}: DownloadOptionsType): Promise<Readable> {
|
}: DownloadOptionsType): Promise<Readable> {
|
||||||
const { cdn, backupDir, backupName } = await this.getInfo();
|
const { cdn, backupDir, backupName } = await this.getInfo();
|
||||||
const { headers } = await this.credentials.getCDNReadCredentials(cdn);
|
const { headers } = await this.credentials.getCDNReadCredentials(cdn);
|
||||||
|
@ -86,6 +88,7 @@ export class BackupAPI {
|
||||||
headers,
|
headers,
|
||||||
downloadOffset,
|
downloadOffset,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
abortSignal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ export enum BackupType {
|
||||||
export class BackupsService {
|
export class BackupsService {
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
|
private downloadController: AbortController | undefined;
|
||||||
|
|
||||||
public readonly credentials = new BackupCredentials();
|
public readonly credentials = new BackupCredentials();
|
||||||
public readonly api = new BackupAPI(this.credentials);
|
public readonly api = new BackupAPI(this.credentials);
|
||||||
|
@ -145,10 +146,26 @@ export class BackupsService {
|
||||||
return backupsService.importBackup(() => createReadStream(backupFile));
|
return backupsService.importBackup(() => createReadStream(backupFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public cancelDownload(): void {
|
||||||
|
if (this.downloadController) {
|
||||||
|
log.warn('importBackup: canceling download');
|
||||||
|
this.downloadController.abort();
|
||||||
|
this.downloadController = undefined;
|
||||||
|
} else {
|
||||||
|
log.error('importBackup: not canceling download, not running');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async download(
|
public async download(
|
||||||
downloadPath: string,
|
downloadPath: string,
|
||||||
{ onProgress }: Omit<DownloadOptionsType, 'downloadOffset'>
|
{ onProgress }: Omit<DownloadOptionsType, 'downloadOffset'>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// Abort previous download
|
||||||
|
this.downloadController?.abort();
|
||||||
|
this.downloadController = controller;
|
||||||
|
|
||||||
let downloadOffset = 0;
|
let downloadOffset = 0;
|
||||||
try {
|
try {
|
||||||
({ size: downloadOffset } = await stat(downloadPath));
|
({ size: downloadOffset } = await stat(downloadPath));
|
||||||
|
@ -163,11 +180,20 @@ export class BackupsService {
|
||||||
try {
|
try {
|
||||||
await ensureFile(downloadPath);
|
await ensureFile(downloadPath);
|
||||||
|
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const stream = await this.api.download({
|
const stream = await this.api.download({
|
||||||
downloadOffset,
|
downloadOffset,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
abortSignal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await pipeline(
|
await pipeline(
|
||||||
stream,
|
stream,
|
||||||
createWriteStream(downloadPath, {
|
createWriteStream(downloadPath, {
|
||||||
|
@ -176,12 +202,24 @@ export class BackupsService {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadController = undefined;
|
||||||
|
|
||||||
|
// Too late to cancel now
|
||||||
try {
|
try {
|
||||||
await this.importFromDisk(downloadPath);
|
await this.importFromDisk(downloadPath);
|
||||||
} finally {
|
} finally {
|
||||||
await unlink(downloadPath);
|
await unlink(downloadPath);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Download canceled
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// No backup on the server
|
// No backup on the server
|
||||||
if (error instanceof HTTPError && error.code === 404) {
|
if (error instanceof HTTPError && error.code === 404) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -8,10 +8,12 @@ import { useSelector } from 'react-redux';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getUpdatesState } from '../selectors/updates';
|
import { getUpdatesState } from '../selectors/updates';
|
||||||
import { getInstallerState } from '../selectors/installer';
|
import { getInstallerState } from '../selectors/installer';
|
||||||
|
import { useAppActions } from '../ducks/app';
|
||||||
import { useInstallerActions } from '../ducks/installer';
|
import { useInstallerActions } from '../ducks/installer';
|
||||||
import { useUpdatesActions } from '../ducks/updates';
|
import { useUpdatesActions } from '../ducks/updates';
|
||||||
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
|
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { backupsService } from '../../services/backups';
|
||||||
import { InstallScreen } from '../../components/InstallScreen';
|
import { InstallScreen } from '../../components/InstallScreen';
|
||||||
import { WidthBreakpoint } from '../../components/_util';
|
import { WidthBreakpoint } from '../../components/_util';
|
||||||
import { InstallScreenStep } from '../../types/InstallScreen';
|
import { InstallScreenStep } from '../../types/InstallScreen';
|
||||||
|
@ -27,6 +29,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
const i18n = useSelector(getIntl);
|
const i18n = useSelector(getIntl);
|
||||||
const installerState = useSelector(getInstallerState);
|
const installerState = useSelector(getInstallerState);
|
||||||
const updates = useSelector(getUpdatesState);
|
const updates = useSelector(getUpdatesState);
|
||||||
|
const { openInbox } = useAppActions();
|
||||||
const { startInstaller, finishInstall } = useInstallerActions();
|
const { startInstaller, finishInstall } = useInstallerActions();
|
||||||
const { startUpdate } = useUpdatesActions();
|
const { startUpdate } = useUpdatesActions();
|
||||||
const hasExpired = useSelector(hasExpiredSelector);
|
const hasExpired = useSelector(hasExpiredSelector);
|
||||||
|
@ -43,6 +46,13 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
}
|
}
|
||||||
}, [backupFile, deviceName, finishInstall]);
|
}, [backupFile, deviceName, finishInstall]);
|
||||||
|
|
||||||
|
const onCancelBackupImport = useCallback((): void => {
|
||||||
|
backupsService.cancelDownload();
|
||||||
|
if (installerState.step === InstallScreenStep.BackupImport) {
|
||||||
|
openInbox();
|
||||||
|
}
|
||||||
|
}, [installerState.step, openInbox]);
|
||||||
|
|
||||||
const suggestedDeviceName =
|
const suggestedDeviceName =
|
||||||
installerState.step === InstallScreenStep.ChoosingDeviceName
|
installerState.step === InstallScreenStep.ChoosingDeviceName
|
||||||
? installerState.deviceName
|
? installerState.deviceName
|
||||||
|
@ -100,6 +110,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
i18n,
|
i18n,
|
||||||
currentBytes: installerState.currentBytes,
|
currentBytes: installerState.currentBytes,
|
||||||
totalBytes: installerState.totalBytes,
|
totalBytes: installerState.totalBytes,
|
||||||
|
onCancel: onCancelBackupImport,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1175,6 +1175,7 @@ export type GetBackupStreamOptionsType = Readonly<{
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
downloadOffset: number;
|
downloadOffset: number;
|
||||||
onProgress: (currentBytes: number, totalBytes: number) => void;
|
onProgress: (currentBytes: number, totalBytes: number) => void;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const getBackupInfoResponseSchema = z.object({
|
export const getBackupInfoResponseSchema = z.object({
|
||||||
|
@ -2815,6 +2816,7 @@ export function initialize({
|
||||||
backupName,
|
backupName,
|
||||||
downloadOffset,
|
downloadOffset,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
abortSignal,
|
||||||
}: GetBackupStreamOptionsType): Promise<Readable> {
|
}: GetBackupStreamOptionsType): Promise<Readable> {
|
||||||
return _getAttachment({
|
return _getAttachment({
|
||||||
cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`,
|
cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`,
|
||||||
|
@ -2824,6 +2826,7 @@ export function initialize({
|
||||||
options: {
|
options: {
|
||||||
downloadOffset,
|
downloadOffset,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
abortSignal,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3550,6 +3553,7 @@ export function initialize({
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
downloadOffset?: number;
|
downloadOffset?: number;
|
||||||
onProgress?: (currentBytes: number, totalBytes: number) => void;
|
onProgress?: (currentBytes: number, totalBytes: number) => void;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
};
|
};
|
||||||
}): Promise<Readable> {
|
}): Promise<Readable> {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
@ -3561,6 +3565,8 @@ export function initialize({
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
options?.abortSignal?.addEventListener('abort', cancelRequest);
|
||||||
|
|
||||||
registerInflightRequest(cancelRequest);
|
registerInflightRequest(cancelRequest);
|
||||||
|
|
||||||
let totalBytes = 0;
|
let totalBytes = 0;
|
||||||
|
|
Loading…
Reference in a new issue