// Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; import classNames from 'classnames'; import type { AttachmentForUIType, AttachmentType, } from '../../types/Attachment.std.js'; import { areAllAttachmentsVisual, getAlt, getImageDimensionsForTimeline, getThumbnailUrl, getUrl, isDownloadable, isIncremental, isVideoAttachment, } from '../../util/Attachment.std.js'; import { Image, CurveType } from './Image.dom.js'; import type { LocalizerType, ThemeType } from '../../types/Util.std.js'; import { AttachmentDetailPill } from './AttachmentDetailPill.dom.js'; import { strictAssert } from '../../util/assert.std.js'; export type DirectionType = 'incoming' | 'outgoing'; export type Props = { attachments: ReadonlyArray; bottomOverlay?: boolean; direction: DirectionType; isSticker?: boolean; shouldCollapseAbove?: boolean; shouldCollapseBelow?: boolean; stickerSize?: number; tabIndex?: number; withContentAbove?: boolean; withContentBelow?: boolean; i18n: LocalizerType; theme?: ThemeType; onError: () => void; showVisualAttachment: (attachment: AttachmentType) => void; showMediaNoLongerAvailableToast: () => void; cancelDownload: () => void; startDownload: () => void; }; const GAP = 1; function getCurves({ direction, shouldCollapseAbove, shouldCollapseBelow, withContentAbove, withContentBelow, }: { direction: DirectionType; shouldCollapseAbove?: boolean; shouldCollapseBelow?: boolean; withContentAbove?: boolean; withContentBelow?: boolean; }): { curveTopLeft: CurveType; curveTopRight: CurveType; curveBottomLeft: CurveType; curveBottomRight: CurveType; } { let curveTopLeft = CurveType.None; let curveTopRight = CurveType.None; let curveBottomLeft = CurveType.None; let curveBottomRight = CurveType.None; if (shouldCollapseAbove && direction === 'incoming') { curveTopLeft = CurveType.Tiny; curveTopRight = CurveType.Normal; } else if (shouldCollapseAbove && direction === 'outgoing') { curveTopLeft = CurveType.Normal; curveTopRight = CurveType.Tiny; } else if (!withContentAbove) { curveTopLeft = CurveType.Normal; curveTopRight = CurveType.Normal; } if (withContentBelow) { curveBottomLeft = CurveType.None; curveBottomRight = CurveType.None; } else if (shouldCollapseBelow && direction === 'incoming') { curveBottomLeft = CurveType.Tiny; curveBottomRight = CurveType.None; } else if (shouldCollapseBelow && direction === 'outgoing') { curveBottomLeft = CurveType.None; curveBottomRight = CurveType.Tiny; } else { curveBottomLeft = CurveType.Normal; curveBottomRight = CurveType.Normal; } return { curveTopLeft, curveTopRight, curveBottomLeft, curveBottomRight, }; } export function ImageGrid({ attachments, bottomOverlay, direction, i18n, isSticker, stickerSize, onError, showMediaNoLongerAvailableToast, showVisualAttachment, cancelDownload, startDownload, shouldCollapseAbove, shouldCollapseBelow, tabIndex, theme, withContentAbove, withContentBelow, }: Props): React.JSX.Element | null { const { curveTopLeft, curveTopRight, curveBottomLeft, curveBottomRight } = getCurves({ direction, shouldCollapseAbove, shouldCollapseBelow, withContentAbove, withContentBelow, }); const withBottomOverlay = Boolean(bottomOverlay && !withContentBelow); const startDownloadClick = React.useCallback( (event: React.MouseEvent) => { if (startDownload) { event.preventDefault(); event.stopPropagation(); startDownload(); } }, [startDownload] ); const startDownloadKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if (startDownload && (event.key === 'Enter' || event.key === 'Space')) { event.preventDefault(); event.stopPropagation(); startDownload(); } }, [startDownload] ); const showAttachmentOrNoLongerAvailableToast = React.useCallback( (attachmentIndex: number) => { const attachment = attachments[attachmentIndex]; strictAssert(attachment, 'Missing attachment'); return attachment.isPermanentlyUndownloadable ? showMediaNoLongerAvailableToast : showVisualAttachment; }, [attachments, showVisualAttachment, showMediaNoLongerAvailableToast] ); if (!attachments || !attachments.length) { return null; } const downloadableAttachments = attachments.filter(attachment => isDownloadable(attachment) ); const detailPill = ( ); const downloadPill = renderDownloadPill({ attachments, i18n, startDownloadClick, startDownloadKeyDown, }); if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) { const [attachment] = attachments; strictAssert(attachment, 'Missing attachment'); const { height, width } = getImageDimensionsForTimeline( attachment, isSticker ? stickerSize : undefined ); return (
{getAlt(attachment, {detailPill}
); } if (attachments.length === 2) { const [attachment1, attachment2] = attachments; strictAssert(attachment1, 'Missing attachment 1'); strictAssert(attachment2, 'Missing attachment 2'); return (
{getAlt(attachment1, {getAlt(attachment2, {detailPill} {downloadPill}
); } if (attachments.length === 3) { const [attachment1, attachment2, attachment3] = attachments; strictAssert(attachment1, 'Missing attachment 1'); strictAssert(attachment2, 'Missing attachment 2'); strictAssert(attachment3, 'Missing attachment 3'); return (
{getAlt(attachment1,
{getAlt(attachment2, {getAlt(attachment3,
{detailPill} {downloadPill}
); } if (attachments.length === 4) { const [attachment1, attachment2, attachment3, attachment4] = attachments; strictAssert(attachment1, 'Missing attachment 1'); strictAssert(attachment2, 'Missing attachment 2'); strictAssert(attachment3, 'Missing attachment 3'); strictAssert(attachment4, 'Missing attachment 4'); return (
{getAlt(attachment1, {getAlt(attachment2,
{getAlt(attachment3, {getAlt(attachment4,
{detailPill} {downloadPill}
); } const moreMessagesOverlay = attachments.length > 5; const moreMessagesOverlayText = moreMessagesOverlay ? `+${attachments.length - 5}` : undefined; const [attachment1, attachment2, attachment3, attachment4, attachment5] = attachments; strictAssert(attachment1, 'Missing attachment 1'); strictAssert(attachment2, 'Missing attachment 2'); strictAssert(attachment3, 'Missing attachment 3'); strictAssert(attachment4, 'Missing attachment 4'); strictAssert(attachment5, 'Missing attachment 4'); return (
{getAlt(attachment1, {getAlt(attachment2,
{getAlt(attachment3, {getAlt(attachment4, {getAlt(attachment5,
{detailPill} {downloadPill}
); } function renderDownloadPill({ attachments, i18n, startDownloadClick, startDownloadKeyDown, }: { attachments: ReadonlyArray; i18n: LocalizerType; startDownloadClick: (event: React.MouseEvent) => void; startDownloadKeyDown: (event: React.KeyboardEvent) => void; }): React.JSX.Element | null { const downloadedOrPendingOrIncremental = attachments.some( attachment => attachment.path || attachment.pending || isIncremental(attachment) ); if (downloadedOrPendingOrIncremental) { return null; } const noneDownloadable = !attachments.some(attachment => isDownloadable(attachment) ); if (noneDownloadable) { return null; } return ( ); }