Files
cinny/src/app/features/room/RoomMediaLightbox.tsx
T
jaredandClaude Opus 5 f111b3c9af
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
feat(media): timeline images open the gallery lightbox at that event (#219)
Clicking an image in the room or thread timeline now opens the same viewer
the media gallery uses — dark backdrop, sender + date, 1/N counter, ←/→ across
the room's media, +/-/0 and wheel/double-click zoom, download, jump to
message — positioned at the clicked event. RoomMediaLightbox feeds it from the
detached media timeline (#163); when the event isn't in the loaded window it
pages back (bounded, 6 pages) and shows the clicked image alone meanwhile, so
the viewer opens instantly. ImageContent gains onOpenViewer (RenderMessageContent
passes onOpenImageViewer); its built-in viewer remains for stickers, search
results, pins, notifications and avatars.

Verified headless: click → 'Media viewer' dialog focused, counter 1/2, '+' →
120 %, Jump to message present, Esc closes; an older image 10 pages up →
found at 10/45 after paging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-19 13:13:28 -04:00

74 lines
2.7 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useRoomMediaTimeline } from '../../hooks/useRoomMediaTimeline';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { Lightbox, toLightboxItems } from './MediaGallery';
// How many media pages to walk back looking for the clicked event before
// settling for a single-item viewer.
const MAX_SEARCH_PAGES = 6;
type RoomMediaLightboxProps = {
room: Room;
eventId: string;
onClose: () => void;
};
/**
* [Gitea #219] The room timeline's image viewer is the gallery lightbox,
* opened at the clicked event: same dark backdrop, sender + date, 1/N counter,
* ←/→ across the room's media, zoom keys, download and "jump to message".
*
* Items come from the same detached media timeline the gallery uses (#163).
* It starts at the live end, so an older image may not be loaded yet: we page
* back a bounded number of times looking for it and meanwhile show the clicked
* image on its own, so the viewer never waits on the network to open.
*/
export function RoomMediaLightbox({ room, eventId, onClose }: RoomMediaLightboxProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { navigateRoom } = useRoomNavigate();
const { events, loadMore, loading, canLoadMore } = useRoomMediaTimeline(mx, room);
const items = useMemo(() => toLightboxItems(room, events), [room, events]);
const index = items.findIndex((it) => it.eventId === eventId);
const pagesRef = useRef(0);
useEffect(() => {
if (index !== -1 || loading || !canLoadMore || pagesRef.current >= MAX_SEARCH_PAGES) return;
pagesRef.current += 1;
loadMore();
}, [index, loading, canLoadMore, loadMore]);
// Until the event is in the loaded window, show just the clicked media.
const fallbackItems = useMemo(() => {
const ev: MatrixEvent | undefined = room.findEventById(eventId) ?? undefined;
return ev ? toLightboxItems(room, [ev]) : [];
}, [room, eventId]);
const handleJump = useCallback(
(id: string) => {
onClose();
navigateRoom(room.roomId, id);
},
[onClose, navigateRoom, room.roomId],
);
const found = index !== -1;
const shownItems = found ? items : fallbackItems;
if (shownItems.length === 0) return null;
return (
<Lightbox
// Remount when the full list arrives so the index and zoom reset cleanly.
key={found ? 'room' : 'single'}
items={shownItems}
initialIndex={found ? index : 0}
useAuthentication={useAuthentication}
onClose={onClose}
onJump={handleJump}
/>
);
}