fix(threads): thread drawer empty with classic sync#343
Merged
Conversation
With classic sync, RoomViewHeader creates Thread objects via room.createThread(id, rootEvent, [], false) — passing no initialEvents. This means thread.events starts as just [rootEvent] (or empty). Two bugs resulted: 1. The gate `if (fromThread.length > 0)` blocked the live-timeline fallback. Since thread.events = [rootEvent], length was 1 (truthy), but after filtering the root out the array was empty — yielding zero replies even though the events were present in the main room timeline. Fix: compute the filtered array first, then gate on its length so the fallback is reached when the thread object has no actual replies yet. 2. Even after #1, subsequent renders and read-receipt logic used thread.events (empty) rather than the live timeline. Fix: add a mount-time useEffect that backfills matching events from the unfiltered live timeline into the Thread object via thread.addEvents() so the authoritative source is populated for future interactions.
Extract the replyEvents IIFE into a module-level function so the fix-first- then-check-length logic can be exercised in isolated unit tests without mounting the full component. Tests cover: - Returns thread.events (minus root) when the Thread object has replies. - Excludes reactions (RelationType.Annotation) from the results. - Classic-sync regression: thread.events = [rootEvent] => falls back to the live room timeline (the bug was that length-1 thread blocked the fallback). - Falls back to live timeline when getThread() returns null. - Filters out events belonging to a different thread. - Returns [] when neither source has relevant events.
Contributor
Author
|
Tested as working in the PR Preview - with classic sync enabled, threads now populate correctly. |
dozro
approved these changes
Mar 17, 2026
Contributor
Deploying with
|
| Status | Preview URL | Commit | Alias | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! | https://pr-343-sable.raspy-dream-bb1d.workers.dev | 86bdb80 | pr-343 |
Wed, 18 Mar 2026 13:47:35 GMT |
7w1
approved these changes
Mar 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When using classic sync (non-sliding-sync), opening a thread drawer showed no messages even though replies existed in the room.
Root cause: Classic sync calls
room.createThread(id, root, [], false)with an emptyinitialEventsarray. The SDK creates the Thread object with only the root event inthread.events. The old code checkedif (fromThread.length > 0)on the unfiltered array — which was truthy (length=1, the root) — and returned the filtered result (empty after removing the root), bypassing the live-timeline fallback entirely.Fix
Filter first, then check: Compute
filteredFromThreadand only skip the fallback if that array is non-empty. If the Thread object exists but has no replies (classic-sync case), fall through to scangetUnfilteredTimelineSet().getLiveTimeline().The logic is extracted into an exported
getThreadReplyEvents(room, threadRootId)helper for testability.A backfill
useEffectis also added: when the drawer opens and the Thread object is empty, it callsthread.addEvents()with the live-timeline events so the authoritative Thread source is populated for subsequent renders (receipts, reactions, etc.).Tests
ThreadDrawer.test.ts(new file) — 6 tests covering: