import React, { useState } from 'react'; import { Icon, Icons, Spinner, Text } from 'folds'; import { MatrixEvent, MsgType } from 'matrix-js-sdk'; import { BlurhashCanvas } from 'react-blurhash'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useDecryptedMediaUrl } from '../../../hooks/useDecryptedMediaUrl'; import { getThumbMxc } from '../../../utils/mediaThumb'; import { validBlurHash } from '../../../utils/blurHash'; import { MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common'; import * as css from './MediaGroupGrid.css'; /** Column count for `n` tiles: 2 → 2, 3 → 3, 4 → 2×2, 5–6 → 3, 7+ → 4. */ export const gridColumns = (n: number): number => { if (n <= 2) return 2; if (n === 3) return 3; if (n === 4) return 2; if (n <= 6) return 3; return 4; }; /** "5 photos" / "2 videos" / "6 items". */ export const describeGroup = (events: MatrixEvent[]): string => { const videos = events.filter((e) => e.getContent().msgtype === MsgType.Video).length; const n = events.length; if (videos === 0) return `${n} photos`; if (videos === n) return `${n} videos`; return `${n} items`; }; function Cell({ mEvent, load, onOpen, }: { mEvent: MatrixEvent; load: boolean; onOpen: (eventId: string) => void; }) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const content = mEvent.getContent(); const isVideo = content.msgtype === MsgType.Video; const thumbMxc = getThumbMxc(mEvent); const info = content.info as Record | undefined; const encInfo = content.file ? ((info?.thumbnail_file as typeof content.file | undefined) ?? content.file) : undefined; const mimeType = (info?.thumbnail_info as { mimetype?: string } | undefined)?.mimetype ?? (info?.mimetype as string | undefined); const blurHash = validBlurHash(info?.[MATRIX_BLUR_HASH_PROPERTY_NAME] as string | undefined); const media = useDecryptedMediaUrl(mx, thumbMxc, encInfo, useAuthentication, mimeType, load); const body = typeof content.body === 'string' ? content.body : ''; return ( ); } type MediaGroupGridProps = { events: MatrixEvent[]; mediaAutoLoad: boolean; onOpen: (eventId: string) => void; onShowSeparately: () => void; }; /** * [Gitea #137] Several consecutive image/video events from one sender shown * as one grid. Each tile opens the room's shared lightbox at that event, so * ←/→ walk through the group (and beyond). Purely a render-time grouping. */ export function MediaGroupGrid({ events, mediaAutoLoad, onOpen, onShowSeparately, }: MediaGroupGridProps) { const [load, setLoad] = useState(mediaAutoLoad); const columns = gridColumns(events.length); return (
setLoad(true)} > {events.map((ev) => ( setLoad(true)} /> ))}
{describeGroup(events)} {!load && ' · tap to load'} · Show separately
); } /** Shown under the last of a gallery the user split up, to put it back together. */ export function RegroupChip({ onClick }: { onClick: () => void }) { return (
Show as gallery
); }