Skip to content

fix(threads): thread drawer empty with classic sync#343

Merged
7w1 merged 2 commits intodevfrom
fix/thread-classic-sync
Mar 19, 2026
Merged

fix(threads): thread drawer empty with classic sync#343
7w1 merged 2 commits intodevfrom
fix/thread-classic-sync

Conversation

@Just-Insane
Copy link
Contributor

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 empty initialEvents array. The SDK creates the Thread object with only the root event in thread.events. The old code checked if (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 filteredFromThread and 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 scan getUnfilteredTimelineSet().getLiveTimeline().

The logic is extracted into an exported getThreadReplyEvents(room, threadRootId) helper for testability.

A backfill useEffect is also added: when the drawer opens and the Thread object is empty, it calls thread.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:

  • Normal case: Thread object has replies → returns them minus root/reactions
  • Classic-sync regression: Thread has only the root → falls back to live timeline
  • No Thread object at all → falls back to live timeline
  • Cross-thread filtering: ignores events from other threads
  • Empty result when neither source has replies

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.
@Just-Insane Just-Insane requested review from 7w1 and hazre as code owners March 17, 2026 20:24
@Just-Insane
Copy link
Contributor Author

Tested as working in the PR Preview - with classic sync enabled, threads now populate correctly.

@Just-Insane Just-Insane reopened this Mar 18, 2026
@github-actions
Copy link
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

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 7w1 added this pull request to the merge queue Mar 19, 2026
Merged via the queue into dev with commit 84e52ad Mar 19, 2026
19 checks passed
@Just-Insane Just-Insane deleted the fix/thread-classic-sync branch March 19, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants