fix(threads): review-wave fixes — decryption re-render, receipt dedupe, chip perf
Two-reviewer audit of the thread stack; confirmed findings fixed: - ThreadTimeline: wrap encrypted events in EncryptedContent so a live-arriving E2EE reply re-renders when its key decrypts (decryption emits neither RoomEvent.Timeline nor ThreadEvent.Update — previously stuck at "Unable to decrypt"). - ThreadPanel: mark-read deduped on the latest event id (RoomEvent.Timeline re-emits per backfilled event/edit/reaction; previously up to N receipt POSTs per panel open) + rejection handled with retry. - RoomTimeline: ThreadSummary chips now mount only for events carrying thread data (each chip holds a room-level listener; one per rendered message would blow the SDK's 100-listener emitter cap) with a single room-level ThreadEvent.New tick for new-thread liveness. - useThreadPendingEvents: keep a sent reply visible through the /send-response→ /sync window (was flashing out of the pending strip before landing). - ThreadTimeline: reseed the window on RoomEvent.TimelineReset (gappy sync left a detached timeline). Documented-acceptable (reviewer-noted): thread typing shows as room typing (no per-thread typing in the spec; Element matches), thread panel + members drawer can be open together, scheduled-send is thread-unaware but unreachable there. Gates: tsc clean, eslint 0 errors, build OK, 616/617 tests (1 IDB skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -95,10 +95,32 @@ export function ThreadPanel({ room, threadId, requestClose }: ThreadPanelProps)
|
||||
);
|
||||
|
||||
// Mark the thread read when the panel is open and on each new thread event.
|
||||
// Deduped on the latest event id: RoomEvent.Timeline re-emits per event during
|
||||
// backfill and for every edit/reaction, and sendReadReceipt POSTs
|
||||
// unconditionally — without the guard, opening a thread with N replies would
|
||||
// fire up to N receipt requests at the same event.
|
||||
const lastReadEventIdRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
lastReadEventIdRef.current = undefined;
|
||||
if (!thread) return undefined;
|
||||
const markRead = () => {
|
||||
markThreadAsRead(mx, thread, privateReadReceipts);
|
||||
const events = thread.liveTimeline.getEvents();
|
||||
let latestId: string | undefined;
|
||||
for (let i = events.length - 1; i >= 0; i -= 1) {
|
||||
const evt = events[i];
|
||||
if (evt && !evt.isSending()) {
|
||||
latestId = evt.getId() ?? undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!latestId || latestId === lastReadEventIdRef.current) return;
|
||||
lastReadEventIdRef.current = latestId;
|
||||
markThreadAsRead(mx, thread, privateReadReceipts).catch(() => {
|
||||
// Allow a retry on the next event if the receipt POST failed.
|
||||
if (lastReadEventIdRef.current === latestId) {
|
||||
lastReadEventIdRef.current = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
markRead();
|
||||
thread.on(ThreadEvent.NewReply, markRead);
|
||||
|
||||
Reference in New Issue
Block a user