diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 506d58939a..3998bd3c36 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -5348,6 +5348,14 @@ "messageformat": "You must open Signal on your phone to continue using this account. If you do not open Signal on your phone, your account will be deleted soon. Learn more", "description": "Description for modal shown when a user must use their idle primary device to retain their account. Learn more link will link to a support page" }, + "icu:LowDiskSpaceBackupImportModal__title": { + "messageformat": "Can't restore media", + "description": "Title for modal shown when there is insufficient disk space to download media from backup" + }, + "icu:LowDiskSpaceBackupImportModal__description": { + "messageformat": "Your device does not have enough free space. Free up {diskSpaceAmount} of space to restore your media.", + "description": "Description for modal shown when there is insufficient disk space to download media from backup. Example {diskSpaceAmount}: '1.23 GB', '512 MB'" + }, "icu:CompositionArea--expand": { "messageformat": "Expand", "description": "Aria label for expanding composition area" diff --git a/images/icons/v3/backup/backup-warning.svg b/images/icons/v3/backup/backup-warning.svg new file mode 100644 index 0000000000..900df1aa8a --- /dev/null +++ b/images/icons/v3/backup/backup-warning.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/stylesheets/components/LowDiskSpaceBackupImportModal.scss b/stylesheets/components/LowDiskSpaceBackupImportModal.scss new file mode 100644 index 0000000000..b38260ab21 --- /dev/null +++ b/stylesheets/components/LowDiskSpaceBackupImportModal.scss @@ -0,0 +1,52 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +@use '../mixins'; +@use '../variables'; + +.LowDiskSpaceBackupImportModal__content { + max-width: 440px; + padding-block: 12px; + padding-inline: 4px; + display: flex; + flex-direction: column; + align-items: center; +} + +.LowDiskSpaceBackupImportModal__icon { + background-color: variables.$color-accent-yellow; + padding-block: 12px; + padding-inline: 12px; + border-radius: 50%; + &::after { + content: ''; + display: block; + @include mixins.color-svg( + '../images/icons/v3/backup/backup-warning.svg', + variables.$color-black + ); + width: 32px; + height: 32px; + } +} + +.LowDiskSpaceBackupImportModal__header { + @include mixins.font-title-medium; + margin-block: 12px; +} + +.LowDiskSpaceBackupImportModal__description { + margin-block: 12px; + margin-inline: 8px; + color: light-dark(variables.$color-gray-60, variables.$color-gray-25); +} + +.LowDiskSpaceBackupImportModal__button { + display: flex; + justify-content: center; + margin-block-start: 18px; + + button { + padding-inline: 26px; + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 46e195088e..15b0315147 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -125,6 +125,7 @@ @use 'components/Lightbox.scss'; @use 'components/ListTile.scss'; @use 'components/LocalDeleteWarningModal.scss'; +@use 'components/LowDiskSpaceBackupImportModal.scss'; @use 'components/MediaEditor.scss'; @use 'components/MediaPermissionsModal.scss'; @use 'components/MediaQualitySelector.scss'; diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index 5bc65e3d5f..ba19db34bb 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -33,6 +33,7 @@ import { } from './BackfillFailureModal'; import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGifMessageSendModal'; import { CriticalIdlePrimaryDeviceModal } from './CriticalIdlePrimaryDeviceModal'; +import { LowDiskSpaceBackupImportModal } from './LowDiskSpaceBackupImportModal'; // NOTE: All types should be required for this component so that the smart // component gives you type errors when adding/removing props. @@ -151,6 +152,9 @@ export type PropsType = { // CriticalIdlePrimaryDeviceModal, criticalIdlePrimaryDeviceModal: boolean; hideCriticalIdlePrimaryDeviceModal: () => void; + // LowDiskSpaceBackupImportModal + lowDiskSpaceBackupImportModal: { bytesNeeded: number } | null; + hideLowDiskSpaceBackupImportModal: () => void; }; export function GlobalModalContainer({ @@ -250,6 +254,9 @@ export function GlobalModalContainer({ // CriticalIdlePrimaryDeviceModal criticalIdlePrimaryDeviceModal, hideCriticalIdlePrimaryDeviceModal, + // LowDiskSpaceBackupImportModal + lowDiskSpaceBackupImportModal, + hideLowDiskSpaceBackupImportModal, }: PropsType): JSX.Element | null { // We want the following dialogs to show in this order: // 1. Errors @@ -441,5 +448,15 @@ export function GlobalModalContainer({ ); } + if (lowDiskSpaceBackupImportModal) { + return ( + + ); + } + return null; } diff --git a/ts/components/LowDiskSpaceBackupImportModal.stories.tsx b/ts/components/LowDiskSpaceBackupImportModal.stories.tsx new file mode 100644 index 0000000000..b8207f5b80 --- /dev/null +++ b/ts/components/LowDiskSpaceBackupImportModal.stories.tsx @@ -0,0 +1,31 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Meta, StoryFn } from '@storybook/react'; +import React, { type ComponentProps } from 'react'; + +import { action } from '@storybook/addon-actions'; +import { LowDiskSpaceBackupImportModal } from './LowDiskSpaceBackupImportModal'; +import { MEBIBYTE } from '../types/AttachmentSize'; + +const { i18n } = window.SignalContext; + +type PropsType = ComponentProps; + +export default { + title: 'Components/LowDiskSpaceBackupImportModal', + component: LowDiskSpaceBackupImportModal, + args: { + i18n, + onClose: action('close'), + bytesNeeded: 1540 * MEBIBYTE, + }, +} satisfies Meta; + +// eslint-disable-next-line react/function-component-definition +const Template: StoryFn = args => ( + +); + +export const Modal = Template.bind({}); +Modal.args = {}; diff --git a/ts/components/LowDiskSpaceBackupImportModal.tsx b/ts/components/LowDiskSpaceBackupImportModal.tsx new file mode 100644 index 0000000000..d6a6f7f38f --- /dev/null +++ b/ts/components/LowDiskSpaceBackupImportModal.tsx @@ -0,0 +1,48 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as React from 'react'; + +import type { LocalizerType } from '../types/Util'; +import { Modal } from './Modal'; +import { Button, ButtonVariant } from './Button'; +import { formatFileSize } from '../util/formatFileSize'; + +export type PropsType = Readonly<{ + bytesNeeded: number; + i18n: LocalizerType; + onClose: () => void; +}>; + +export function LowDiskSpaceBackupImportModal(props: PropsType): JSX.Element { + const { i18n, bytesNeeded, onClose } = props; + + return ( + + + + + + {i18n('icu:LowDiskSpaceBackupImportModal__title')} + + + + {i18n('icu:LowDiskSpaceBackupImportModal__description', { + diskSpaceAmount: formatFileSize(bytesNeeded), + })} + + + + + {i18n('icu:Confirmation--confirm')} + + + + + ); +} diff --git a/ts/jobs/AttachmentDownloadManager.ts b/ts/jobs/AttachmentDownloadManager.ts index d6b4496092..f3eb5b47dc 100644 --- a/ts/jobs/AttachmentDownloadManager.ts +++ b/ts/jobs/AttachmentDownloadManager.ts @@ -1,6 +1,7 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { noop, omit, throttle } from 'lodash'; +import { statfs } from 'node:fs/promises'; import * as durations from '../util/durations'; import * as log from '../logging/log'; @@ -61,6 +62,7 @@ import { isPermanentlyUndownloadable, isPermanentlyUndownloadableWithoutBackfill, } from './helpers/attachmentBackfill'; +import { formatCountForLogging } from '../logging/formatCountForLogging'; export { isPermanentlyUndownloadable }; @@ -91,6 +93,14 @@ const BACKUP_RETRY_CONFIG = { ...DEFAULT_RETRY_CONFIG, maxAttempts: Infinity, }; + +type RunDownloadAttachmentJobOptions = { + abortSignal: AbortSignal; + isForCurrentlyVisibleMessage: boolean; + maxAttachmentSizeInKib: number; + maxTextAttachmentSizeInKib: number; +}; + type AttachmentDownloadManagerParamsType = Omit< JobManagerParamsType, 'getNextJobs' | 'runJob' @@ -104,12 +114,11 @@ type AttachmentDownloadManagerParamsType = Omit< runDownloadAttachmentJob: (args: { job: AttachmentDownloadJobType; isLastAttempt: boolean; - options: { - abortSignal: AbortSignal; - isForCurrentlyVisibleMessage: boolean; - }; + options: RunDownloadAttachmentJobOptions; dependencies?: DependenciesType; }) => Promise>; + onLowDiskSpaceBackupImport: (bytesNeeded: number) => Promise; + statfs: typeof statfs; }; function getJobId(job: CoreAttachmentDownloadJobType): string { @@ -135,6 +144,13 @@ export class AttachmentDownloadManager extends JobManager Promise; + #statfs: typeof statfs; + #maxAttachmentSizeInKib = getMaximumIncomingAttachmentSizeInKb(getValue); + #maxTextAttachmentSizeInKib = + getMaximumIncomingTextAttachmentSizeInKb(getValue); + + #minimumFreeDiskSpace = this.#maxAttachmentSizeInKib * 5; #attachmentBackfill = new AttachmentBackfill(); @@ -169,6 +185,19 @@ export class AttachmentDownloadManager extends JobManager { + if (!window.storage.get('backupMediaDownloadPaused')) { + await Promise.all([ + window.storage.put('backupMediaDownloadPaused', true), + // Show the banner to allow users to resume from the left pane + window.storage.put('backupMediaDownloadBannerDismissed', false), + ]); + } + window.reduxActions.globalModals.showLowDiskSpaceBackupImportModal( + bytesNeeded + ); + }, + statfs, }; constructor(params: AttachmentDownloadManagerParamsType) { @@ -184,7 +213,7 @@ export class AttachmentDownloadManager extends JobManager { + const { bsize, bavail } = await this.#statfs( + window.SignalContext.getPath('userData') + ); + return bsize * bavail; + } + + async #checkFreeDiskSpaceForBackupImport(): Promise<{ + outOfSpace: boolean; + }> { + let freeDiskSpace: number; + + try { + freeDiskSpace = await this.#getFreeDiskSpace(); + } catch (e) { + log.error( + 'checkFreeDiskSpaceForBackupImport: error checking disk space', + Errors.toLogFormat(e) + ); + // Still attempt the download + return { outOfSpace: false }; + } + + if (freeDiskSpace <= this.#minimumFreeDiskSpace) { + const remainingBackupBytesToDownload = + window.storage.get('backupMediaDownloadTotalBytes', 0) - + window.storage.get('backupMediaDownloadCompletedBytes', 0); + + log.info( + 'AttachmentDownloadManager.checkFreeDiskSpaceForBackupImport: insufficient disk space. ' + + `Available: ${formatCountForLogging(freeDiskSpace)}, ` + + `Needed: ${formatCountForLogging(remainingBackupBytesToDownload)} ` + + `Minimum threshold: ${this.#minimumFreeDiskSpace}` + ); + + await this.#onLowDiskSpaceBackupImport(remainingBackupBytesToDownload); + return { outOfSpace: true }; + } + + return { outOfSpace: false }; + } + static get instance(): AttachmentDownloadManager { if (!AttachmentDownloadManager._instance) { AttachmentDownloadManager._instance = new AttachmentDownloadManager( @@ -345,10 +429,7 @@ async function runDownloadAttachmentJob({ }: { job: AttachmentDownloadJobType; isLastAttempt: boolean; - options: { - abortSignal: AbortSignal; - isForCurrentlyVisibleMessage: boolean; - }; + options: RunDownloadAttachmentJobOptions; dependencies?: DependenciesType; }): Promise> { const jobIdForLogging = getJobIdForLogging(job); @@ -369,6 +450,8 @@ async function runDownloadAttachmentJob({ abortSignal: options.abortSignal, isForCurrentlyVisibleMessage: options?.isForCurrentlyVisibleMessage ?? false, + maxAttachmentSizeInKib: options.maxAttachmentSizeInKib, + maxTextAttachmentSizeInKib: options.maxTextAttachmentSizeInKib, dependencies, }); @@ -491,13 +574,13 @@ export async function runDownloadAttachmentJobInner({ job, abortSignal, isForCurrentlyVisibleMessage, + maxAttachmentSizeInKib, + maxTextAttachmentSizeInKib, dependencies, }: { job: AttachmentDownloadJobType; - abortSignal: AbortSignal; - isForCurrentlyVisibleMessage: boolean; dependencies: DependenciesType; -}): Promise { +} & RunDownloadAttachmentJobOptions): Promise { const { messageId, attachment, attachmentType } = job; const jobIdForLogging = getJobIdForLogging(job); @@ -507,16 +590,16 @@ export async function runDownloadAttachmentJobInner({ throw new Error(`${logId}: Key information required for job was missing.`); } - const maxInKib = getMaximumIncomingAttachmentSizeInKb(getValue); - const maxTextAttachmentSizeInKib = - getMaximumIncomingTextAttachmentSizeInKb(getValue); - const { size } = attachment; const sizeInKib = size / KIBIBYTE; - if (!Number.isFinite(size) || size < 0 || sizeInKib > maxInKib) { + if ( + !Number.isFinite(size) || + size < 0 || + sizeInKib > maxAttachmentSizeInKib + ) { throw new AttachmentSizeError( - `${logId}: Attachment was ${sizeInKib}kib, max is ${maxInKib}kib` + `${logId}: Attachment was ${sizeInKib}kib, max is ${maxAttachmentSizeInKib}kib` ); } if ( diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index f14c143d52..3ef89b8932 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -132,6 +132,9 @@ export type GlobalModalsStateType = ReadonlyDeep<{ isSignalConnectionsVisible: boolean; isStoriesSettingsVisible: boolean; isWhatsNewVisible: boolean; + lowDiskSpaceBackupImportModal: { + bytesNeeded: number; + } | null; messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null; notePreviewModalProps: NotePreviewModalPropsType | null; usernameOnboardingState: UsernameOnboardingState; @@ -222,6 +225,10 @@ const SHOW_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL = 'globalModals/SHOW_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL'; const HIDE_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL = 'globalModals/HIDE_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL'; +const SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL = + 'globalModals/SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL'; +const HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL = + 'globalModals/HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL'; export type ContactModalStateType = ReadonlyDeep<{ contactId: string; @@ -449,6 +456,17 @@ type HideCriticalIdlePrimaryDeviceModalActionType = ReadonlyDeep<{ type: typeof HIDE_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL; }>; +type ShowLowDiskSpaceBackupImportModalActionType = ReadonlyDeep<{ + type: typeof SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL; + payload: { + bytesNeeded: number; + }; +}>; + +type HideLowDiskSpaceBackupImportModalActionType = ReadonlyDeep<{ + type: typeof HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL; +}>; + type ToggleEditNicknameAndNoteModalActionType = ReadonlyDeep<{ type: typeof TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL; payload: EditNicknameAndNoteModalPropsType | null; @@ -489,6 +507,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | HideBackfillFailureModalActionType | HideContactModalActionType | HideCriticalIdlePrimaryDeviceModalActionType + | HideLowDiskSpaceBackupImportModalActionType | HideSendAnywayDialogActiontype | HideStoriesSettingsActionType | HideTapToViewNotAvailableModalActionType @@ -503,6 +522,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | ShowContactModalActionType | ShowEditHistoryModalActionType | ShowErrorModalActionType + | ShowLowDiskSpaceBackupImportModalActionType | ShowMediaPermissionsModalActionType | ShowSendAnywayDialogActionType | ShowShortcutGuideModalActionType @@ -548,6 +568,7 @@ export const actions = { hideBlockingSafetyNumberChangeDialog, hideContactModal, hideCriticalIdlePrimaryDeviceModal, + hideLowDiskSpaceBackupImportModal, hideStoriesSettings, hideTapToViewNotAvailableModal, hideUserNotFoundModal, @@ -560,6 +581,7 @@ export const actions = { showEditHistoryModal, showErrorModal, showGV2MigrationDialog, + showLowDiskSpaceBackupImportModal, showShareCallLinkViaSignal, showShortcutGuideModal, showStickerPackPreview, @@ -1179,6 +1201,23 @@ function hideCriticalIdlePrimaryDeviceModal(): ThunkAction< }; } +function showLowDiskSpaceBackupImportModal( + bytesNeeded: number +): ShowLowDiskSpaceBackupImportModalActionType { + return { + type: SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL, + payload: { + bytesNeeded, + }, + }; +} + +function hideLowDiskSpaceBackupImportModal(): HideLowDiskSpaceBackupImportModalActionType { + return { + type: HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL, + }; +} + function toggleEditNicknameAndNoteModal( payload: EditNicknameAndNoteModalPropsType | null ): ToggleEditNicknameAndNoteModalActionType { @@ -1303,6 +1342,7 @@ export function getEmptyState(): GlobalModalsStateType { isSignalConnectionsVisible: false, isStoriesSettingsVisible: false, isWhatsNewVisible: false, + lowDiskSpaceBackupImportModal: null, usernameOnboardingState: UsernameOnboardingState.NeverShown, profileEditorHasError: false, profileEditorInitialEditState: undefined, @@ -1740,5 +1780,19 @@ export function reducer( }; } + if (action.type === SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL) { + return { + ...state, + lowDiskSpaceBackupImportModal: action.payload, + }; + } + + if (action.type === HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL) { + return { + ...state, + lowDiskSpaceBackupImportModal: null, + }; + } + return state; } diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index 0d155204c1..db7cbbc4b2 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -139,6 +139,7 @@ export const SmartGlobalModalContainer = memo( editNicknameAndNoteModalProps, errorModalProps, forwardMessagesProps, + lowDiskSpaceBackupImportModal, mediaPermissionsModalProps, messageRequestActionsConfirmationProps, notePreviewModalProps, @@ -161,6 +162,7 @@ export const SmartGlobalModalContainer = memo( closeErrorModal, closeMediaPermissionsModal, hideCriticalIdlePrimaryDeviceModal, + hideLowDiskSpaceBackupImportModal, hideTapToViewNotAvailableModal, hideUserNotFoundModal, hideWhatsNewModal, @@ -236,6 +238,8 @@ export const SmartGlobalModalContainer = memo( draftGifMessageSendModalProps={draftGifMessageSendModalProps} forwardMessagesProps={forwardMessagesProps} hideCriticalIdlePrimaryDeviceModal={hideCriticalIdlePrimaryDeviceModal} + hideLowDiskSpaceBackupImportModal={hideLowDiskSpaceBackupImportModal} + lowDiskSpaceBackupImportModal={lowDiskSpaceBackupImportModal} messageRequestActionsConfirmationProps={ messageRequestActionsConfirmationProps } diff --git a/ts/test-electron/services/AttachmentDownloadManager_test.ts b/ts/test-electron/services/AttachmentDownloadManager_test.ts index 0f1c704bef..f5c4732d6e 100644 --- a/ts/test-electron/services/AttachmentDownloadManager_test.ts +++ b/ts/test-electron/services/AttachmentDownloadManager_test.ts @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import { assert } from 'chai'; import { omit } from 'lodash'; +import type { StatsFs } from 'fs'; import * as MIME from '../../types/MIME'; import { @@ -23,6 +24,8 @@ import { type AttachmentType, AttachmentVariant } from '../../types/Attachment'; import { strictAssert } from '../../util/assert'; import { AttachmentDownloadSource } from '../../sql/Interface'; import { getAttachmentCiphertextLength } from '../../AttachmentCrypto'; +import { MEBIBYTE } from '../../types/AttachmentSize'; +import { generateAci } from '../../types/ServiceId'; function composeJob({ messageId, @@ -66,14 +69,22 @@ describe('AttachmentDownloadManager/JobManager', () => { let sandbox: sinon.SinonSandbox; let clock: sinon.SinonFakeTimers; let isInCall: sinon.SinonStub; + let onLowDiskSpaceBackupImport: sinon.SinonStub; + let statfs: sinon.SinonStub; beforeEach(async () => { await DataWriter.removeAll(); + await window.storage.user.setAciAndDeviceId(generateAci(), 1); sandbox = sinon.createSandbox(); clock = sandbox.useFakeTimers(); isInCall = sandbox.stub().returns(false); + onLowDiskSpaceBackupImport = sandbox + .stub() + .callsFake(async () => + window.storage.put('backupMediaDownloadPaused', true) + ); runJob = sandbox.stub().callsFake(async () => { return new Promise<{ status: 'finished' | 'retry' }>(resolve => { Promise.resolve().then(() => { @@ -81,6 +92,12 @@ describe('AttachmentDownloadManager/JobManager', () => { }); }); }); + statfs = sandbox.stub().callsFake(() => + Promise.resolve({ + bavail: 100_000_000_000, + bsize: 100, + } as StatsFs) + ); downloadManager = new AttachmentDownloadManager({ ...AttachmentDownloadManager.defaultParams, @@ -95,6 +112,8 @@ describe('AttachmentDownloadManager/JobManager', () => { maxBackoffTime: 10 * MINUTE, }, }), + onLowDiskSpaceBackupImport, + statfs, }); }); @@ -294,6 +313,39 @@ describe('AttachmentDownloadManager/JobManager', () => { assert.strictEqual(runJob.callCount, 5); }); + it('triggers onLowDiskSpace for backup import jobs', async () => { + const jobs = await addJobs(1, idx => ({ + source: AttachmentDownloadSource.BACKUP_IMPORT, + digest: `digestFor${idx}`, + attachment: { + contentType: MIME.IMAGE_JPEG, + size: 128, + digest: `digestFor${idx}`, + backupLocator: { + mediaName: 'medianame', + }, + }, + })); + + statfs.callsFake(() => Promise.resolve({ bavail: 0, bsize: 8 })); + + await downloadManager?.start(); + await advanceTime(2 * MINUTE); + assert.strictEqual(runJob.callCount, 0); + assert.strictEqual(onLowDiskSpaceBackupImport.callCount, 1); + assert.isTrue(window.storage.get('backupMediaDownloadPaused')); + + statfs.callsFake(() => + Promise.resolve({ bavail: 100_000_000_000, bsize: 8 }) + ); + + const job0Attempts = getPromisesForAttempts(jobs[0], 1)[0]; + await window.storage.put('backupMediaDownloadPaused', false); + await advanceTime(2 * MINUTE); + assert.strictEqual(runJob.callCount, 1); + await job0Attempts.started; + }); + it('handles retries for failed', async () => { const jobs = await addJobs(2); const job0Attempts = getPromisesForAttempts(jobs[0], 1); @@ -478,6 +530,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: true, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, @@ -510,6 +564,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: true, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, @@ -561,6 +617,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: true, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, @@ -598,6 +656,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: true, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, @@ -639,6 +699,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: false, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, @@ -681,6 +743,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: false, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, @@ -730,6 +794,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => { job, isForCurrentlyVisibleMessage: false, abortSignal: abortController.signal, + maxAttachmentSizeInKib: 100 * MEBIBYTE, + maxTextAttachmentSizeInKib: 2 * MEBIBYTE, dependencies: { deleteDownloadData, downloadAttachment, diff --git a/ts/types/AttachmentSize.ts b/ts/types/AttachmentSize.ts index e599919bc6..7e716e7661 100644 --- a/ts/types/AttachmentSize.ts +++ b/ts/types/AttachmentSize.ts @@ -6,7 +6,7 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow'; import type * as RemoteConfig from '../RemoteConfig'; export const KIBIBYTE = 1024; -const MEBIBYTE = 1024 * 1024; +export const MEBIBYTE = 1024 * 1024; const DEFAULT_MAX = 100 * MEBIBYTE; export const getMaximumOutgoingAttachmentSizeInKb = (