import React, { useCallback, useEffect, useState } from 'react'; import { Box, Button, Header, Icon, IconButton, Icons, Scroll, Spinner, Text, Tooltip, TooltipProvider, config, } from 'folds'; import { Direction, EventType, MatrixEvent, MsgType, Room } from 'matrix-js-sdk'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { mxcUrlToHttp } from '../../utils/matrix'; import { ContainerColor } from '../../styles/ContainerColor.css'; type GalleryTab = 'image' | 'video' | 'file'; type MediaGalleryProps = { room: Room; onClose: () => void; }; const TAB_LABELS: Record = { image: 'Images', video: 'Videos', file: 'Files', }; const TAB_MSGTYPES: Record = { image: MsgType.Image, video: MsgType.Video, file: MsgType.File, }; function TabButton({ label, active, onClick, }: { label: string; active: boolean; onClick: () => void; }) { return ( ); } export function MediaGallery({ room, onClose }: MediaGalleryProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [tab, setTab] = useState('image'); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [paginationToken, setPaginationToken] = useState(null); const msgtype = TAB_MSGTYPES[tab]; const loadMedia = useCallback( async (fromToken: string | null, append: boolean) => { setLoading(true); try { const response = await mx.createMessagesRequest( room.roomId, fromToken, 100, Direction.Backward, undefined, ); const { end, chunk } = response; const filtered = chunk .filter( (ev) => ev.type === EventType.RoomMessage && ev.content?.msgtype === msgtype && !ev.unsigned?.redacted_because, ) .map((ev) => new MatrixEvent(ev)); setEvents((prev) => (append ? [...prev, ...filtered] : filtered)); setPaginationToken(end ?? null); } catch { // silently swallow fetch errors — gallery stays showing what it has } finally { setLoading(false); } }, [mx, room.roomId, msgtype], ); useEffect(() => { setEvents([]); setPaginationToken(null); loadMedia(null, false).catch(() => undefined); }, [loadMedia]); const handleLoadMore = () => { if (paginationToken) loadMedia(paginationToken, true).catch(() => undefined); }; return ( {/* Header */}
Media Close } > {(triggerRef) => ( )}
{/* Tab bar */} {(Object.keys(TAB_LABELS) as GalleryTab[]).map((t) => ( setTab(t)} /> ))} {/* Content */} {loading && events.length === 0 && ( )} {!loading && events.length === 0 && ( {`No ${TAB_LABELS[tab].toLowerCase()} found in this room.`} )} {/* Image/Video grid */} {(tab === 'image' || tab === 'video') && events.length > 0 && (
{events.map((mEvent) => { const content = mEvent.getContent(); const mxcUrl: string | undefined = content.url ?? content.file?.url; if (!mxcUrl) return null; const thumbUrl = mxcUrlToHttp(mx, mxcUrl, useAuthentication, 120, 120, 'crop') ?? ''; const body: string = content.body ?? ''; return ( {body} ); })}
)} {/* File list */} {tab === 'file' && events.length > 0 && ( {events.map((mEvent) => { const content = mEvent.getContent(); const mxcUrl: string | undefined = content.url ?? content.file?.url; const body: string = content.body ?? 'Unnamed file'; const downloadUrl = mxcUrl ? (mxcUrlToHttp(mx, mxcUrl, useAuthentication) ?? '#') : '#'; return ( {body} { const anchor = document.createElement('a'); anchor.href = downloadUrl; anchor.download = body; anchor.target = '_blank'; anchor.rel = 'noreferrer'; anchor.click(); }} > ); })} )} {/* Load more */} {paginationToken !== null && !loading && ( )} {/* Loading more spinner */} {loading && events.length > 0 && ( )}
); }