Skip to content

feat: thread paginator and manager revamp - #1888

Merged
isekovanic merged 19 commits into
release-v10from
feat/thread-paginator
Sep 30, 2026
Merged

isekovanic merged 19 commits into
release-v10from
feat/thread-paginator

Conversation

@isekovanic

@isekovanic isekovanic commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

CLA

  • I have signed the Stream CLA (required).
  • Code changes are tested

Linear ticket: https://linear.app/stream/issue/REACT-1187/integrate-entitystore-and-paginators-to-thread-manager

Description of the changes, What, Why and How?

client.threads now has one store of live Thread instances, and the thread list is a normal paginator on top of it, the same setup as channel lists.

threadsById was built from the thread list, so it only knew about listed threads. Anything that resolves "the live thread for this id" missed a thread that was open but not listed: hard deletes, offline replay, connection recovery, the composer's submit target. UI SDKs worked around it by pushing opened threads into the list.

Changes:

  • threadStore (EntityStore<Thread>) is the only store. It holds every live thread and counts who holds it. There are two holders:

    • the list: ThreadPaginator builds a StoreBackedItemIndex over the store;
    • REGISTERED_HOLDER: an opened thread (ensure() or its first activate()).

    One instance per id, always.

  • client.threads.paginator (ThreadPaginator):

    • keeps server order, with no client-side sort;
    • resolves every queried thread to the stored instance, and rehydrates it if it's stale and not active;
    • reload() swaps the list in place and is also the first load. A failed reload keeps the list, same as master.
  • client.threads.ensure({ channel, parentMessage }) is how UI SDKs open a thread. It returns the stored thread or builds one, and registers it either way, so a list query landing before activate() can't create a duplicate. A thread it builds starts stale and loads once, on first open.

  • Opened threads stay registered for the session. deactivate() only flips active, like channels. A recovery pass marks inactive registered threads stale, and they reload on their next activate().

  • Teardown follows the channel. There's one local pendingDisposal listener per channel. When the channel is disposed (deleted, user removed, disconnect), both its listed and opened threads are released. No extra event handlers.

  • Thread.hydrateState: with reconcile it merges, as reload() does. Without it, as on the list path, it replaces the replies, as on master. This fixes stale and deleted replies coming back after a reconnect.

  • EntityStore gets isHeldBy(id, holder) and values().

  • Composer, message operations, offline queue and recovery now resolve threads through client.threads.get(id).

Also fixed here:

  • Editing a thread reply always failed with 400. The update payload was built with localMessageToNewMessagePayload, so it sent server-owned fields such as type. It now uses toUpdatedMessagePayload. Regression from refactor: unify message operation apis #1882.
  • A channel could vanish from the channel list after login with a fresh offline DB. A first page loaded as a keepPreviousItems refresh kept a stale hasMoreHead: true, so a channel that moved to the top went into a hidden interval. postQueryReconcile now anchors a first page's hasMoreHead from the start offset.

Changelog

  • Breaking: client.threads.threadsById removed. Use client.threads.get(id) to resolve a thread and client.threads.paginator.getItem(id) for list membership.
  • Breaking: the thread list moved to client.threads.paginator (ThreadPaginator). state.threads, state.pagination and state.ready are removed; read paginator.state instead.
  • Breaking: client.threads.loadNextPage() and client.threads.queryThreads() removed; use paginator.toTail() and client.queryThreadsAndHydrate().
  • Added client.threads.ensure({ channel, parentMessage }) for opening a thread with one instance per id.
  • Opened threads stay registered and live for the session, whether or not the list holds them.
  • Fixed thread-reply edits failing with 400.
  • Fixed channels vanishing from the channel list after login with a fresh offline DB.
  • Fixed stale and deleted replies reappearing on a closed listed thread after a reconnect.

@isekovanic

Copy link
Copy Markdown
Contributor Author

Open question with some context: Initially, thread_manager and the thread list in general did not support any explicit passing of sort/filters because of the fact that they did not exist in the initial implementation (server side) 2 years ago. This has now obviously changed.

Shall we maybe take a similar approach to ChannelManager and allow ThreadManager to actually hold multiple paginators ? Otherwise we can keep the current approach as well and build on top of that

@@ -0,0 +1,154 @@
# stream-chat-react: moving to the new thread manager and `ThreadPaginator`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll update this with any further changes to the PR and we can remove it once it's no longer needed (purely here for stream-chat-react migration guidelines)

Comment thread src/thread_manager.ts Outdated
Comment thread src/thread_manager.ts
@isekovanic
isekovanic merged commit d1562aa into release-v10 Sep 30, 2026
4 checks passed
@isekovanic
isekovanic deleted the feat/thread-paginator branch September 30, 2026 14:29
github-actions Bot pushed a commit that referenced this pull request Sep 30, 2026
## [10.0.0-rc.16](v10.0.0-rc.15...v10.0.0-rc.16) (2026-09-30)

### ⚠ BREAKING CHANGES

* thread paginator and manager revamp (#1888)

### Bug Fixes

* channel paginator merging bugs ([#1894](#1894)) ([dbbbd75](dbbbd75)), closes [#1865](#1865)

### Features

* thread paginator and manager revamp ([#1888](#1888)) ([d1562aa](d1562aa))
@stream-ci-bot

Copy link
Copy Markdown

🎉 This PR is included in version 10.0.0-rc.16 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants