import { Room, RoomEvent, RoomMemberEvent, MatrixEvent } from 'matrix-js-sdk'; import { useEffect, useState } from 'react'; import { useMatrixClient } from './useMatrixClient'; 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. function nearestRenderableId( liveEvents: MatrixEvent[], eventIndex: Map, evtId: string, ): string | null { const idx = eventIndex.get(evtId) ?? -1; 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; } function computePositions(room: Room, myUserId: string): Map { const map = new Map(); const liveEvents = room.getLiveTimeline().getEvents(); // Build O(1) index once instead of O(T) findIndex per member const eventIndex = new Map(liveEvents.map((e, i) => [e.getId() ?? '', i])); for (const member of room.getJoinedMembers()) { if (member.userId === myUserId) continue; const evtId = room.getEventReadUpTo(member.userId); if (!evtId) continue; const targetId = nearestRenderableId(liveEvents, eventIndex, evtId); if (!targetId) continue; const arr = map.get(targetId); if (arr) arr.push(member.userId); else map.set(targetId, [member.userId]); } return map; } export function useRoomReadPositions(room: Room): Map { const mx = useMatrixClient(); const myUserId = mx.getUserId() ?? ''; const [positions, setPositions] = useState(() => computePositions(room, myUserId)); useEffect(() => { setPositions(computePositions(room, myUserId)); let debounceTimer: ReturnType | null = null; const onReceipt = (): void => { if (debounceTimer !== null) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { setPositions(computePositions(room, myUserId)); debounceTimer = null; }, 150); }; const onMembership = (): void => setPositions(computePositions(room, myUserId)); room.on(RoomEvent.Receipt, onReceipt); (room as any).on(RoomMemberEvent.Membership, onMembership); return () => { if (debounceTimer !== null) clearTimeout(debounceTimer); room.removeListener(RoomEvent.Receipt, onReceipt); (room as any).removeListener(RoomMemberEvent.Membership, onMembership); }; }, [room, myUserId]); return positions; }