Files
Desktop/ts/hooks/useAttachmentStatus.std.ts
2025-11-18 14:40:01 -08:00

103 lines
2.7 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentForUIType } from '../types/Attachment.std.js';
import { getUrl } from '../util/Attachment.std.js';
import { MediaTier } from '../types/AttachmentDownload.std.js';
import { missingCaseError } from '../util/missingCaseError.std.js';
import { getAttachmentCiphertextSize } from '../util/AttachmentCrypto.std.js';
import { useDelayedValue } from './useDelayedValue.std.js';
const TRANSITION_DELAY = 200;
type InternalState = 'NeedsDownload' | 'Downloading' | 'ReadyToShow';
export type AttachmentStatusType = Readonly<
| {
state: 'NeedsDownload';
}
| {
state: 'Downloading';
totalDownloaded: number | undefined;
size: number;
}
| {
state: 'ReadyToShow';
}
>;
export function useAttachmentStatus(
attachment: AttachmentForUIType
): AttachmentStatusType;
export function useAttachmentStatus(
attachment: AttachmentForUIType | undefined
): AttachmentStatusType | undefined;
export function useAttachmentStatus(
attachment: AttachmentForUIType | undefined
): AttachmentStatusType | undefined {
const isAttachmentNotAvailable =
attachment == null ||
(attachment.isPermanentlyUndownloadable && !attachment.wasTooBig);
const url = attachment == null ? undefined : getUrl(attachment);
let nextState: InternalState = 'ReadyToShow';
if (attachment && isAttachmentNotAvailable) {
nextState = 'ReadyToShow';
} else if (attachment && url == null && !attachment.pending) {
nextState = 'NeedsDownload';
} else if (attachment && url == null && attachment.pending) {
nextState = 'Downloading';
}
const state = useDelayedValue(nextState, TRANSITION_DELAY);
if (attachment == null) {
return undefined;
}
// Idle
if (state === 'NeedsDownload' && nextState === state) {
return { state: 'NeedsDownload' };
}
const { size: unpaddedPlaintextSize, totalDownloaded } = attachment;
const size = getAttachmentCiphertextSize({
unpaddedPlaintextSize,
mediaTier: MediaTier.STANDARD,
});
// Transition
if (state !== nextState) {
if (nextState === 'NeedsDownload') {
return { state: 'NeedsDownload' };
}
if (nextState === 'Downloading') {
return { state: 'Downloading', size, totalDownloaded };
}
if (nextState === 'ReadyToShow') {
return { state: 'Downloading', size, totalDownloaded: size };
}
throw missingCaseError(nextState);
}
if (state === 'NeedsDownload') {
return { state: 'NeedsDownload' };
}
if (state === 'Downloading') {
return { state: 'Downloading', size, totalDownloaded };
}
if (state === 'ReadyToShow') {
return { state: 'ReadyToShow' };
}
throw missingCaseError(state);
}