2026-05-19 16:26:25 -04:00
|
|
|
import { Room, RoomEvent, RoomMemberEvent, MatrixEvent } from 'matrix-js-sdk';
|
2026-05-15 18:56:17 -04:00
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import { useMatrixClient } from './useMatrixClient';
|
2026-05-16 01:34:20 -04:00
|
|
|
import { reactionOrEditEvent } from '../utils/room';
|
|
|
|
|
|
|
|
|
|
// Receipts can land on reaction/edit events which RoomTimeline skips (renders null).
|
|
|
|
|
// Walk backwards from the receipt event to find the nearest event that IS rendered.
|
2026-05-19 16:26:25 -04:00
|
|
|
function nearestRenderableId(
|
|
|
|
|
liveEvents: MatrixEvent[],
|
|
|
|
|
eventIndex: Map<string, number>,
|
2026-05-21 23:30:50 -04:00
|
|
|
evtId: string,
|
2026-05-19 16:26:25 -04:00
|
|
|
): string | null {
|
|
|
|
|
const idx = eventIndex.get(evtId) ?? -1;
|
2026-05-16 01:34:20 -04:00
|
|
|
if (idx === -1) return null;
|
|
|
|
|
for (let i = idx; i >= 0; i--) {
|
|
|
|
|
const e = liveEvents[i];
|
|
|
|
|
if (!reactionOrEditEvent(e)) return e.getId() ?? null;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-05-15 18:56:17 -04:00
|
|
|
|
|
|
|
|
function computePositions(room: Room, myUserId: string): Map<string, string[]> {
|
|
|
|
|
const map = new Map<string, string[]>();
|
2026-05-16 01:34:20 -04:00
|
|
|
const liveEvents = room.getLiveTimeline().getEvents();
|
2026-05-19 16:26:25 -04:00
|
|
|
// Build O(1) index once instead of O(T) findIndex per member
|
2026-05-21 20:49:33 -04:00
|
|
|
const eventIndex = new Map<string, number>(liveEvents.map((e, i) => [e.getId() ?? '', i]));
|
2026-05-15 18:56:17 -04:00
|
|
|
for (const member of room.getJoinedMembers()) {
|
|
|
|
|
if (member.userId === myUserId) continue;
|
|
|
|
|
const evtId = room.getEventReadUpTo(member.userId);
|
|
|
|
|
if (!evtId) continue;
|
2026-05-19 16:26:25 -04:00
|
|
|
const targetId = nearestRenderableId(liveEvents, eventIndex, evtId);
|
2026-05-16 01:34:20 -04:00
|
|
|
if (!targetId) continue;
|
|
|
|
|
const arr = map.get(targetId);
|
2026-05-15 18:56:17 -04:00
|
|
|
if (arr) arr.push(member.userId);
|
2026-05-16 01:34:20 -04:00
|
|
|
else map.set(targetId, [member.userId]);
|
2026-05-15 18:56:17 -04:00
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRoomReadPositions(room: Room): Map<string, string[]> {
|
|
|
|
|
const mx = useMatrixClient();
|
|
|
|
|
const myUserId = mx.getUserId() ?? '';
|
|
|
|
|
const [positions, setPositions] = useState(() => computePositions(room, myUserId));
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setPositions(computePositions(room, myUserId));
|
2026-05-16 01:34:20 -04:00
|
|
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
const onReceipt = (): void => {
|
|
|
|
|
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
|
|
|
debounceTimer = setTimeout(() => {
|
|
|
|
|
setPositions(computePositions(room, myUserId));
|
|
|
|
|
debounceTimer = null;
|
|
|
|
|
}, 150);
|
|
|
|
|
};
|
2026-05-19 16:26:25 -04:00
|
|
|
const onMembership = (): void => setPositions(computePositions(room, myUserId));
|
2026-05-15 18:56:17 -04:00
|
|
|
room.on(RoomEvent.Receipt, onReceipt);
|
2026-05-22 11:16:11 -04:00
|
|
|
(room as any).on(RoomMemberEvent.Membership, onMembership);
|
2026-05-15 18:56:17 -04:00
|
|
|
return () => {
|
2026-05-16 01:34:20 -04:00
|
|
|
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
2026-05-15 18:56:17 -04:00
|
|
|
room.removeListener(RoomEvent.Receipt, onReceipt);
|
2026-05-22 11:16:11 -04:00
|
|
|
(room as any).removeListener(RoomMemberEvent.Membership, onMembership);
|
2026-05-15 18:56:17 -04:00
|
|
|
};
|
|
|
|
|
}, [room, myUserId]);
|
|
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
|
}
|