Skip to content

fix: odd edge cases for channel list recovery - #1966

Merged
isekovanic merged 7 commits into
release-v10from
perf/reconnection-reconciliation-perf-updates
Oct 30, 2026
Merged

isekovanic merged 7 commits into
release-v10from
perf/reconnection-reconciliation-perf-updates

Conversation

@isekovanic

@isekovanic isekovanic commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Ticket: https://linear.app/stream/issue/REACT-1476/fix-odd-edge-cases-for-channel-list-recovery

CLA

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

Description of the changes, What, Why and How?

Fixes to the reconnect recovery of channel lists (ChannelPaginator.recoverLoadedItems), one per commit. The first four came up while testing the recovery on device, the last three in review. The last one is in BasePaginator and also covers pull-to-refresh.

Hold list pagination while a recovery runs

The recovery's first page is a refresh, which resets the list's offset to 0, and the later pages use a forced query shape that never commits an offset. A next page requested while the recovery ran (the list end reached) therefore asked for offset: 0, fetched page 1 again and placed it as a tail page. A recovery page landing could also clear that query's isLoading.

executeQuery now refuses tailward and headward queries while the recovery runs. A refresh still goes through. A refused next page is queried once after the recovery ends, because the list end does not ask again when the recovery changed nothing.

Keep the loaded channels past the range a recovery re-queried

The recovery re-queries at most 150 channels, but it removed every loaded channel no page returned, including all of those past 150. With the list scrolled past 150, the rows on screen were removed, the list jumped and remounted rows, and only one page loaded back.

A channel no page returned is now removed only when it sorts above the last channel the server returned. Channels below it were not re-queried and stay as they are. When a page comes back shorter than requested and shorter than a page, the pages reached the end of the list, so every loaded channel not returned is removed, as before. A channel past the re-queried range that was deleted while away stays until a live event or a later query covers it.

Wait for the list's in-flight query before recovering

The recovery's pages go through executeQuery, which refuses to run while the list is loading. With a next page in flight at reconnect, the first recovery page was refused and the whole recovery was skipped, so deleted, left, hidden and archived channels stayed in the list. The recovery now waits for that query to settle first. The ChannelManager test that asserted the skip now asserts the wait.

Continue the shown list with later recovery pages

A later recovery page is queried at a caller's shape, so it starts an interval of its own and merges only with intervals it overlaps. A page that shared no channel with the list, for example after several channels above it were deleted, became a separate interval, and the loop made it the shown one. The list then showed only that page. Recovery pages start where the previous one ended, so they are now placed as a continuation of the shown interval.

Let a new recovery replace a running one

Every reconnect starts a recovery of each list, and nothing stopped two from running at once. The recovery's pages are silent, so they never publish isLoading, and a second recovery did not wait for the first. The two loops interleaved their pages and shared _lastPageCids, and the first one to end opened pagination again while the other was still running.

A call while a recovery runs now returns right away, and the running pass stops once its page in flight lands and starts over from the first page. The pages it already fetched were watched on a connection that is gone, so they have to be queried again. The loop has no cap, so the pass that starts after the last reconnect always runs to the end.

Hand the list position back when a recovery stops early

The first recovery page resets offset to 0 and no recovery page commits one, so only the end of the recovery set it back. A failed page, or a pass replaced as above, returned before that and left offset at 0 with the whole list loaded. The next pages then fetched channels the list already had, one page at a time. The recovery now sets offset to the number of loaded channels whenever it returns early.

Restore the position after a failed refresh

A keepPreviousItems refresh (pull-to-refresh in the RN SDK, or the deferred refresh of a list loaded from the offline cache) resets the position before its request, because getNextQueryShape() reads it from state. When the request failed, no page was placed and nothing wrote the position back, so it stayed at the first page, with the same result as above. prepareQuery now passes the position it reset along with the query (positionBeforeReset), and placeQueryPage puts it back when there is no page to place. A refresh that succeeds is unchanged.

Testing

  • Each fix has a unit test that fails without it.
  • A randomized run of 300 reconnects with deletions, moves and new channels went from 12 collapsed lists to none, with no wrong channel or order within the re-queried range.
  • On an Android emulator with a release build of the RN SampleApp against the real backend:
    • 90 test channels with deletes, leaves, hides and archives at the top, middle and bottom of the loaded range, plus one channel that got a message, come back exactly right in the paginator state and on screen;
    • a next page requested mid-recovery is held and loaded once the recovery ends;
    • with 210 channels loaded and the list scrolled near row 200, the recovery keeps all 210 channels and the rows on screen.
  • The in-flight query case and the last three fixes are covered by the unit tests only.
  • tsc, types:tests, eslint and prettier on the changed files, and yarn test (4833 tests) pass.

Changelog

  • Fixed a next page requested during a reconnect recovery fetching the first page again.
  • The reconnect recovery no longer removes loaded channels past the 150 it re-queries, so a list scrolled far down keeps its rows.
  • The reconnect recovery now runs when the list was loading a page at reconnect, instead of being skipped.
  • Fixed a reconnect recovery collapsing the list to one page when a later page shared no channel with it.
  • A reconnect while a recovery runs now restarts it, instead of running two recoveries at once.
  • Fixed the next pages after a failed reconnect recovery fetching channels the list already had.
  • Fixed a failed keepPreviousItems refresh leaving the list position at the first page.

@MartinCupela

Copy link
Copy Markdown
Contributor

The four fixes look right to me (read against the code, not run). One gap and two nits:

1. Two recoveries of the same list can overlap, and the first one's finally re-opens paging while the second runs.

  • ConnectionRecoveryManager starts recoverChannelLists() detached on each reconnect edge (ConnectionRecoveryManager.ts:100, :132). Unlike recoverActiveChannels, it has no isRecovering guard.
  • The recovery's pages are silent with keepPreviousItems, so they never publish isLoading: true (BasePaginator.ts:3087-3090). A second recoverLoadedItems() therefore doesn't wait in waitForQueryToSettle. It starts at once and runs its own offset loop next to the first.
  • When the first finishes, its finally sets _isRecoveringLoadedItems = false while the second is still paging. executeQuery then lets tailward queries through again, which is the case the first commit fixes.

A quick double reconnect on a flaky network is enough to hit it. Making recoverLoadedItems single-flight would cover it: keep the running promise, and have a call that arrives meanwhile await it and then run once more, since the connection changed again. That would also keep the two loops from interleaving their pages.

2. Nit: ChannelManager.recover's doc comment is stale. It says recovery "is not silent: the isLoading transition guards against an overlapping query" (ChannelManager.ts:1003-1011). The pages are silent, and the guard is now _isRecoveringLoadedItems.

3. Nit: the end-of-list check assumes the server returns a full pageSize. reachedEnd is set when a page is shorter than min(this.pageSize, requested). If an app sets pageSize above the server's maximum limit for queryChannels, every full page from the server looks short. Recovery then removes all loaded channels it didn't get back, which is the behaviour the second commit replaces. That only happens with an unusual config, but comparing against the limit the server actually applied, or treating only an empty page as the end, would avoid it.

@isekovanic

Copy link
Copy Markdown
Contributor Author

Heyo @MartinCupela , thanks for the review !

  1. Really nice catch, fixed in 39e4481. However I decided to fix it by superseding the query rather than queueing. The reason for this is because if we wait for the current one to finish, we could potentially do multiple queryChannels calls for nothing.

  2. Fixed

  3. I’d keep this one as it is. The end rule is the same one BasePaginator uses for normal paging, so a page is the tail when requestedPageSize > items.length. With pageSize above the server max, the list already stops after its first page and never loads enough for the recovery to remove anything past the range (this is how we are handling this already). Treating only an empty page as the end breaks short lists. Say A, B, C are loaded, C gets deleted and a new N comes in. The server returns N, A, B, the loop ends without an empty page and C stays. And comparing against the limit the server applied would mean hardcoding the cap. Is this okay from your pov ?

@MartinCupela MartinCupela left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @isekovanic, all three look good to me.

  1. Superseding is better than queueing: the earlier pass's pages were watched on a connection that's gone, so starting over is right. Restoring the position when a recovery stops early or a refresh fails (8aade93b, f7886ab6) covers the edges I'd have worried about.
  2. 👍
  3. Agreed, keep it. It's the same end rule normal paging uses, so a list with pageSize above the server max never loads past what recovery covers. Your A, B, C / N example shows why "only an empty page is the end" would be wrong.

One small nit, not blocking: a recovery requested while one runs now returns at once, before the restarted pass finishes. So ConnectionRecoveryManager.recover() ("Runs a full recovery now") can resolve while the channel lists are still recovering. Returning the running promise from recoverLoadedItems would let callers await the real end. On reconnect it doesn't matter, since that path runs detached.

@isekovanic
isekovanic merged commit dbc2468 into release-v10 Oct 30, 2026
5 of 6 checks passed
@isekovanic
isekovanic deleted the perf/reconnection-reconciliation-perf-updates branch October 30, 2026 12:05
github-actions Bot pushed a commit that referenced this pull request Oct 30, 2026
## [10.0.0-rc.27](v10.0.0-rc.26...v10.0.0-rc.27) (2026-10-30)

### Bug Fixes

* consolidate master and v10 ([#1957](#1957)) ([802a956](802a956))
* handle tokenProvider rejection during reconnect token refresh ([#1963](#1963)) ([e22d218](e22d218))
* narrow channel.state.typing to typing.start events ([#1960](#1960)) ([acb2345](acb2345)), closes [#1920](#1920)
* odd edge cases for channel list recovery ([#1966](#1966)) ([dbc2468](dbc2468))
* pass full request options to hydration query ([#1959](#1959)) ([489774b](489774b))

### Features

* regenerate from latest api spec; docs sync ([#1969](#1969)) ([ae1e86a](ae1e86a))
@stream-ci-bot

Copy link
Copy Markdown

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

The release is available on:

Your semantic-release bot 📦🚀

github-actions Bot pushed a commit that referenced this pull request Nov 6, 2026
## [10.0.0](v9.55.0...v10.0.0) (2026-11-06)

### ⚠ BREAKING CHANGES

* release v10 from master as latest (#2004)
* `LocalAttachmentMetadata` requires only `id`. `file` and `uploadState` are
optional, so code that reads them from a `LocalAttachment` must handle `undefined`. A `file` must
come with its `uploadState`: `localMetadata: { id, file }` no longer compiles, and an attachment
in that shape from untyped code is no longer sent. An attachment with neither field (a custom type
or `giphy`) is sent as it is, takes no upload slot, and counts as content in `hasSendableData`.
* CustomCommandData is no longer exported. Delete its augmentations and imports. A leftover augmentation still compiles and has no effect, so search for the name rather than relying on tsc. An app that wants a closed set of command names declares its own union. See ai-docs/v10-breaking-changes.md#custom-data-augmentation.
* the client no longer dispatches capabilities.changed and
the name is no longer an EventType. Subscribe to channel.state instead:
channel.state.subscribeWithSelector(({ ownCapabilities }) => ({ ownCapabilities }), fn).
See ai-docs/v10-breaking-changes.md#capabilities-changed-removed.
* AttachmentManager.uploadAttachment() is removed. Retry a failed upload, or upload an attachment you built yourself such as a voice recording, with `attachmentManager.uploadFile(attachment)`. It keeps the attachment's id, fields and position, and runs the pre- and post-upload middleware, which uploadAttachment() skipped. uploadFile() does not check `isUploadEnabled`, so check it first to skip the upload as uploadAttachment() did. uploadFile() always resolves to an attachment: where a result was checked for `undefined`, check `result.localMetadata.uploadState` instead. See ai-docs/v10-breaking-changes.md#upload-attachment-removed.
* the message composer installs the command middlewares by default: createActiveCommandGuardMiddleware and createCommandStringExtractionMiddleware in the text composer chain, createCommandInjectionMiddleware in the composition chain and createDraftCommandInjectionMiddleware in the draft composition chain. Typing `/giphy cats` activates giphy and leaves `cats` in the text, and a command picked with `textComposer.setCommand()` is sent with its name. Delete your own installs of these middlewares. A UI without a command chip removes all four by id: `stream-io/text-composer/active-command-guard` and `stream-io/text-composer/command-string-extraction` from `textComposer.middlewareExecutor`, `stream-io/message-composer-middleware/command-string-injection` from `compositionMiddlewareExecutor` and `stream-io/message-composer-middleware/draft-command-string-injection` from `draftCompositionMiddlewareExecutor`. See ai-docs/v10-breaking-changes.md#composer-command-middlewares-default.
* Channel.watch() resolves with the channel instance to use (the channel, or the instance that superseded it) instead of the server response.
- watch() sends no query for a channel that is already watched and loaded (watchStatus Watching, initialized, and messagePaginator.hasResults) when it passes no options or the same options as the last successful watch() during the current watch; other options send a query. It no longer refreshes a watched channel: use channel.reload(), and load other messages through channel.messagePaginator (jumpToMessage(), toTail(), toHead()).
- A watch() while another with the same options is in flight joins it instead of sending a request.
- Aborting requestOptions.signal ends only that caller's wait; the request is cancelled once every caller waiting for it has aborted.
- Channel.ensureWatched() is removed (v10 release candidates only); call watch().
```

## Changelog

- `Channel.watch()` resolves with the channel and sends no query for a
channel already watched and loaded, unless it asks for options the
current watch was not sent with. `Channel.ensureWatched()` is removed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
* remove messageSetToAddToIfDoesNotExist and let the message paginator place every page (#1926)
* open api hot fixes (#1927)
* `StreamChatOptions` and `client.options` are removed.
`new StreamChat(key, config)` and `StreamChat.getInstance(key, config)` take
the configuration tree (`DeepPartial<InstanceConfigTree>`) as their second
argument; the `config` wrapper is dropped.
Option moves:
- `allowServerSideConnect`, `axiosRequestConfig`, `baseURL`, `browser`,
  `persistUserOnConnectionFailure` -> `client.<same name>`
- `notifications` -> `client.notificationManager`
- top-level axios fields (`timeout`, `httpsAgent`, ...) ->
  `client.axiosRequestConfig.<key>`
Example: `new StreamChat(key, { client: { allowServerSideConnect: true } })`.
`client.persistUserOnConnectionFailure` is no longer a field (use
`client.config.setConfig('client', { persistUserOnConnectionFailure })`);
`client.browser` and `client.node` are read-only getters.
`baseURL`, `axiosRequestConfig` and `notificationManager` are read at
construction, and the tree copies `axiosRequestConfig` instead of keeping
your object. See v9-to-v10-migration-guide-client-construction.md.
* client.activeChannels is removed; use client.channelManager.get(cid) and client.channelManager.values().
* client.hydrateActiveChannels() is renamed to client.hydrateChannels().
* client.getChannelById() and client.getChannelByMembers() are removed; use client.channel() or client.channelManager.ensure({ type, id?, data? }).
* thread.deactivate() is removed; thread.activate() returns the function that ends the activation.
* channel._disconnect() is renamed to channel.disconnect().
* channel.disconnected is removed; read the read-only channel.pendingDisposal instead, which only channel.disconnect() sets.
* when a channel created from members gets a cid another stored instance already holds, the stored instance stays, as in v9, and the new one is marked channel.supersededBy; if the new one is open and the stored one isn't, its unsent composition moves to the stored one, so UIs holding the new instance should switch to channel.supersededBy.
* channel.initialized is set by query() and create() too, not only by watch().
* channel search results are not watched; watch a result when it is opened.
* MessageSearchSource no longer watches the channels it fetches for its results; watch the channel when a result is opened, e.g. with jumpToMessage(id, { watchChannel: true }).
* adopt TS generator changes (#1896)
* reenable WS fallback (#1889)
* thread paginator and manager revamp (#1888)
* **MessageDeliveryReporter:** The `isLocalUnreadCountEnabled` client option is
removed. Use `channel.readEvents.localUnreadCountEnabled` in the
configuration service instead (see above).
* brand server-sent timestamps as TimestampNS and ship the Date guard (#1884)
* `connection.changed` is removed. Connectivity is published as two state stores
and nothing else: `client.networkConnection.state` for the device and `client.wsConnection.state`
for this client's socket. The event was silent on `closeConnection()` and two error paths, and
held a drop for five seconds; the stores are written on every transition and publish immediately.
* `client.wsConnection` is now a `WSConnection` wrapper rather than the
`StableWSConnection` itself, and is never null. The live socket is `client.wsConnection.connection`,
replaced on every connect; read `isHealthy`, `isConnecting`, `connect()` and `disconnect()` from the
wrapper. `StableWSConnection` is constructed with `{ wsConnection }` instead of `{ client }`.
* `client.defaultWSTimeout` is removed, along with the `WebSocketImpl` and
`wsUrlParams` client options. They move to the socket's configuration as `connectTimeoutMs`,
`webSocketImpl` and `urlParams`, set with `client.config.set({ client: { wsConnection: { … } } })`.
Unlike the fields and options they replace, these survive a reconnect.
* `ThreadManagerState.lastConnectionDropAt` is removed. Read
`client.wsConnection.state.lastUnhealthyAt`, which is written on every status transition,
including the `disconnect()` path the old event was silent about.
* requests that watch a channel or subscribe to presence now wait for a WebSocket
connection id instead of silently returning unwatched data. The gate is in `ApiClient._doRequest`,
so it covers every endpoint carrying `watch` or `presence`, plus `stopWatchingChannel` and
`longPoll`. With no socket open and none being established the request rejects with "No connection
id is available"; an explicit `watch: false` is still honoured, and a caller's `AbortSignal`
abandons the wait. Test fixtures that fake a connected user without a live socket will now throw.
* a UI that renders a "connection lost" banner must hold the drop itself. The
socket's store publishes drops the moment they happen, where the old event delayed them by five
seconds. `client.wsConnection.config.offlineNotificationDisplayDelayMs` (5s) is the shared value to
wait for; nothing in this package acts on it.
* `connection.recovered` is no longer dispatched when the socket drops while a
recovery is running, because every reload in it can have failed. Work keyed off that event will
correctly stop running for recoveries that recovered nothing.
* openapi related clean up (#1870)
* **SearchController:** remove InternalSearchControllerState (#1866)
* remove response date transform (#1849)
* `client.instanceConfigurationService` is replaced by `client.config`,
and the exported class `InstanceConfigurationService` is renamed to
`InstanceConfigurationRegistry`.
* `client.setInstanceConfigurationFunction()` is removed. Register
setup functions with `client.config.setSetupFunction(key, fn)`.
`client.setMessageComposerSetupFunction()` is unchanged.
* the per-class setup types are replaced by generic ones keyed by
instance. Removed: `ChannelSetupFunction`, `ChannelSetupState`,
`ChannelTearDownFunction`, `MessageComposerSetupFunction`, `MessageComposerSetupState`,
`MessageComposerTearDownFunction`, `StreamChatSetupFunction`, `StreamChatSetupState`,
`StreamChatTearDownFunction`, `ThreadSetupFunction`, `ThreadSetupState`,
`ThreadTearDownFunction`, `SetInstanceConfigurationFunctions`,
`SetInstanceConfigurationServiceStates` and `SetupFnOf`. Use `InstanceSetupFunction`,
`InstanceSetupState`, `InstanceSetupTearDownFunction` and `InstanceSetupKey`.
* `ChannelInstanceConfig` is renamed to `ChannelConfig` and
`ThreadInstanceConfig` to `ThreadConfig`.
* `Channel.getConfig()` is removed. Read the server's channel-type
configuration from `channel.serverConfig`, or the resolved configuration from
`channel.config`.
* `client.configs` is renamed to `client.channelServerConfigs` and
`client.configsStore` to `client.channelServerConfigsStore`. Keys are still cids, so
lookups translate directly. Prefer `channel.serverConfig`.
* `channel.config.commands` is renamed to
`channel.config.availableCommands`. The server owns the list, so it is read-only and
absent from the declarative configuration tree.
* `applyCommandValidatorOverride` is no longer exported. Command
validation is configured through the composer's configuration.
* `MessageComposerConfig` gains a required `polls`;
`AttachmentManagerConfig` gains required `enabled` and `customCdn`;
`LocationComposerConfig` gains a required `minShareDurationMs`. A config object
built literally must supply them.
* `linkPreviews.enabled` now defaults to `true`, where it defaulted to
`false`. It is ANDed with the channel type's `url_enrichment`, so `true` means "no
opinion, let the server decide"; the old default double-gated the feature and kept it
off even where the server had enabled it.
* server flags gate the features they describe, ANDed with the
registered value rather than the client value winning: `typingEvents` with
`typing_events`, `readEvents` with `read_events`, `attachments` with `uploads`,
`polls` with `polls`, `location` with `shared_locations`, and `linkPreviews` with
`url_enrichment`. Code that set a flag to `true` and assumed it took effect may now
find the feature off.
* setters no longer discard a write when the server is masking the
field. Previously the setter compared against the effective value, so the write was
dropped and the earlier request was honoured once the server relented. Affects
`linkPreviewsManager.enabled`, `textComposer.enabled`, `textComposer.maxLengthOnEdit`,
`textComposer.maxLengthOnSend` and `attachmentManager.maxNumberOfFilesPerMessage`.
* remove hand-written types and API wrappers (#1836)
* **i18n:** `Streami18n.init()` now rejects when i18next fails to
initialize, where it previously logged and resolved. Callers that ignore
the returned promise will see an unhandled rejection. The instance
remains usable in its degraded English form either way.
`Streami18n.brand` is removed; nothing in either UI SDK read it, and a
custom integration that recognized instances through it should compare
against its own marker or accept the instance it was handed.
* **i18n:** share the translation runtime as stream-chat/i18n (#1830)
* `ChannelManager` keeps its name but is a different class
table

Co-authored-by: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com>
* paginator-based message storage replaces
channel.state.messages and channel.state.last_message_at. The message
list, thread replies and pinned messages are now owned by
channel.messagePaginator, thread.messagePaginator and
channel.pinnedMessagesPaginator. OpenAPI-generated clients replace the
hand-written request/response types. See
docs/breaking-changes-v14-v15.md for the full migration guide.

### Bug Fixes

* accept poll.update data without id ([#1997](#1997)) ([c5863a9](c5863a9))
* add docs snippets; fix open api issues ([#1913](#1913)) ([53505e7](53505e7))
* add messages received after truncate to state ([#1950](#1950)) ([b178379](b178379))
* address code issues found while reviewing v10 source comments ([#1922](#1922)) ([2fc319e](2fc319e))
* adjust paginator offset when items are removed ([#1842](#1842)) ([61b5a1f](61b5a1f))
* apply thread reloads and queries to the registered thread ([#1929](#1929)) ([acb33c4](acb33c4)), closes [#1913](#1913)
* avoid types from NodeJS namespace ([#1947](#1947)) ([68fb042](68fb042))
* bring back reaction apis to channel ([#1906](#1906)) ([d419fe3](d419fe3)), closes [#1897](#1897) [#1897](#1897)
* channel list reconciliation ([#1942](#1942)) ([f7f9ec8](f7f9ec8))
* channel paginator merging bugs ([#1894](#1894)) ([dbbbd75](dbbbd75)), closes [#1865](#1865)
* consolidate master and v10 ([#1957](#1957)) ([802a956](802a956))
* count hidden messages on each side of a jump page ([#1940](#1940)) ([487ac3d](487ac3d))
* **deps:** bump [@stream-io](https://github.com/stream-io) toolkit packages to the .mjs/.cjs builds ([#1971](#1971)) ([aa97cf4](aa97cf4)), closes [GetStream/js-toolkit#58](https://github.com/GetStream/js-toolkit/issues/58)
* dispatch connection.recovered on reconnect when recovery is disabled ([#1994](#1994)) ([13bb533](13bb533))
* docs fixes ([#1949](#1949)) ([4c3e46d](4c3e46d))
* drop capabilities.changed; channel.state ownCapabilities is the change signal ([#1992](#1992)) ([6da7713](6da7713))
* drop server-side remnants, add WS interface ([#1819](#1819)) ([39c0c53](39c0c53))
* ensure API errors parsing works even with multiple axios copies ([#1909](#1909)) ([e0cfba9](e0cfba9))
* existing messages retry guards ([#1982](#1982)) ([c7c971f](c7c971f))
* handle tokenProvider rejection during reconnect token refresh ([#1963](#1963)) ([e22d218](e22d218))
* handle user.updated in threads ([#1979](#1979)) ([9064ffc](9064ffc))
* **i18n:** reject from init() and drop the Streami18n brand static ([#1839](#1839)) ([67bb1c7](67bb1c7))
* install the command middlewares in the composer by default ([#1985](#1985)) ([1dc25e2](1dc25e2))
* issues found by the RN e2e tests ([#1996](#1996)) ([f5d1a2d](f5d1a2d))
* jump to thread reply bug ([#1948](#1948)) ([5ec1b07](5ec1b07))
* let an edit clear all custom fields ([#1983](#1983)) ([9437b09](9437b09))
* narrow channel.state.typing to typing.start events ([#1960](#1960)) ([acb2345](acb2345)), closes [#1920](#1920)
* notify channel state subscribers on watcher, ban and member removal events ([#1977](#1977)) ([3a7be78](3a7be78))
* odd edge cases for channel list recovery ([#1966](#1966)) ([dbc2468](dbc2468))
* offline db hydration race conditions ([#1865](#1865)) ([a4587e1](a4587e1))
* open api hot fixes ([#1927](#1927)) ([4fb4b52](4fb4b52))
* optimistic updates for translated messages ([#1975](#1975)) ([91fc290](91fc290))
* **pagination:** count a page set with setItems as loaded, even when empty ([#1995](#1995)) ([bfff3c8](bfff3c8))
* **pagination:** keep loaded items when filterQueryResults empties a later page ([#1986](#1986)) ([78a32c3](78a32c3))
* **paginator:** correct windows filled by hydration rather than by a query ([#1860](#1860)) ([93b6fa0](93b6fa0))
* **paginator:** do not issue the first-page query twice ([#1858](#1858)) ([6462cef](6462cef))
* pass full request options to hydration query ([#1959](#1959)) ([489774b](489774b))
* pending task folding ([#1873](#1873)) ([028f6e8](028f6e8))
* **poll:** keep translations from poll.updated in the poll state ([#1937](#1937)) ([e47b75a](e47b75a)), closes [#1878](#1878)
* post-merge/OAPI-reconciliation adjustments ([#1812](#1812)) ([5073c67](5073c67))
* reconnect past connection timeout ([#1874](#1874)) ([2004a83](2004a83)), closes [#1760](#1760)
* regenerate api layer (fix generated filter types) ([#1827](#1827)) ([e540d88](e540d88))
* remove CustomCommandData ([#1989](#1989)) ([f737d61](f737d61))
* respect predefined filters from db ([#1869](#1869)) ([cbc7707](cbc7707))
* restore composer custom data from drafts and edits, keep custom on edit ([#1978](#1978)) ([51ddcad](51ddcad))
* **search:** match message text by word prefix in MessageSearchSource ([#1987](#1987)) ([ef58ef5](ef58ef5))
* send the read request regardless of read receipt privacy settings in v10 ([#1855](#1855)) ([3b24387](3b24387))
* set thread cursor when initied from setItems ([#1898](#1898)) ([63bef43](63bef43))
* signal api ([#1828](#1828)) ([9032b1a](9032b1a))
* stability and perf fixes ([#1910](#1910)) ([080af2c](080af2c))
* unread snapshot count, pinned replies, joined and invite filters, read events check ([#1981](#1981)) ([1247d10](1247d10))
* v10 in depth review hotfixes ([#1920](#1920)) ([98a298d](98a298d))
* v10 regressions found in side-by-side QA ([#1965](#1965)) ([b87d598](b87d598))

### Features

* add instance configuration service ([#1831](#1831)) ([434966c](434966c))
* add message/channel paginator stack with thread minimal-init and state stores ([#1795](#1795)) ([62f0507](62f0507))
* adopt TS generator changes ([#1896](#1896)) ([82af6de](82af6de))
* brand server-sent timestamps as TimestampNS and ship the Date guard ([#1884](#1884)) ([e51198a](e51198a)), closes [GetStream/chat#17356](https://github.com/GetStream/chat/issues/17356) [GetStream/chat#17460](https://github.com/GetStream/chat/issues/17460) [#17356](https://github.com/GetStream/stream-chat-js/issues/17356) [GetStream/stream-chat-react#3297](GetStream/stream-chat-react#3297) [GetStream/stream-chat-react-native#3822](GetStream/stream-chat-react-native#3822) [GetStream/chat#17425](https://github.com/GetStream/chat/issues/17425)
* channel state migration ([#1834](#1834)) ([9876add](9876add))
* connection recovery ([#1841](#1841)) ([8ec9a9d](8ec9a9d))
* destructive reconciliation ([#1822](#1822)) ([3c46fbf](3c46fbf))
* establish network connection observer services ([#1859](#1859)) ([4c46949](4c46949))
* generalize entity store in V10 ([#1814](#1814)) ([d066ba0](d066ba0))
* **i18n:** share the translation runtime as stream-chat/i18n ([#1830](#1830)) ([40cf062](40cf062)), closes [GetStream/stream-chat-react#3271](GetStream/stream-chat-react#3271) [GetStream/stream-chat-react-native#3777](GetStream/stream-chat-react-native#3777)
* integrate OpenAPI generated clients ([#1760](#1760)) ([0776bc4](0776bc4))
* let AttachmentManager.uploadFile retry an existing attachment ([#1991](#1991)) ([cf8b1fa](cf8b1fa))
* let integrators send a poll with the message by removing the poll-only middleware ([#1970](#1970)) ([b515849](b515849)), closes [GetStream/stream-chat-react#3365](GetStream/stream-chat-react#3365)
* message pruning ([696f56f](696f56f))
* message pruning ([#1875](#1875)) ([d4d2c6c](d4d2c6c))
* **MessageComposer:** add send and update message API ([#1881](#1881)) ([95b2502](95b2502))
* **MessageComposer:** reference the composer on upload notifications ([#1871](#1871)) ([d520fc1](d520fc1))
* **messageDelivery:** report receipt state from the furthest cursor ([#1883](#1883)) ([79f3ac1](79f3ac1))
* **MessageDeliveryReporter:** mark channel read locally when local read count tracking are enabled ([#1886](#1886)) ([439a018](439a018))
* migrate ChannelPaginatorsOrchestrator to ChannelManager ([#1813](#1813)) ([c694930](c694930))
* move to WS API v2 ([#1833](#1833)) ([d530f43](d530f43))
* **NotificationManager:** add ensureTimeout for callers that cannot know if notification was already triggered ([#1872](#1872)) ([e5d06aa](e5d06aa))
* openapi related clean up ([#1870](#1870)) ([b3fa906](b3fa906))
* optimistic reactions ([#1808](#1808)) ([54740f1](54740f1))
* poll optimistic votes v10 port ([#1904](#1904)) ([143446b](143446b))
* port search improvements ([#1829](#1829)) ([7276299](7276299)), closes [#1823](#1823)
* reenable WS fallback ([#1889](#1889)) ([45841cc](45841cc))
* regenerate from latest api spec; docs sync ([#1969](#1969)) ([ae1e86a](ae1e86a))
* regenerate from latest open api spec ([#1821](#1821)) ([22344ca](22344ca))
* regenerate open api spec + improve tooling ([#1824](#1824)) ([a3198e7](a3198e7))
* release v10 from master as latest ([#2004](#2004)) ([40ead99](40ead99)), closes [#2003](#2003) [#1476](#1476) [#1938](#1938) [#1937](#1937) [#1962](#1962) [#1963](#1963) [#1944](#1944) [#1952](#1952) [#1953](#1953) [#1954](#1954) [#1957](#1957) [#1976](#1976) [#1908](#1908) [#2003](#2003)
* remaining optimistic updates ([#1843](#1843)) ([808b2c8](808b2c8))
* remove hand-written types and API wrappers ([#1836](#1836)) ([dc49a78](dc49a78))
* remove handwritten upload methods; align method names ([#1832](#1832)) ([1a6bdb8](1a6bdb8))
* remove messageSetToAddToIfDoesNotExist and let the message paginator place every page ([#1926](#1926)) ([2b7960d](2b7960d))
* remove response date transform ([#1849](#1849)) ([ef5dcc6](ef5dcc6))
* replace StreamChat.activeChannels with entity item index for loaded channels ([#1901](#1901)) ([ec9178a](ec9178a))
* replace StreamChatOptions with the configuration tree ([#1911](#1911)) ([f05b861](f05b861))
* send and count attachments with nothing to upload ([#1993](#1993)) ([b2dfb8a](b2dfb8a)), closes [ai-docs/v10-breaking-changes.md#non-upload-attachments](https://github.com/ai-docs/v10-breaking-changes.md/issues/non-upload-attachments)
* settle pending attachment uploads ([#1880](#1880)) ([59f5869](59f5869))
* thread paginator and manager revamp ([#1888](#1888)) ([d1562aa](d1562aa))
* update to latest api spec ([461eb24](461eb24))
* update to latest API spec ([#2005](#2005)) ([5668f20](5668f20))
* v10 ([cd3141e](cd3141e))

### Refactors

* call the paginator intervals intervals, not windows ([#1939](#1939)) ([69aa18a](69aa18a))
* **channel:** remove no-op overrides, dead branch and redundant id guards ([#1936](#1936)) ([050953e](050953e)), closes [#1870](#1870)
* **i18n:** move the translation runtime to @stream-io/i18n ([#1861](#1861)) ([45f45bd](45f45bd))
* make Channel.watch() watch a channel only when it is not watched and loaded ([#1943](#1943)) ([3c363cb](3c363cb))
* **SearchController:** remove InternalSearchControllerState ([#1866](#1866)) ([fc64f7f](fc64f7f))
* simplify optimistic updates ([#1897](#1897)) ([8b84f02](8b84f02)), closes [#1882](#1882)
* unify message operation apis ([#1882](#1882)) ([2ed1c75](2ed1c75))

### Performance Improvements

* store publish amplification ([#1876](#1876)) ([d93ee7d](d93ee7d))
@stream-ci-bot

Copy link
Copy Markdown

🎉 This PR is included in version 10.0.0 🎉

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