2026-07-10 01:08:34 -04:00
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
|
import { NotificationCountType, Room, RoomEvent, Thread, ThreadEvent } from 'matrix-js-sdk';
|
|
|
|
|
|
2026-07-10 01:21:01 -04:00
|
|
|
// Cheap signature over the fields the Threads list actually renders on (id,
|
|
|
|
|
// last-activity ts, reply count, unread, participation, and a root-edit token).
|
|
|
|
|
// We only push a new snapshot when this changes, so noisy repeated
|
|
|
|
|
// ThreadEvent.Update / UnreadNotifications re-emits that don't alter the list
|
|
|
|
|
// don't cause a re-render.
|
|
|
|
|
// `replyToEvent` reads the server bundle's latest_event, so last-activity is
|
|
|
|
|
// accurate immediately (before/without lazily paginating each thread's replies);
|
|
|
|
|
// `length` covers reply-count changes (e.g. a mid-thread redaction) and
|
|
|
|
|
// `replacingEventId` covers a root-message edit changing the row snippet.
|
2026-07-10 01:08:34 -04:00
|
|
|
const signatureOf = (room: Room, threads: Thread[]): string =>
|
|
|
|
|
threads
|
|
|
|
|
.map((t) => {
|
2026-07-10 01:21:01 -04:00
|
|
|
const ts = t.replyToEvent?.getTs() ?? t.rootEvent?.getTs() ?? 0;
|
2026-07-10 01:08:34 -04:00
|
|
|
const unread = room.getThreadUnreadNotificationCount(t.id, NotificationCountType.Total) ?? 0;
|
2026-07-10 01:21:01 -04:00
|
|
|
const rootEdit = t.rootEvent?.replacingEventId() ?? '';
|
|
|
|
|
const participated = t.hasCurrentUserParticipated ? 1 : 0;
|
|
|
|
|
return `${t.id}:${ts}:${t.length}:${unread}:${participated}:${rootEdit}`;
|
2026-07-10 01:08:34 -04:00
|
|
|
})
|
|
|
|
|
.sort()
|
|
|
|
|
.join('|');
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Live list of a room's threads. Populates the full server list once via
|
|
|
|
|
* `fetchRoomThreads`, then keeps a snapshot in sync with the room's thread and
|
|
|
|
|
* unread events. Returns the raw `Thread[]`; the caller derives/filters/sorts.
|
|
|
|
|
*/
|
|
|
|
|
export function useRoomThreads(room: Room): Thread[] {
|
|
|
|
|
const [threads, setThreads] = useState<Thread[]>(() => room.getThreads());
|
|
|
|
|
const sigRef = useRef<string>('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
const next = room.getThreads();
|
|
|
|
|
const sig = signatureOf(room, next);
|
|
|
|
|
if (sig === sigRef.current) return;
|
|
|
|
|
sigRef.current = sig;
|
|
|
|
|
setThreads(next);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Force the first push, then pull the full server-known thread list.
|
|
|
|
|
sigRef.current = '';
|
|
|
|
|
refresh();
|
2026-07-11 13:52:36 -04:00
|
|
|
room
|
|
|
|
|
.fetchRoomThreads()
|
|
|
|
|
.then(refresh)
|
|
|
|
|
.catch(() => undefined);
|
2026-07-10 01:08:34 -04:00
|
|
|
|
|
|
|
|
room.on(ThreadEvent.New, refresh);
|
|
|
|
|
room.on(ThreadEvent.NewReply, refresh);
|
|
|
|
|
room.on(ThreadEvent.Update, refresh);
|
|
|
|
|
room.on(ThreadEvent.Delete, refresh);
|
|
|
|
|
room.on(RoomEvent.UnreadNotifications, refresh);
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
room.removeListener(ThreadEvent.New, refresh);
|
|
|
|
|
room.removeListener(ThreadEvent.NewReply, refresh);
|
|
|
|
|
room.removeListener(ThreadEvent.Update, refresh);
|
|
|
|
|
room.removeListener(ThreadEvent.Delete, refresh);
|
|
|
|
|
room.removeListener(RoomEvent.UnreadNotifications, refresh);
|
|
|
|
|
};
|
|
|
|
|
}, [room]);
|
|
|
|
|
|
|
|
|
|
return threads;
|
|
|
|
|
}
|