// Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { CSSProperties } from 'react'; import React, { useCallback } from 'react'; import classNames from 'classnames'; import { Blurhash } from 'react-blurhash'; import { Spinner } from '../Spinner'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { AttachmentForUIType, AttachmentType, } from '../../types/Attachment'; import { defaultBlurHash, isIncremental, isPermanentlyUndownloadable, isReadyToView, } from '../../types/Attachment'; import { ProgressCircle } from '../ProgressCircle'; import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler'; export enum CurveType { None = 0, Tiny = 4, Small = 10, Normal = 18, } export type Props = { alt: string; attachment: AttachmentForUIType; url?: string; className?: string; height?: number; width?: number; cropWidth?: number; cropHeight?: number; tabIndex?: number; overlayText?: string; noBorder?: boolean; noBackground?: boolean; bottomOverlay?: boolean; closeButton?: boolean; curveBottomLeft?: CurveType; curveBottomRight?: CurveType; curveTopLeft?: CurveType; curveTopRight?: CurveType; darkOverlay?: boolean; playIconOverlay?: boolean; blurHash?: string; i18n: LocalizerType; theme?: ThemeType; showMediaNoLongerAvailableToast?: () => void; showVisualAttachment?: (attachment: AttachmentType) => void; cancelDownload?: () => void; startDownload?: () => void; onClickClose?: (attachment: AttachmentType) => void; onError?: () => void; }; export function Image({ alt, attachment, blurHash, bottomOverlay, className, closeButton, curveBottomLeft, curveBottomRight, curveTopLeft, curveTopRight, darkOverlay, height = 0, i18n, noBackground, noBorder, showMediaNoLongerAvailableToast, showVisualAttachment, startDownload, cancelDownload, onClickClose, onError, overlayText, playIconOverlay, tabIndex, theme, url, width = 0, cropWidth = 0, cropHeight = 0, }: Props): JSX.Element { const resolvedBlurHash = blurHash || defaultBlurHash(theme); const curveStyles: CSSProperties = { borderStartStartRadius: curveTopLeft || CurveType.None, borderStartEndRadius: curveTopRight || CurveType.None, borderEndStartRadius: curveBottomLeft || CurveType.None, borderEndEndRadius: curveBottomRight || CurveType.None, }; const showVisualAttachmentClick = useCallback( (event: React.MouseEvent) => { if (showVisualAttachment) { event.preventDefault(); event.stopPropagation(); showVisualAttachment(attachment); } }, [attachment, showVisualAttachment] ); const showVisualAttachmentKeyDown = useCallback( (event: React.KeyboardEvent) => { if ( showVisualAttachment && (event.key === 'Enter' || event.key === 'Space') ) { event.preventDefault(); event.stopPropagation(); showVisualAttachment(attachment); } }, [attachment, showVisualAttachment] ); const cancelDownloadClick = useCallback( (event: React.MouseEvent) => { if (cancelDownload) { event.preventDefault(); event.stopPropagation(); cancelDownload(); } }, [cancelDownload] ); const cancelDownloadKeyDown = useCallback( (event: React.KeyboardEvent) => { if (cancelDownload && (event.key === 'Enter' || event.key === 'Space')) { event.preventDefault(); event.stopPropagation(); cancelDownload(); } }, [cancelDownload] ); const startDownloadClick = useCallback( (event: React.MouseEvent) => { if (startDownload) { event.preventDefault(); event.stopPropagation(); startDownload(); } }, [startDownload] ); const startDownloadKeyDown = useCallback( (event: React.KeyboardEvent) => { if (startDownload && (event.key === 'Enter' || event.key === 'Space')) { event.preventDefault(); event.stopPropagation(); startDownload(); } }, [startDownload] ); const undownloadableClick = useUndownloadableMediaHandler( showMediaNoLongerAvailableToast ); const imageOrBlurHash = url ? ( {alt} ) : ( ); const startDownloadButton = !attachment.path && !attachment.pending && !isIncremental(attachment) ? ( ) : undefined; const isUndownloadable = isPermanentlyUndownloadable(attachment); // eslint-disable-next-line no-nested-ternary const startDownloadOrUnavailableButton = startDownload ? ( isUndownloadable ? ( ) : ( startDownloadButton ) ) : null; const spinner = isIncremental(attachment) || !cancelDownload ? undefined : getSpinner({ attachment, i18n, cancelDownloadClick, cancelDownloadKeyDown, tabIndex, }); return (
{imageOrBlurHash} {startDownloadOrUnavailableButton} {spinner} {attachment.caption ? ( {i18n('icu:imageCaptionIconAlt')} ) : null} {bottomOverlay ? (
) : null} {(attachment.path || isIncremental(attachment)) && !isUndownloadable && playIconOverlay ? (
) : null} {overlayText ? (
{overlayText}
) : null} {darkOverlay || !noBorder ? (
) : null} {showVisualAttachment && isReadyToView(attachment) ? (
); } export function getSpinner({ attachment, cancelDownloadClick, cancelDownloadKeyDown, i18n, tabIndex, }: { attachment: AttachmentForUIType; cancelDownloadClick: (event: React.MouseEvent) => void; cancelDownloadKeyDown: ( event: React.KeyboardEvent ) => void; i18n: LocalizerType; tabIndex: number | undefined; }): JSX.Element | undefined { const downloadFraction = attachment.pending && !isIncremental(attachment) && attachment.size && attachment.totalDownloaded ? attachment.totalDownloaded / attachment.size : undefined; if (downloadFraction) { return ( ); } if (!attachment.pending) { return undefined; } return ( ); }