// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useRef, useState } from 'react'; import classNames from 'classnames'; import { ProgressCircle } from '../ProgressCircle'; import { usePrevious } from '../../hooks/usePrevious'; import type { AttachmentForUIType } from '../../types/Attachment'; import { roundFractionForProgressBar } from '../../util/numbers'; const TRANSITION_DELAY = 200; export type PropsType = { attachment: AttachmentForUIType | undefined; isAttachmentNotAvailable: boolean; isIncoming: boolean; renderAttachmentDownloaded: () => JSX.Element; }; enum IconState { NeedsDownload = 'NeedsDownload', Downloading = 'Downloading', Downloaded = 'Downloaded', } export function AttachmentStatusIcon({ attachment, isAttachmentNotAvailable, isIncoming, renderAttachmentDownloaded, }: PropsType): JSX.Element | null { const [isWaiting, setIsWaiting] = useState(false); let state: IconState = IconState.Downloaded; if (attachment && isAttachmentNotAvailable) { state = IconState.Downloaded; } else if (attachment && !attachment.path && !attachment.pending) { state = IconState.NeedsDownload; } else if (attachment && !attachment.path && attachment.pending) { state = IconState.Downloading; } const timerRef = useRef(); const previousState = usePrevious(state, state); // We need useLayoutEffect; otherwise we might get a flash of the wrong visual state. // We do calculations here which change the UI! React.useLayoutEffect(() => { if (state === previousState) { return; } if ( previousState === IconState.NeedsDownload && state === IconState.Downloading ) { setIsWaiting(true); if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { timerRef.current = undefined; setIsWaiting(false); }, TRANSITION_DELAY); } else if ( previousState === IconState.Downloading && state === IconState.Downloaded ) { setIsWaiting(true); if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { timerRef.current = undefined; setIsWaiting(false); }, TRANSITION_DELAY); } }, [previousState, state]); if (attachment && state === IconState.NeedsDownload) { return (
); } if ( attachment && (state === IconState.Downloading || (state === IconState.Downloaded && isWaiting)) ) { const { size, totalDownloaded } = attachment; let downloadFraction = size && totalDownloaded ? roundFractionForProgressBar(totalDownloaded / size) : undefined; if (state === IconState.Downloading && isWaiting) { downloadFraction = undefined; } if (state === IconState.Downloaded && isWaiting) { downloadFraction = 1; } return (
{downloadFraction ? (
) : undefined}
); } return (
{renderAttachmentDownloaded()}
); }