fix(gallery): paginate media, activity log and export on detached timeline sets — never the live timeline (#163)
RoomTimeline renders a numeric index window into the live timeline's event arrays; SDK back-pagination prepends, so any side panel calling paginateEventTimeline(room.getLiveTimeline()) shifted the visible messages into the past on the next render and broke at-bottom tracking. New utils/detachedTimeline.ts builds a timeline set that mirrors the already-loaded history and paginates independently: a room-registered filtered set (server-side contains_url / types filter) when the filter is usable, else a private EventTimelineSet seeded from the live timeline. useRoomMediaTimeline wraps it for the gallery (live events + redactions handled); RoomActivityLog uses a type filter (safe in encrypted rooms); ExportRoomHistory pages a private set so a full export no longer parks thousands of events in the live timeline. Verified with Playwright against a local Synapse in a 400-message plain room and a 200-message encrypted room: timeline stays at the bottom through gallery pages, activity load-more and a full export; live messages keep auto-scrolling; all media found in both rooms. Also adds scripts/dev-homeserver.sh + scripts/dev-seed.py (local throwaway Synapse for driving the real UI) and documents them. Closes #163 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Box, Button, Icon, IconButton, Icons, Scroll, Spinner, Text, color, config } from 'folds';
|
||||
import { MatrixEvent } from 'matrix-js-sdk';
|
||||
import { Page, PageContent, PageHeader } from '../../components/page';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import {
|
||||
collectTimelineEvents,
|
||||
createDetachedTimelineSet,
|
||||
createTypesFilter,
|
||||
} from '../../utils/detachedTimeline';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -343,15 +348,23 @@ export function RoomActivityLog({ requestClose }: RoomActivityLogProps) {
|
||||
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
|
||||
const [canLoadMore, setCanLoadMore] = useState(true);
|
||||
|
||||
// [Gitea #163] Page through a detached, type-filtered timeline set: the
|
||||
// live timeline (rendered by index behind this modal) must not be mutated.
|
||||
const detached = useMemo(
|
||||
() =>
|
||||
createDetachedTimelineSet(mx, room, {
|
||||
filter: createTypesFilter(mx.getSafeUserId(), STATE_EVENT_TYPES, 'io.lotus.activity'),
|
||||
filterSafeWhenEncrypted: true,
|
||||
}),
|
||||
[mx, room],
|
||||
);
|
||||
|
||||
const getStateEvents = useCallback((): MatrixEvent[] => {
|
||||
const typeSet = new Set<string>(STATE_EVENT_TYPES);
|
||||
return room
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
return collectTimelineEvents(detached.set.getLiveTimeline())
|
||||
.filter((ev) => typeSet.has(ev.getType()) && !ev.isRedacted())
|
||||
.slice()
|
||||
.reverse();
|
||||
}, [room]);
|
||||
}, [detached]);
|
||||
|
||||
const [events, setEvents] = useState<MatrixEvent[]>(() => getStateEvents());
|
||||
|
||||
@@ -363,7 +376,7 @@ export function RoomActivityLog({ requestClose }: RoomActivityLogProps) {
|
||||
if (loading || !canLoadMore) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const hasMore = await mx.paginateEventTimeline(room.getLiveTimeline(), {
|
||||
const hasMore = await mx.paginateEventTimeline(detached.set.getLiveTimeline(), {
|
||||
backwards: true,
|
||||
limit: 50,
|
||||
});
|
||||
@@ -375,7 +388,7 @@ export function RoomActivityLog({ requestClose }: RoomActivityLogProps) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, canLoadMore, mx, room, getStateEvents]);
|
||||
}, [loading, canLoadMore, mx, detached, getStateEvents]);
|
||||
|
||||
// Auto-paginate on mount — state events are rarely in the initial sync
|
||||
// window, so we immediately fetch backwards to populate the log.
|
||||
|
||||
Reference in New Issue
Block a user