Add backup attachment restore idle state

This commit is contained in:
trevor-signal 2024-10-24 16:21:02 -04:00 committed by GitHub
parent 56ccd02232
commit 8601baa7f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 122 additions and 17 deletions

View file

@ -4787,6 +4787,14 @@
"messageformat": "Restore paused",
"description": "Label indicating media (attachment) download progress has been paused (due to user interaction)"
},
"icu:BackupMediaDownloadProgress__title-idle": {
"messageformat": "{currentSize} of {totalSize} restored",
"description": "Label indicating media (attachment) download progress"
},
"icu:BackupMediaDownloadProgress__description-idle": {
"messageformat": "Media will continue to download in the background",
"description": "Description text when attachment download is idle, and will continue in the background"
},
"icu:BackupMediaDownloadProgress__button-pause": {
"messageformat": "Pause transfer",
"description": "Text for button to pause media (attachment) download after backup impor"

View file

@ -4,7 +4,6 @@
.BackupMediaDownloadProgress {
border-radius: 10px;
display: flex;
align-items: center;
gap: 10px;
padding: 11px;
padding-inline-end: 16px;
@ -26,6 +25,10 @@
}
}
.BackupMediaDownloadProgress__icon {
margin-top: 6px;
}
.BackupMediaDownloadProgress__icon--complete {
&::after {
content: '';
@ -46,6 +49,27 @@
}
}
}
.BackupMediaDownloadProgress__icon--idle {
&::after {
content: '';
display: block;
width: 24px;
height: 24px;
@include light-theme {
@include color-svg(
'../images/icons/v3/backup/backup-bold.svg',
$color-ultramarine
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v3/backup/backup-bold.svg',
$color-ultramarine-light
);
}
}
}
button.BackupMediaDownloadProgress__button {
@include button-reset;
@include font-subtitle-bold;
@ -86,7 +110,7 @@ button.BackupMediaDownloadProgress__button-close {
width: 20px;
height: 20px;
@include light-theme {
@include color-svg('../images/icons/v3/x/x.svg', $color-gray-75);
@include color-svg('../images/icons/v3/x/x.svg', $color-gray-45);
}
@include dark-theme {
@include color-svg('../images/icons/v3/x/x.svg', $color-gray-20);
@ -102,7 +126,7 @@ button.BackupMediaDownloadProgress__button-close {
min-height: 36px;
}
.BackupMediaDownloadProgress__progressbar-hint {
.BackupMediaDownloadProgress__description {
@include font-subtitle;
@include light-theme {

View file

@ -45,6 +45,14 @@ export function Paused(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} isPaused />;
}
export function Idle(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} isIdle />;
}
export function PausedAndIdle(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} isPaused isIdle />;
}
export function Complete(args: PropsType): JSX.Element {
return (
<BackupMediaDownloadProgress {...args} downloadedBytes={args.totalBytes} />

View file

@ -14,6 +14,7 @@ export type PropsType = Readonly<{
i18n: LocalizerType;
downloadedBytes: number;
totalBytes: number;
isIdle: boolean;
isPaused: boolean;
handleCancel: VoidFunction;
handleClose: VoidFunction;
@ -25,6 +26,7 @@ export function BackupMediaDownloadProgress({
i18n,
downloadedBytes,
totalBytes,
isIdle,
isPaused,
handleCancel: handleConfirmedCancel,
handleClose,
@ -45,31 +47,57 @@ export function BackupMediaDownloadProgress({
downloadedBytes / totalBytes
);
const closeButton = (
<button
type="button"
onClick={handleClose}
className="BackupMediaDownloadProgress__button-close"
aria-label={i18n('icu:close')}
/>
);
let content: JSX.Element | undefined;
let icon: JSX.Element | undefined;
let actionButton: JSX.Element | undefined;
if (fractionComplete === 1) {
icon = <div className="BackupMediaDownloadProgress__icon--complete" />;
icon = (
<div className="BackupMediaDownloadProgress__icon BackupMediaDownloadProgress__icon--complete" />
);
content = (
<>
<div className="BackupMediaDownloadProgress__title">
{i18n('icu:BackupMediaDownloadProgress__title-complete')}
</div>
<div className="BackupMediaDownloadProgress__progressbar-hint">
<div className="BackupMediaDownloadProgress__description">
{formatFileSize(downloadedBytes)}
</div>
</>
);
actionButton = (
<button
type="button"
onClick={handleClose}
className="BackupMediaDownloadProgress__button-close"
aria-label={i18n('icu:close')}
/>
actionButton = closeButton;
} else if (isIdle && !isPaused) {
icon = (
<div className="BackupMediaDownloadProgress__icon BackupMediaDownloadProgress__icon--idle" />
);
content = (
<>
<div className="BackupMediaDownloadProgress__title">
{i18n('icu:BackupMediaDownloadProgress__title-idle', {
currentSize: formatFileSize(downloadedBytes),
totalSize: formatFileSize(totalBytes),
})}
</div>
<div className="BackupMediaDownloadProgress__description">
{i18n('icu:BackupMediaDownloadProgress__description-idle')}
</div>
</>
);
actionButton = closeButton;
} else {
icon = <ProgressCircle fractionComplete={fractionComplete} />;
icon = (
<div className="BackupMediaDownloadProgress__icon">
<ProgressCircle fractionComplete={fractionComplete} />
</div>
);
if (isPaused) {
content = (
@ -94,7 +122,7 @@ export function BackupMediaDownloadProgress({
{i18n('icu:BackupMediaDownloadProgress__title-in-progress')}
</div>
<div className="BackupMediaDownloadProgress__progressbar-hint">
<div className="BackupMediaDownloadProgress__description">
{i18n('icu:BackupMediaDownloadProgress__progressbar-hint', {
currentSize: formatFileSize(downloadedBytes),
totalSize: formatFileSize(totalBytes),

View file

@ -139,6 +139,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
},
backupMediaDownloadProgress: {
downloadBannerDismissed: false,
isIdle: false,
isPaused: false,
totalBytes: 0,
downloadedBytes: 0,

View file

@ -62,6 +62,7 @@ export type PropsType = {
backupMediaDownloadProgress: {
totalBytes: number;
downloadedBytes: number;
isIdle: boolean;
isPaused: boolean;
downloadBannerDismissed: boolean;
};

View file

@ -22,6 +22,7 @@ import {
AttachmentSizeError,
type AttachmentType,
AttachmentVariant,
mightBeOnBackupTier,
} from '../types/Attachment';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import {
@ -185,7 +186,14 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
size: attachment.size,
ciphertextSize: getAttachmentCiphertextLength(attachment.size),
attachment,
source,
// If the attachment does not have a backupLocator, we don't want to store it as a
// "backup import" attachment, since it's really just a normal attachment that we'll
// try to download from the transit tier (or it's an invalid attachment, etc.). We
// may need to extend the attachment_downloads table in the future to better
// differentiate source vs. location.
source: mightBeOnBackupTier(attachment)
? source
: AttachmentDownloadSource.STANDARD,
});
if (!parseResult.success) {
@ -237,6 +245,13 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
messageIds
);
}
static async waitForIdle(callback?: VoidFunction): Promise<void> {
await AttachmentDownloadManager.instance.waitForIdle();
if (callback) {
callback();
}
}
}
type DependenciesType = {
@ -284,7 +299,7 @@ async function runDownloadAttachmentJob({
};
}
if (job.attachment.backupLocator?.mediaName) {
if (mightBeOnBackupTier(job.attachment)) {
const currentDownloadedSize =
window.storage.get('backupMediaDownloadCompletedBytes') ?? 0;
drop(

View file

@ -88,6 +88,7 @@ export abstract class JobManager<CoreJobType> {
this.enabled = true;
await this.params.markAllJobsInactive();
await this.maybeStartJobs();
this.tick();
}
@ -241,7 +242,7 @@ export abstract class JobManager<CoreJobType> {
timestamp: Date.now(),
});
if (nextJobs.length === 0) {
if (nextJobs.length === 0 && this.activeJobs.size === 0) {
if (this.idleCallbacks.length > 0) {
const callbacks = this.idleCallbacks;
this.idleCallbacks = [];

View file

@ -109,6 +109,7 @@ import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
import { loadAllAndReinitializeRedux } from '../allLoaders';
import { resetBackupMediaDownloadProgress } from '../../util/backupMediaDownload';
import { getEnvironment, isTestEnvironment } from '../../environment';
import { drop } from '../../util/drop';
const MAX_CONCURRENCY = 10;
@ -339,6 +340,11 @@ export class BackupImportStream extends Writable {
!isTestEnvironment(getEnvironment())
) {
await AttachmentDownloadManager.start();
drop(
AttachmentDownloadManager.waitForIdle(async () => {
await window.storage.put('backupMediaDownloadIdle', true);
})
);
}
done();

View file

@ -258,10 +258,12 @@ export const getBackupMediaDownloadProgress = createSelector(
downloadedBytes: number;
isPaused: boolean;
downloadBannerDismissed: boolean;
isIdle: boolean;
} => ({
totalBytes: state.backupMediaDownloadTotalBytes ?? 0,
downloadedBytes: state.backupMediaDownloadCompletedBytes ?? 0,
isPaused: state.backupMediaDownloadPaused ?? false,
isIdle: state.backupMediaDownloadIdle ?? false,
downloadBannerDismissed: state.backupMediaDownloadBannerDismissed ?? false,
})
);

View file

@ -409,6 +409,15 @@ describe('AttachmentDownloadManager/JobManager', () => {
idx % 2 === 0
? AttachmentDownloadSource.BACKUP_IMPORT
: AttachmentDownloadSource.STANDARD,
digest: `digestFor${idx}`,
attachment: {
contentType: MIME.IMAGE_JPEG,
size: 128,
digest: `digestFor${idx}`,
backupLocator: {
mediaName: 'medianame',
},
},
}));
// make one of the backup job messages visible to test that code path as well
downloadManager?.updateVisibleTimelineMessages(['message-0', 'message-1']);

View file

@ -147,6 +147,7 @@ export type StorageAccessType = {
backupMediaDownloadCompletedBytes: number;
backupMediaDownloadPaused: boolean;
backupMediaDownloadBannerDismissed: boolean;
backupMediaDownloadIdle: boolean;
setBackupSignatureKey: boolean;
lastReceivedAtCounter: number;
preferredReactionEmoji: ReadonlyArray<string>;

View file

@ -17,6 +17,7 @@ export async function resetBackupMediaDownloadItems(): Promise<void> {
window.storage.remove('backupMediaDownloadCompletedBytes'),
window.storage.remove('backupMediaDownloadBannerDismissed'),
window.storage.remove('backupMediaDownloadPaused'),
window.storage.remove('backupMediaDownloadIdle'),
]);
}