diff --git a/AGENTS.md b/AGENTS.md
index 04bbdc424..9609b7790 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -161,7 +161,7 @@ Some components also take component props for their own parts — `Attachment` (
When adding a customizable component: add the slot to `ComponentContextValue` (`src/context/ComponentContext.tsx`), provide a default implementation, and read it through `useComponentContext()`.
-Request customization is not a component prop either: `sendMessageRequest`, `updateMessageRequest`, `deleteMessageRequest` and `markReadRequest` handlers are registered on `client.config` (see "Per-component request-handler props removed" in `ai-docs/ai-migration-v14-v15.md`).
+Request customization is not a component prop either: `sendMessageRequest`, `updateMessageRequest`, `deleteMessageRequest` and `markReadRequest` handlers are registered on `client.config` (see [section 14, "Request handlers"](ai-docs/ai-migration-v14-v15.md#14-request-handlers) of the v14 → v15 migration guide).
### `useStateStore` requires a selector
diff --git a/ai-docs/ai-migration-v14-v15.md b/ai-docs/ai-migration-v14-v15.md
index f51dfc98b..b6da572a7 100644
--- a/ai-docs/ai-migration-v14-v15.md
+++ b/ai-docs/ai-migration-v14-v15.md
@@ -1,26 +1,64 @@
-# stream-chat-react v14 → v15 AI Migration Guide
+# stream-chat-react v14 → v15: AI migration guide
-Execution-only guide for a coding agent migrating a third-party app from `stream-chat-react` v14 to v15. Apply the changes below and fix each compile or runtime break before moving on. For evidence and edge cases, verify against the installed SDK source (see below).
+Execution-only guide for a coding agent migrating a third-party app from `stream-chat-react` v14 to v15. It covers the root package and its subpath entry points (`stream-chat-react/slot-layout`, `/slot-geometry`, `/channel-detail`, `/emojis`, `/ai-components`, `/mp3-encoder` and the `css/*` stylesheets). v15 requires `stream-chat` v10, itself a breaking client release, so upgrade both together; this guide covers the `stream-chat` changes a React app usually hits, and the [stream-chat guide](#companion-documents) covers the rest. The i18n runtime now comes from `@stream-io/i18n` and the state store from `@stream-io/state-store`, both installed as dependencies of this package.
-Every breaking change confirmed in [`ai-docs/v15-breaking-changes.md`](https://github.com/GetStream/stream-chat-react/blob/master/ai-docs/v15-breaking-changes.md) (the audit this guide is written from, `BC-001` to `BC-077`) is covered here. The [index at the end](#index-of-breaking-changes) maps each `BC` entry to its section.
+Every breaking change in [`v15-breaking-changes.md`](https://github.com/GetStream/stream-chat-react/blob/master/ai-docs/v15-breaking-changes.md) (`BC-001` to `BC-083`) is covered here. The [index at the end](#index-of-breaking-changes) maps each `BC` entry to its section.
-## Source of truth
+Verified against `master` @ `1c02ba07f` (`stream-chat-react@0.0.0-development`, after the `v15.0.0-rc.3` tag; `stream-chat` peer `^10.0.0-rc.29`, checked against stream-chat-js `release-v10` @ `fdfcb296`), 2026-11-03.
-Never rely on training knowledge. The v14 surface is heavily represented in pretraining data and will mislead you. Before writing any code, verify against the user's installed packages:
+
-1. `node_modules/stream-chat-react/dist/types/index.d.ts`: the public React type surface (props, override keys, hook signatures). Entry points other than the root have their own folders: `dist/types/plugins/SlotLayout/` for `stream-chat-react/slot-layout`, and so on.
-2. `node_modules/stream-chat/dist/types/index.d.ts`: the core client types. `Channel` (`channel.d.ts`: `messagePaginator`, `messageOperations`, `configState`, `watch`), `ChannelManager.d.ts`, `thread.d.ts`, `pagination/paginators/`, `messageOperations/types.d.ts`.
-3. `node_modules/stream-chat-react/package.json`: the `exports` map and peer dependencies.
+## Rules for the agent
-Rule: before claiming a symbol, prop or method exists, or writing a replacement snippet, grep the installed `.d.ts` and read the current signature. Do not reconstruct it from memory.
+1. **The installed packages are the source of truth, not memory.** The v14 / `stream-chat` v9 surface is heavily represented in training data and will mislead you. Read:
+ - `node_modules/stream-chat-react/dist/types/index.d.ts`: the public React type surface (props, override keys, hook signatures). Entry points other than the root have their own folders: `dist/types/plugins/SlotLayout/` for `stream-chat-react/slot-layout`, and so on.
+ - `node_modules/stream-chat/dist/types/index.d.ts`: the core client types. `Channel` (`channel.d.ts`: `messagePaginator`, `messageOperations`, `configState`, `watch`), `ChannelManager.d.ts`, `thread.d.ts`, `pagination/paginators/`, `messageOperations/types.d.ts`.
+ - `node_modules/stream-chat-react/package.json`: the `exports` map and `peerDependencies`.
+2. **Verify before writing.** Before claiming that a symbol, prop or method exists, or writing a replacement snippet, grep the installed `.d.ts` and read the current signature. Do not reconstruct it from memory.
+3. **Stop and report when a symbol this guide names is missing** from the installed package. Do not invent a fallback or pick a similar name; record it under open decisions in the [report](#report-template).
+4. **Detect before editing.** Run the [detection](#detection) commands and work only on sections with hits. A section with no hits needs no edits.
+5. **Make the smallest edit that fixes the break.** Much of the v14 / `stream-chat` v9 surface carries over (`client.channel()`, `channel.watch()`, ...). When existing app code still type-checks and does what the app needs, keep it. One exception compiles but changed meaning: `channel.markRead()` is now the direct request (it skips the delivery reporter and any `markReadRequest` handler, and throws when read events are off); the v14 behavior is `channel.markReadViaReporter()` ([section 5](#markreadonmount-is-removed-the-list-marks-read)). Do not rewrite code into the shape of a snippet below: the snippets show one valid form, not the only one.
+6. **Do not stop at a green build.** The [breaks that do not fail to compile](#breaks-that-do-not-fail-to-compile) change behavior at runtime, and a plain-JS app gets no compile errors at all. Walk that table, and run the [leftover-identifier grep](#verification).
+7. **Keep the app's behavior.** Where a section says an SDK default changed and the app relied on it, restore the old behavior in app code (the sections show how) or list it as a decision for the human. Do not adopt a new default silently.
+8. **Do not hand-edit lockfiles or generated files** (`yarn.lock`, `package-lock.json`, `pnpm-lock.yaml`, build output, generated code). Change them through the package manager or the generator.
+9. **`stream-chat` compile errors.** The app also calls `stream-chat` directly, and v10 breaks it too. The changes React apps hit most are covered inline (sections [1](#packages), [5](#channel-input-shape), [9](#stream-chat-v10-store-changes-you-will-meet-here), [16](#16-dates-on-response-types-are-unix-nanosecond-numbers), [17](#17-other-stream-chat-v10-data-shapes)). For any other `stream-chat` error, look the symbol up in the [stream-chat v9 → v10 guide](#companion-documents) and in its [type-rename table](https://github.com/GetStream/stream-chat-js/blob/release-v10/ai-docs/v10-breaking-changes.md#type-rename-table).
-Rule: change only what a section says is broken. Much of the v14 / `stream-chat` v9 surface carries over (`client.channel()`, `channel.watch()`, `channel.markRead()`, ...). When existing app code still type-checks and does what the app needs, keep it. Make the smallest edit that fixes the break rather than rewriting the code into the shape of a snippet below. The snippets show one valid form, not the only one.
+`ai-docs/` and `examples/` are not in the npm package (`files` ships only `dist`, `package.json` and `README.md`), so you cannot read them from `node_modules`. Fetch them by URL; links to this repository point at `master`.
-Links to this repository point at `master`, because `ai-docs/` and `examples/` are not in the npm package (`files` ships only `dist`, `package.json` and `README.md`), so an agent working in a consumer repo cannot read them from `node_modules`. At the stable v15 release, re-pin them to the release tag.
+## Companion documents
-## How to migrate
+Fetch a document only when a section sends you there, or the trigger in the right column applies.
-Work through the sections in order. They follow the order an app is usually rebuilt in:
+| Document | Raw URL | Fetch when |
+| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| stream-chat v9 → v10 guide | `https://raw.githubusercontent.com/GetStream/stream-chat-js/release-v10/ai-docs/ai-migration-v9-to-v10.md` | A `stream-chat` compile error or call this guide does not cover (client options, `queryChannels`, events, filters, custom data, moderation, polls, reminders, offline) |
+| stream-chat v10 breaking-changes reference | `https://raw.githubusercontent.com/GetStream/stream-chat-js/release-v10/ai-docs/v10-breaking-changes.md` | The exact v9 → v10 shape of one symbol; its `#type-rename-table` maps every renamed type |
+| React v15 breaking-changes reference | `https://raw.githubusercontent.com/GetStream/stream-chat-react/master/ai-docs/v15-breaking-changes.md` | Evidence or edge cases for one `BC-###` entry (this guide's index names the entry) |
+| React v15 i18n guide | `https://raw.githubusercontent.com/GetStream/stream-chat-react/master/ai-docs/i18n-v15-migration.md` | The app registers translations, sets a non-English language, calls `t(...)` with SDK strings, or customizes `Streami18n` ([section 18](#18-i18n)) |
+| v14 → v15 i18n key map | `https://raw.githubusercontent.com/GetStream/stream-chat-react/master/ai-docs/i18n-v15-key-map.json` | Renaming a v14 English key in a dictionary or a `t()` call |
+| Instance configuration | `https://raw.githubusercontent.com/GetStream/stream-chat-react/master/ai-docs/instance-configuration.md` | Registering request handlers, page sizes, composer setup or other `client.config` settings ([sections 13 to 15](#14-request-handlers)) |
+
+
+
+## Workflow
+
+### Phase 0: preflight
+
+- Record the current versions from `package.json` and the lockfile: `stream-chat-react`, `stream-chat`, `react`, `react-dom`, `typescript`, `dayjs`, `i18next`. Record the package manager (from the lockfile), the Node version (`node -v`, `.nvmrc`, `engines`, CI config) and whether the app is TypeScript or plain JS.
+- Require a clean git tree (`git status`). If it is not clean, stop and ask.
+- Run the app's typecheck, lint, tests and build once (find the commands in `package.json` `scripts`) and record the result as the baseline. Failures already present are not yours to fix.
+
+### Phase 1: install and platform
+
+[Section 1](#1-install-and-imports): Node, `stream-chat-react` v15 with `stream-chat` in the declared peer range, `dayjs` / `i18next`, a single `@stream-io/state-store`.
+
+### Phase 2: detect
+
+Run the [detection](#detection) block. List the sections with hits; those are the sections to apply.
+
+### Phase 3: apply the sections in order
+
+Work through the sections with hits in this order, the order an app is usually rebuilt in. Fix each compile error before moving on.
1. [Install and imports](#1-install-and-imports): dependency versions, moved and removed exports.
2. [Client and ``](#2-client-and-chat)
@@ -43,47 +81,228 @@ Work through the sections in order. They follow the order an app is usually rebu
19. [CSS and markup](#19-css-and-markup)
20. [Tests](#20-tests)
-Then run the [silent-break checklist](#breaks-that-do-not-fail-to-compile) once more: those are the changes a green build does not prove.
+### Phase 4: walk the silent breaks
+
+Check every row of [breaks that do not fail to compile](#breaks-that-do-not-fail-to-compile), with its Detect patterns, even when the build is green. Mark each row changed, not applicable, or a decision for the human.
+
+### Phase 5: verify
+
+Run [Verification](#verification): the app's checks against the baseline, the leftover-identifier grep, the dedupe checks and the runtime smoke list.
+
+### Phase 6: report
+
+Write the report below.
+
+### Definition of done
+
+- The typecheck passes, and lint, tests and build are no worse than the Phase 0 baseline.
+- Every detection hit is fixed, or listed in the report as deliberately kept (an app identifier that only shares a name).
+- The leftover-identifier grep prints nothing outside comments and changelogs.
+- Every silent-break row is marked changed, not applicable, or open decision.
+- `stream-chat` satisfies the installed `stream-chat-react` peer range, and `@stream-io/state-store` and `dayjs` resolve to one copy each.
+- The report is written.
+
+### Report template
+
+```md
+## stream-chat-react v14 → v15 migration report
+
+- Versions: stream-chat-react → ; stream-chat → ; Node → ; package manager
+- Checks, baseline → now: typecheck <…> → <…>; lint <…> → <…>; tests <…> → <…>; build <…> → <…>
+- Changed, by section: §: (one line per file and section)
+- Skipped sections (no detection hits): §, …
+- Silent breaks: : changed | not applicable () | open decision
+- Open decisions for the human:
+- Runtime checks still to do:
+```
+
+## Detection
+
+Run from the app's repository root. Each command prints `file:line` hits for the **v14** names a section replaces; a section with no hits needs no edits. `code` searches `.ts .tsx .mts .cts .js .jsx .mjs .cjs`, `styles` searches `.css .scss .sass .less`; both skip `node_modules` and build output. A hit can be the app's own identifier with the same name: read it before editing. The commands need ripgrep (`rg`); without it, run each pattern with `grep -rnE` over the same file types, excluding `node_modules`.
+
+```bash
+code() { rg -n --hidden -t ts -t js -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' -g '!.git' -e "$1" .; }
+styles() { rg -n --hidden -g '*.{css,scss,sass,less}' -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' -e "$1" .; }
+
+# §1 install and imports: versions, Node in CI, moved imports, StateStore from stream-chat
+rg -n --hidden -g 'package.json' -g '!**/node_modules/**' -e '"(stream-chat|stream-chat-react|dayjs|i18next|@stream-io/[a-z-]+)"\s*:' .
+rg -n --hidden -g '.nvmrc' -g '.node-version' -g '*.{yml,yaml}' -g 'Dockerfile*' -g '!**/node_modules/**' -g '!.git' -e 'node-version|FROM node|^v?[0-9]+(\.[0-9]+)*$' .
+code '\b(ChatView|ChatViewContext|useChatViewContext|useActiveThread|ChatViewSelector\w*|ChatView\w*SelectorButton|defaultChatViewSelectorItemSet|AIStates|getAttachmentPreviewUrl|isUploadConfirmationPending|hasPendingUploadState|StateStore)\b'
+# §2 client and
+code '\b(useCreateChatClient|useChat|defaultLanguage|channelsQueryState|latestMessageDatesByChannels|recoverStateOnReconnect|enableWSFallback|wsUrlParams|isLocalUnreadCountEnabled|StreamChatOptions)\b|connection\.changed'
+# §3 layout: Window, ChatView
+code ': required channel, watching, props that moved
+code '=22.18.0` locally and in CI | [1](#packages) |
+| `ChatView`, `useChatViewContext`, `useActiveThread`, `ChatViewSelector*` from `stream-chat-react` | The same names from `stream-chat-react/slot-layout` | [1](#imports-that-moved) |
+| `AIStates`, `getAttachmentPreviewUrl`, `isUploadConfirmationPending` from `stream-chat-react` | The same names from `stream-chat` | [1](#imports-that-moved) |
+| `hasPendingUploadState` | `isPendingUpload` from `stream-chat` | [1](#imports-that-moved) |
+| `StateStore` from `stream-chat` | `StateStore` from `@stream-io/state-store` | [1](#imports-that-moved) |
+| `client.queryChannels(...)` returning `Channel[]` | `client.queryChannelsAndHydrate(...)` (`queryChannels` returns the raw response) | [1](#packages) |
+| `client.queryUsers(filter, sort, options)` | `client.queryUsers({ payload: { filter_conditions, sort, limit } })` | [1](#packages) |
+| `ChannelSort` / `UserSort` / `ReactionSort` objects (`{ last_message_at: -1 }`) | `SortParamRequest[]` (`[{ field: 'last_message_at', direction: -1 }]`) | [1](#packages), [6](#channellist-is-driven-by-a-channelpaginator) |
+| `useCreateChatClient({ options })` | `useCreateChatClient({ config: { client: { ... } } })` | [2](#usecreatechatclient-options--config) |
+| `enableWSFallback`, `wsUrlParams`, `recoverStateOnReconnect`, `isLocalUnreadCountEnabled` options | `client.wsConnection.enableWSFallback`, `client.wsConnection.urlParams`, `client.connectionRecovery.enabled`, `channel.readEvents.localUnreadCountEnabled` | [2](#usecreatechatclient-options--config) |
+| `useChat({ client, defaultLanguage, i18nInstance }).translators` | `useStreami18n({ client, i18nInstance })` | [2](#usechat-and-chatdefaultlanguage) |
+| `` | Set the language on the `Streami18n` you pass as `i18nInstance` | [2](#usechat-and-chatdefaultlanguage) |
+| `useChatContext().channel` | `useChannel()` in the channel subtree; `useWorkspaceNavigation().openChannels` elsewhere | [2](#chatcontext-fields) |
+| `useChatContext().setActiveChannel(channel)` | `useWorkspaceNavigation().openChannel(channel, { event })` | [4](#4-selecting-channels-and-opening-threads-workspace-navigation) |
+| `useChatContext().channelsQueryState` | `useChannelPaginatorState(paginator)` | [2](#chatcontext-fields) |
+| `useChatContext().latestMessageDatesByChannels` | `useCooldownRemaining()` / `channel.cooldownTimer` | [2](#chatcontext-fields) |
+| `useChatContext('MyComponent')` (any context hook with an argument) | No argument | [10](#context-hooks-take-no-componentname-argument) |
+| `client.on('connection.changed', ...)` | `useWSConnectionState()` / `useNetworkConnectionState()` | [2](#the-lost-connection-notification) |
+| `` | Nothing: render the header, list and composer directly in `` | [3](#window-and-windowprops-are-removed) |
+| `` / `` | `` | [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
+| `` | `` or `` | [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
+| `useThreadsViewContext().activeThread` / `.setActiveThread(thread)` | `useSlotThread({ slot })` / `useWorkspaceNavigation().openThread(thread)` | [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
+| `useChatViewContext().activeChatView` / `.setActiveChatView` | `.activeView` / `.setActiveView` | [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
+| `useChannelActionContext().openThread(message)` / `.closeThread` | `useWorkspaceNavigation().openThread({ channel, message })` / `.closeThread(threadId)`, or `useCloseThread()` | [4](#4-selecting-channels-and-opening-threads-workspace-navigation) |
+| `` without `channel`, `EmptyPlaceholder` | ``; render `ChannelPlaceholder` (or `ChannelSlot`'s `fallback`) while none is selected | [5](#channel-requires-channel-and-renders-no-placeholder-loading-or-error-state) |
+| `channelQueryOptions`, `initializeOnMount` | `await channel.watch(options)` before rendering | [5](#channel-no-longer-watches-the-channel) |
+| `getChannel({ client, id, type })` | `await client.channel(type, id).watch()` | [5](#channel-no-longer-watches-the-channel) |
+| `client.channel(type, { members: [id1, id2] })` | `{ members: [{ user_id: id1 }, { user_id: id2 }] }`; render the instance `watch()` resolves with | [5](#channel-no-longer-watches-the-channel) |
+| `markReadOnMount` | A message list marks read; without one, `channel.markReadViaReporter()` | [5](#markreadonmount-is-removed-the-list-marks-read) |
+| `activeUnreadHandler`, the `(3) Title` tab title | App code | [5](#channel-no-longer-writes-documenttitle) |
+| `Channel` `giphyVersion`, `imageAttachmentSizeHandler`, `videoAttachmentSizeHandler`, `shouldGenerateVideoThumbnail` | The same props on `Attachment`, through a `WithComponents` `Attachment` override | [5](#props-that-moved-off-channel) |
+| `Channel` `onMentionsClick` / `onMentionsHover` | The same props on `MessageList` / `Message` | [5](#props-that-moved-off-channel) |
+| `doSendMessageRequest`, `doUpdateMessageRequest`, `doDeleteMessageRequest`, `doMarkReadRequest` | `client.config.set({ channel: { requestHandlers: { sendMessageRequest, updateMessageRequest, deleteMessageRequest, markReadRequest } } })` | [14](#14-request-handlers) |
+| `` | `new ChannelPaginator({ client, filters, sort, id })`, `client.channelManager.insertPaginator({ paginator })`, then `` | [6](#channellist-is-driven-by-a-channelpaginator) |
+| `ChannelList` `options.limit` / other `options` | `paginatorOptions.pageSize` / `requestOptions` | [6](#channellist-is-driven-by-a-channelpaginator) |
+| `setActiveChannelOnMount` (default `true`) | Nothing opens by default; open the first channel yourself | [6](#v14-list-defaults-that-changed) |
+| `showChannelSearch` | `` (header, search and every registered list) | [6](#channellist-is-driven-by-a-channelpaginator) |
+| `ChannelListUI` override | The list slots (`EmptyListIndicator`, `FirstPageLoadingIndicator`, `ListItem`, ...) | [6](#channellistui-is-removed-use-the-list-slots) |
+| `useChannelListContext().channels` / `.loadNextPage()` / `.setChannels` | `useChannelPaginatorState(paginator).channels` / `paginator.toTail()` / `client.channelManager.ingestChannel(channel)` | [6](#channellistcontext-lost-channels-hasnextpage-loadnextpage-and-setchannels) |
+| `ChannelListItemUIProps.lastMessage` / `latestMessagePreview` | `previewedMessage` / `` | [6](#channellistitem-props) |
+| `` | `` with `ThreadHeader`, `MessageList` and `MessageComposer` as children | [7](#thread-takes-a-thread-prop-and-renders-its-children) |
+| `Thread` `autoFocus` (default `true`) | `` inside the thread | [7](#thread-takes-a-thread-prop-and-renders-its-children) |
+| `Thread` `additionalMessageListProps` and the other `additional*Props` | Props on the children you render | [7](#thread-takes-a-thread-prop-and-renders-its-children) |
+| `ThreadHeaderProps.closeThread` / `.thread` | `useCloseThread()` / `useThreadContext()` | [7](#threadheader-lost-closethread-and-thread) |
+| `HeaderStartContent` override | `StartContent` prop on `ChannelHeader` / `ThreadHeader` | [8](#8-channel-and-thread-headers) |
+| `useChannelStateContext()` | `useChannel()`, then `useStateStore(channel.messagePaginator.state, selector)` or `useStateStore(channel.state, selector)` | [9](#channelstatecontext-and-channelactioncontext-are-removed) |
+| `loadMore()` / `loadMoreNewer()` / `hasMore` / `hasMoreNewer` | `messagePaginator.toTail()` / `.toHead()` / `.hasMoreTail` / `.hasMoreHead` | [9](#channelstatecontext-and-channelactioncontext-are-removed) |
+| `jumpToMessage(id)` / `jumpToLatestMessage()` / `jumpToFirstUnreadMessage()` | `messagePaginator.jumpToMessage(id)` / `.jumpToTheLatestMessage()` / `.jumpToTheFirstUnreadMessage()` | [9](#channelstatecontext-and-channelactioncontext-are-removed) |
+| `sendMessage` / `editMessage` / `retrySendMessage` / `deleteMessage` from the action context | `messageComposer.send()` / `.update()`; `channel.messageOperations.retry(...)` / `.delete(...)` | [9](#channelstatecontext-and-channelactioncontext-are-removed) |
+| `useTypingContext().typing` | `useStateStore(channel.state, typingSelector).typing` | [9](#typingcontext-is-removed) |
+| `channelUnreadUiState` / `setChannelUnreadUiState` | `messagePaginator.unreadStateSnapshot` / `.setUnreadSnapshot(partial)` | [9](#the-unread-snapshot-channelunreaduistate-is-removed) |
+| `channelConfig?.typing_events` (and the other flags) | `channel.configState` → `typingEvents.enabled`, `readEvents.enabled`, `replies.enabled`, ... | [9](#channel-configuration-channelconfig--channelconfigstate) |
+| `channel.getConfig()` | `channel.serverConfig` (a getter) | [9](#stream-chat-v10-store-changes-you-will-meet-here) |
+| `channel.state.readStore` / `typingStore` / `membersStore` / `watcherStore` / `ownCapabilitiesStore` | `channel.state` (one store; drop the `.Store` segment) | [9](#stream-chat-v10-store-changes-you-will-meet-here) |
+| `WatcherState` / `channel.disconnected` | `ChannelWatchState` / `channel.pendingDisposal` | [9](#stream-chat-v10-store-changes-you-will-meet-here) |
+| `ComponentContext.Message`, the `Message={...}` prop | `MessageUI` override (`VirtualMessage` in `VirtualizedMessageList`) | [10](#message-ui-overrides-consolidate-on-the-messageui-slot) |
+| `ComponentContext.ThreadHeader` override | Render `` as a child of `` (not the `ThreadHead` slot) | [10](#removed-componentcontext-slots) |
+| `messageActions` prop, `MESSAGE_ACTIONS`, `getMessageActions()` | `messageActionSet` on `MessageActions`; permissions from `useUserRole(message)` | [10](#messageactions-and-message_actions-are-removed) |
+| `` | Removed; `reviewProcessedMessage`, or act on the paginator | [11](#the-messages-prop-is-removed) |
+| `disableDateSeparator` | `withDateSeparator` (inverted) | [11](#disabledateseparator--withdateseparator) |
+| `threadList` / `head` list props | `!!useThreadContext()` / the `ThreadHead` slot | [11](#threadlist-and-head-are-removed-lists-detect-the-thread) |
+| `highlightedMessageId` | `messagePaginator.messageFocusSignal` | [11](#pagination-and-state-props-are-removed) |
+| `messageLimit` | `client.config.set({ channel: { messagePaginator: { pageSize } } })` | [11](#pagination-and-state-props-are-removed) |
+| `useMessageContext().mutes` | `useChatContext().mutes` | [12](#messagecontextmutes-and-messageisunread-are-removed) |
+| `retrySendMessage` prop / `retry(message)` | `retrySendMessageRequest` handler / `retry({ localMessage: message })` | [12](#retrysendmessage-and-useretryhandler) |
+| `usePinHandler(message).canPin` | `useUserRole(message).canPin` | [12](#usepinhandler-no-longer-returns-canpin) |
+| `allowConcurrentPlayback` / `allowConcurrentAudioPlayback` | Removed; one voice message plays at a time | [12](#allowconcurrentplayback-is-removed-one-voice-message-plays-at-a-time) |
+| `overrideSubmitHandler` | A `sendMessageRequest` handler on `client.config` | [13](#overridesubmithandler-is-removed) |
+| `useMessageComposerContext().handleSubmit` | `useMessageComposerSubmitFn()` | [13](#handlesubmit-is-removed) |
+| `preventClearingOnUnmount` | Removed; call `composer.clear()` yourself | [13](#preventclearingonunmount-is-removed) |
+| `useAttachmentManagerState().hasCustomDoUploadRequest` | `.customCdn` | [13](#useattachmentmanagerstate-hascustomdouploadrequest--customcdn) |
+| `client.setMessageComposerSetupFunction(fn)` | `client.config.setSetupFunction('messageComposer', fn)` | [13](#setmessagecomposersetupfunction-is-deprecated) |
+| `new Date(message.created_at)`, `dayjs(created_at)` | `convertTimestampToDate(created_at)` from `stream-chat` (may return `undefined`) | [16](#16-dates-on-response-types-are-unix-nanosecond-numbers) |
+| `channel.data?.name` / `.image` | `channel.data?.custom?.name` / `.image` | [17](#17-other-stream-chat-v10-data-shapes) |
+| `handleDelete({ deleteForMe, hardDelete })` | `handleDelete({ delete_for_me, hard })` | [17](#17-other-stream-chat-v10-data-shapes) |
+| `client.flagMessage(id)` / `channel.addMembers(ids)` / `client.getThread(id, options)` | `client.moderation.flagMessage({ message_id })` / `channel.addMembers({ members: ids })` / `client.getThreadAndHydrate({ message_id }, { watch: true })` | [17](#17-other-stream-chat-v10-data-shapes) |
+| `t('English text')` | `t('namespaced.key', 'English text')`, or `t(asDynamicKey('myApp.key'), 'English text')` for app strings | [18](#18-i18n) |
+| `deTranslations` ... `trTranslations`, `isLanguageSupported` | Register your own dictionaries; `i18n.registeredLanguages.has(lang)` | [18](#18-i18n) |
+| `.str-chat__main-panel`, `.str-chat__container` | `.str-chat__channel` (the content column); size panels on the slot or your own row | [19](#19-css-and-markup) |
+| `.str-chat__channel-list-inner` | `.str-chat__channel-list .str-chat__infinite-scroll-paginator` | [19](#19-css-and-markup) |
+| Row highlight on `[aria-pressed=true]` | `[aria-selected=true]` | [19](#19-css-and-markup) |
## Breaks that do not fail to compile
-These compile cleanly, or are ignored in plain JS, and change behavior at runtime. Check each one explicitly; the absence of an error does not mean the app is migrated.
-
-| Symptom | Cause | Fix |
-| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Clicking a channel-list row, a search result, a thread-list row or a message's reply button does nothing; no row shows active | Selection and thread opening go through workspace navigation, whose default outside `ChatView` is a no-op (it logs a one-time console warning per operation) | Use `ChatView`, or provide `WorkspaceNavigationProvider` ([section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) |
-| No channel is open when the app loads | v14 `ChannelList.setActiveChannelOnMount` defaulted to `true`; v15 selects nothing | Open the first channel yourself ([section 6](#v14-list-defaults-that-changed)) |
-| Inside `ChatView`, returning from the threads view shows the first channel instead of the one that was open, and the channel list reloads | `ChatView` unmounts the inactive view, so an auto-open guarded by a `useRef`, or a paginator created inside the view, starts over on every switch | Create the paginator above `ChatView`; auto-open only while `openChannels` is empty ([section 6](#v14-list-defaults-that-changed)) |
-| Without `ChatView`, a default `ThreadHeader` shows no close button | `defaultWorkspaceNavigation.isThreadDismissable` returns `false` | Override it in your provider ([section 4](#without-chatview)) |
-| A new message in a channel outside the list's filters no longer adds that channel to the list | v14 `allowNewMessagesFromUnfilteredChannels` defaulted to `true`; v15 adds only channels the filters match | Widen the paginator's filters ([section 6](#v14-list-defaults-that-changed)) |
-| The channel list scrolls infinitely instead of showing a "Load more" button | v14 `Paginator` defaulted to `LoadMorePaginator` | Keep it, or render your own list ([section 6](#v14-list-defaults-that-changed)) |
-| Opening a thread no longer focuses its composer | v14 `Thread.autoFocus` defaulted to `true`, so every thread focused it | `` in the thread ([section 7](#thread-takes-a-thread-prop-and-renders-its-children)) |
-| A thread renders below the channel instead of beside it, or a panel collapses to a narrow column | The channel no longer contains a flex row, and `ChatView` slots are `flex: 1 1 0` siblings | Make your container a row; size panels by slot ([section 19](#panel-layout)) |
-| In `views` that render a list beside a ``, the channel list or thread list fills the view, the channel or thread beside it is not visible and the page scrolls horizontally | `--str-chat__channel-list-width` and `--str-chat__thread-list-width` default to `100%`, and the lists do not shrink | Set both widths on the `ChatView` root ([section 19](#panel-layout)) |
-| A channel name or image renders empty | `name` / `image` are custom data in `stream-chat` v10: `channel.data.custom.name` | Read `custom` ([section 17](#17-other-stream-chat-v10-data-shapes)) |
-| A held channel throws "pending disposal" after logout or a user switch | `disconnectUser()` disposes the user's channels | Drop held channels on disconnect ([section 4](#without-chatview)) |
-| A channel renders empty, with no error, and gets no new messages | `Channel` no longer watches the channel | Watch it first: `channel.watch()` ([section 5](#channel-no-longer-watches-the-channel)) |
-| A direct message the app just created renders, but new messages never arrive | The created members-only instance was superseded by the store's instance for the same cid | Render the instance `watch()` resolves with ([section 5](#channel-no-longer-watches-the-channel)) |
-| After a failed send, the composer is empty instead of holding the text and attachments | The composer is released before the request and nothing is put back | Nothing to do; the failed message is retried from the list ([section 13](#a-failed-send-or-edit-no-longer-restores-the-composer)) |
-| A component that rendered blank outside its provider now throws | Required context hooks throw instead of warning and returning `{}` | Render it inside the provider ([section 10](#required-context-hooks-throw-outside-their-provider)) |
-| A standalone `` (a preview, a pinned-message card) throws | `Message` needs a channel in context | Wrap it in `ChannelInstanceProvider` ([section 12](#a-standalone-message-needs-a-channel-in-context)) |
-| The channel is not marked read | Mark-read moved from `Channel` mount to the message list | Render a message list, or mark read yourself ([section 5](#markreadonmount-is-removed-the-list-marks-read)) |
-| The tab title no longer shows `(3) Title` | `Channel` no longer writes `document.title` | Write it in app code ([section 5](#channel-no-longer-writes-documenttitle)) |
-| Mount effects in a channel's children stop re-running when the channel changes | `Channel` no longer remounts its subtree per channel | Depend on the `channel` instance ([section 5](#channel-lifecycle-activation-no-remount-no-reload-no-search-jump)) |
-| After switching channels, a child still shows the previous channel's name, draft or other local state | `Channel` no longer remounts its subtree, so `useState(valueFromChannel)` keeps the value of the first channel it saw | Reset the state when `channel` changes ([section 5](#channel-lifecycle-activation-no-remount-no-reload-no-search-jump)) |
-| Search results open unwatched, empty channels in a custom result component | Search results are no longer watched | `watch()` on open ([section 4](#search-results)) |
-| Translations or overrides stop applying; non-English UI renders English | Keys renamed, non-English dictionaries removed | Rename keys, register dictionaries ([section 18](#18-i18n)) |
-| CSS on `.str-chat__main-panel`, `.str-chat__container`, `.str-chat__channel-list-inner` or `.str-chat__thread--virtualized` stops applying | Those wrappers and classes are gone | Grep for them, combinators included, and retarget ([section 19](#19-css-and-markup)) |
-| Plain JS: `useCreateChatClient({ options })` stops applying `baseURL` and other settings | `options` was renamed to `config` | [Section 2](#usecreatechatclient-options--config) |
-| Plain JS: a removed prop or override key (`Message`, `HeaderStartContent`, `do*Request`, `overrideSubmitHandler`, ...) does nothing | Ignored without a warning | Grep for each removed name listed in this guide |
-| A supplied composer keeps its content after `MessageComposer` unmounts | A supplied composer is never cleared on unmount | Call `composer.clear()` yourself ([section 13](#preventclearingonunmount-is-removed)) |
-| A voice message stops another one playing in a different panel | Playback is exclusive app-wide | Nothing to do ([section 12](#allowconcurrentplayback-is-removed-one-voice-message-plays-at-a-time)) |
-| The attachment menu offers different actions than before | Actions follow the composer's resolved configuration | Set `customCdn` where it applies ([section 13](#attachment-and-poll-availability)) |
-| A custom `MessageTimestamp` does not show in the header of a gallery you build yourself | The gallery renders outside a message and reads the item; it uses the override only when the item carries its `message` | Set `message` on each `GalleryItem` ([section 12](#the-gallery-header-renders-the-messagetimestamp-override-for-items-that-carry-their-message)) |
-| `dayjs(created_at)` renders `Invalid Date` | Server timestamps are unix nanoseconds | Convert with `convertTimestampToDate` ([section 16](#16-dates-on-response-types-are-unix-nanosecond-numbers)) |
-| The "New Messages!" notification no longer shows after jumping to older messages | It shows only when a message arrives while the list is scrolled up | Read `messagePaginator.hasMoreHead` for an "older messages shown" indicator ([section 11](#the-new-messages-notification-shows-only-for-messages-that-arrived)) |
-| The scroll-to-latest button shows a larger number than before | It shows the channel's unread count for the current user, not the messages that arrived while scrolled up | Nothing to do; see [section 11](#scrolltolatestmessagebutton-shows-the-channels-unread-count) |
-| A custom loader given to `InfiniteScrollPaginator` (or `ChannelList` / `SearchSourceResultList`) is called earlier and more often | `loadNextDebounceMs` now limits how often the scroll position is checked, rather than waiting for scrolling to stop | Ignore calls while a page is loading ([section 11](#loading-older-messages-keeps-your-place-loadnextdebouncems-rate-limits-the-check)) |
+These compile cleanly, or are ignored in plain JS, and change behavior at runtime. Check each one explicitly; the absence of an error does not mean the app is migrated. **Detect** lists `rg` patterns (run them with the `code` / `styles` helpers from [Detection](#detection)) or what to review; a row with no hit and nothing to review does not apply.
+
+| Symptom | Cause | Fix | Detect |
+| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| Clicking a channel-list row, a search result, a thread-list row or a message's reply button does nothing; no row shows active | Selection and thread opening go through workspace navigation, whose default outside `ChatView` is a no-op (it logs a one-time console warning per operation) | Use `ChatView`, or provide `WorkspaceNavigationProvider` ([section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) | `` in the thread ([section 7](#thread-takes-a-thread-prop-and-renders-its-children)) | ``, the channel list or thread list fills the view, the channel or thread beside it is not visible and the page scrolls horizontally | `--str-chat__channel-list-width` and `--str-chat__thread-list-width` default to `100%`, and the lists do not shrink | Set both widths on the `ChatView` root ([section 19](#panel-layout)) | `views=` with `ChannelNavigation` / `ChannelList` / `ThreadList`, and no `--str-chat__channel-list-width` (`styles`) |
+| A channel name or image renders empty | `name` / `image` are custom data in `stream-chat` v10: `channel.data.custom.name` | Read `custom` ([section 17](#17-other-stream-chat-v10-data-shapes)) | `.data?.name`, `.data.name`, `.data?.image` |
+| A held channel throws "pending disposal" after logout or a user switch | `disconnectUser()` disposes the user's channels | Drop held channels on disconnect ([section 4](#without-chatview)) | `disconnectUser`, `useState` (a preview, a pinned-message card) throws | `Message` needs a channel in context | Wrap it in `ChannelInstanceProvider` ([section 12](#a-standalone-message-needs-a-channel-in-context)) | `` misses messages sent while the client was offline, or a channel reloads twice after a reconnect | The client reloads only activated channels after a reconnect, and v14's `Channel` no longer reloads on `connection.recovered` | Call `channel.activate()` in a custom surface; remove app-side reloads on `connection.recovered` ([section 5](#channel-lifecycle-activation-no-remount-no-reload-no-search-jump)) | `connection.recovered`, `.reload(`, custom channel surfaces without ` {
const { openChannels } = useWorkspaceNavigation();
const channel = openChannels[0];
@@ -805,7 +1025,7 @@ const channel = await client.channel('messaging', 'general').watch();
-**Channel input shape (`stream-chat` v10).** The third argument of `client.channel()` is `ChannelInput`. `members` is `ChannelMemberRequest[]`, `{ user_id }` objects instead of an id list, and custom fields such as `name` go under `custom` (`{ custom: { name } }`). Plain ids now fail to compile (`TS2769`), though the API still accepts them at runtime (checked with `stream-chat@10.0.0-rc.22`), so this is a type fix, not a behavior change.
+**Channel input shape (`stream-chat` v10).** The third argument of `client.channel()` is `ChannelInput`. `members` is `ChannelMemberRequest[]`, `{ user_id }` objects instead of an id list, and custom fields such as `name` go under `custom` (`{ custom: { name } }`). Plain ids now fail to compile (`TS2769`), though the API still accepts them at runtime, so this is a type fix, not a behavior change.
[`examples/vite`](https://github.com/GetStream/stream-chat-react/tree/master/examples/vite) and the tutorial steps show the full flow.
@@ -841,7 +1061,7 @@ These keep the same signature and change behavior:
```
- **Activation.** `Channel` calls `channel.activate()` on mount and the returned release on unmount (refcounted, so several consumers can hold one instance). While a channel is active, the client skips re-seeding its message list on channel-list hydration, which would perturb a larger scrolled-back window. A custom channel surface that does not render `Channel` must activate the channel itself, or the client re-seeds its list and `client.channelManager.releaseUnusedChannels()` may drop it.
-- **No reload on reconnect.** `Channel` no longer calls `channel.reload()` on `connection.recovered`. The client reloads active channels itself after a reconnect, so a custom surface needs only the activation:
+- **No reload on reconnect.** `Channel` no longer calls `channel.reload()` on `connection.recovered`. After a reconnect, `client.connectionRecovery` re-queries the registered channel lists and reloads every active channel and thread itself, then dispatches `connection.recovered`. Do not add a `connection.recovered` handler that reloads channels: they are already reloaded, and it would reload them twice. A channel that nobody activated is not reloaded ([`stream-chat` reference](https://github.com/GetStream/stream-chat-js/blob/release-v10/ai-docs/v10-breaking-changes.md#channel-activate-recovery)), so a custom surface needs the activation:
```ts
import { useEffect } from 'react';
@@ -863,7 +1083,7 @@ When writing your own: the counts are user-scoped (the unread message total from
### `markReadOnMount` is removed; the list marks read
-`Channel` no longer marks the channel read when it mounts. `MessageList` / `VirtualizedMessageList` mark it read (`useMarkRead`) while the list is at the bottom, showing the live head, the tab is visible and the channel is not flagged unread. **A `Channel` rendered without a message list is never marked read**; call `channel.markRead()` yourself there. `markReadOnMount={false}` has no direct equivalent. To change how read requests go out, register a `markReadRequest` handler ([section 14](#14-request-handlers)).
+`Channel` no longer marks the channel read when it mounts. `MessageList` / `VirtualizedMessageList` mark it read (`useMarkRead`) while the list is at the bottom, showing the live head, the tab is visible and the channel is not flagged unread. **A `Channel` rendered without a message list is never marked read**; call `channel.markReadViaReporter()` yourself there. It goes through the delivery reporter and your `markReadRequest` handler, and resolves `null` when read events are off; `channel.markRead()` is the direct request, which skips both and throws when read events are off. `markReadOnMount={false}` has no direct equivalent. To change how read requests go out, register a `markReadRequest` handler ([section 14](#14-request-handlers)).
### Props that moved off `Channel`
@@ -1441,21 +1661,21 @@ The message paginator has two ends: the **tail** is the oldest loaded message, t
**Actions** (was `useChannelActionContext`):
-| v14 | v15 |
-| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `loadMore` (older) / `loadMoreNewer` | `channel.messagePaginator.toTail()` / `.toHead()`. `MessageList` uses exactly these: `toTail` on scroll to the top, `toHead` on scroll to the bottom. |
-| `loadMoreThread` | `thread.messagePaginator.toTail()` |
-| `jumpToMessage(id)` / `jumpToLatestMessage()` / `jumpToFirstUnreadMessage()` | `messagePaginator.jumpToMessage(id)` / `.jumpToTheLatestMessage()` / `.jumpToTheFirstUnreadMessage()` |
-| `sendMessage` | `messageComposer.send()`, or `useMessageComposerSubmitFn()` in a composer UI. Lower level: `channel.messageOperations.send({ localMessage, message, options })` |
-| `retrySendMessage` | `channel.messageOperations.retry({ localMessage })`, or `handleRetry` from `useMessageContext()` |
-| `editMessage` | `messageComposer.update()`, or `channel.messageOperations.update({ localMessage, options })` |
-| `deleteMessage` | `channel.messageOperations.delete({ localMessage, options })`, or `handleDelete` from `useMessageContext()` |
-| `updateMessage` / `removeMessage` (local-only list writes) | `messagePaginator.ingestItem(message)` / `.removeItem({ id })`. Normally unnecessary: the client applies WebSocket events itself. |
-| `markRead` | `channel.markRead()`. The unread UI snapshot is separate state on the paginator; there is no `updateChannelUiUnreadState` option. |
-| `setChannelUnreadUiState` | `channel.messagePaginator.setUnreadSnapshot(partial)` |
-| `openThread` / `closeThread` | `useWorkspaceNavigation().openThread({ channel, message })` / `.closeThread(threadId)`; inside a thread, `useCloseThread()` ([section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) |
-| `onMentionsClick` / `onMentionsHover` | The props on `MessageList` / `Message` |
-| `dispatch` | None; there is no React reducer |
+| v14 | v15 |
+| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `loadMore` (older) / `loadMoreNewer` | `channel.messagePaginator.toTail()` / `.toHead()`. `MessageList` uses exactly these: `toTail` on scroll to the top, `toHead` on scroll to the bottom. |
+| `loadMoreThread` | `thread.messagePaginator.toTail()` |
+| `jumpToMessage(id)` / `jumpToLatestMessage()` / `jumpToFirstUnreadMessage()` | `messagePaginator.jumpToMessage(id)` / `.jumpToTheLatestMessage()` / `.jumpToTheFirstUnreadMessage()` |
+| `sendMessage` | `messageComposer.send()`, or `useMessageComposerSubmitFn()` in a composer UI. Lower level: `channel.messageOperations.send({ localMessage, message, options })` |
+| `retrySendMessage` | `channel.messageOperations.retry({ localMessage })`, or `handleRetry` from `useMessageContext()` |
+| `editMessage` | `messageComposer.update()`, or `channel.messageOperations.update({ localMessage, options })` |
+| `deleteMessage` | `channel.messageOperations.delete({ localMessage, options })`, or `handleDelete` from `useMessageContext()` |
+| `updateMessage` / `removeMessage` (local-only list writes) | `messagePaginator.ingestItem(message)` / `.removeItem({ id })`. Normally unnecessary: the client applies WebSocket events itself. |
+| `markRead` | `channel.markReadViaReporter()` (`channel.markRead()` is now the direct request: it skips the reporter and the `markReadRequest` handler). The unread UI snapshot is separate state on the paginator; there is no `updateChannelUiUnreadState` option. |
+| `setChannelUnreadUiState` | `channel.messagePaginator.setUnreadSnapshot(partial)` |
+| `openThread` / `closeThread` | `useWorkspaceNavigation().openThread({ channel, message })` / `.closeThread(threadId)`; inside a thread, `useCloseThread()` ([section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) |
+| `onMentionsClick` / `onMentionsHover` | The props on `MessageList` / `Message` |
+| `dispatch` | None; there is no React reducer |
`channel.messageOperations` and `thread.messageOperations` perform the optimistic local update and then the request, honoring the request handlers registered on `client.config` ([section 14](#14-request-handlers)). For a thread reply, use `thread.messageOperations` / `thread.messagePaginator`: thread replies are an independent paginator, not part of the channel's list.
@@ -1503,7 +1723,7 @@ export const LoadOlder = () => {
### `TypingContext` is removed
-`TypingContext`, `TypingContextValue`, `TypingProvider` and `useTypingContext` are removed. Subscribe to the channel's own store: `typing` is `Record` keyed by user id. The non-reactive snapshot is `channel.state.typing`. The SDK's own `TypingIndicator`, `TypingIndicatorHeader`, `ChannelHeader` and `ThreadHeader` read this slice and filter it by `parent_id` for threads. The text composer keeps no copy: `TextComposerState` has no `typing` in `stream-chat` v10, so a custom indicator written against a v15 release candidate that read `messageComposer.textComposer.state` reads `channel.state` instead, in a thread too.
+`TypingContext`, `TypingContextValue`, `TypingProvider` and `useTypingContext` are removed. Subscribe to the channel's own store: `typing` is keyed by user id and holds each user's `typing.start` event. The non-reactive snapshot is `channel.state.typing`. The SDK's own `TypingIndicator`, `TypingIndicatorHeader`, `ChannelHeader` and `ThreadHeader` read this slice and filter it by `parent_id` for threads. The text composer keeps no copy (`TextComposerState` has no `typing`), so read `channel.state` in a thread too.
```tsx
// v14
@@ -1526,7 +1746,7 @@ export const TypingCount = () => {
};
```
-**Reading who is typing needs a type guard.** The slice is typed `Record`, and `Event` is a discriminated union in `stream-chat` v10, so `event.user` does not compile on it (`TS2339`). The client stores only `typing.start` events there; narrow to that member:
+**Reading who is typing.** The slice is typed `Record>`, so `event.user` is available directly:
```tsx
// v14
@@ -1535,28 +1755,22 @@ const names = Object.values(typing ?? {}).map(({ user }) => user?.name);
```
```tsx
-import type { ChannelStateData, Event, EventPayload } from 'stream-chat';
+import type { ChannelStateData } from 'stream-chat';
import { useChannel, useChatContext, useStateStore } from 'stream-chat-react';
const typingSelector = ({ typing }: ChannelStateData) => ({ typing });
-const isTypingStart = (event: Event): event is EventPayload<'typing.start'> =>
- event.type === 'typing.start';
-
export const TypingUsers = () => {
const { client } = useChatContext();
const channel = useChannel();
const { typing } = useStateStore(channel.state, typingSelector);
const names = Object.values(typing)
- .filter(isTypingStart)
.filter(({ user }) => user?.id !== client.user?.id)
.map(({ user }) => user?.name ?? user?.id);
return names.length ? {names.join(', ')} typing : null;
};
```
-If the installed `stream-chat` types the slice as `Record>`, the guard is unnecessary: read `event.user` directly.
-
For a thread-scoped indicator, compare each event's `parent_id` with the thread's parent message (`useStateStore(useThreadContext()?.state, selector)`). The `typing_events` gate is `typingEvents.enabled` on `channel.configState` (below).
### The unread snapshot: `ChannelUnreadUiState` is removed
@@ -2041,6 +2255,36 @@ export const RetryButton = () => {
`usePinHandler(message, notifications?)` returns `{ handlePin }`. Read `canPin` from `useUserRole(message).canPin`. In untyped code the removed field is `undefined`, so a pin button gated on it never shows.
+### Message action handlers reject when they fail
+
+`handlePin`, `handleDelete`, `handleMute`, `handleFlag` and `handleMarkUnread` from `useMessageContext()` (and the `usePinHandler`, `useDeleteHandler`, `useMuteHandler`, `useFlagHandler` and `useMarkUnreadHandler` hooks) now reject:
+
+- when the request fails. `handlePin` rolls the optimistic pin back and rethrows; v14 rolled back and resolved.
+- when called without the message they act on (a custom message UI or hook call outside a message). v14 logged a `console.warn` or returned without a request.
+
+The default `MessageActions` show the error notification. A custom button that calls one of them catches the rejection, or it goes unhandled. The handlers are typed `ReactEventHandler`, so `await` them in an `async` handler rather than chaining `.catch`:
+
+```tsx
+import { useMessageContext } from 'stream-chat-react';
+
+export const PinButton = () => {
+ const { handlePin } = useMessageContext();
+ return (
+
+ );
+};
+```
+
### `AIStates` moved to `stream-chat`
`import { AIStates } from 'stream-chat'`. The values are unchanged, but it is now `as const`, so an inferred array of its members no longer accepts the wide `AIState` that `useAIState` returns (`.includes(aiState)` fails to compile). Widen the array:
@@ -2764,16 +3008,18 @@ Four formatter keys carry English day words inside their `calendarFormats` (`tim
## 19. CSS and markup
-| v14 markup or class | v15 |
-| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `div.str-chat__main-panel` (+ `--thread-open`), rendered by `Window` | Gone. `Channel` renders `div.str-chat__channel` as the content column (flex column with header, list, composer). Target `.str-chat__thread-container` / `.str-chat__thread` for the thread panel. The v14 getting-started CSS on `#root .str-chat__main-panel` matches nothing. |
-| `div.str-chat__container` inside `Channel` (or `customClasses.chatContainer`) | Gone, and `customClasses.chatContainer` is removed from the type. Use `customClasses.channel` for the `.str-chat__channel` column. |
-| `.str-chat__channel-list-inner`, `.str-chat__channel-list-inner__main` (from `ChannelListUI`, which held `role="listbox"`) | Gone. Target `.str-chat__channel-list .str-chat__infinite-scroll-paginator` (the `role="listbox"` scroll container, inside `ChannelNavigation`) and `.str-chat__infinite-scroll-paginator__content`. A bare `` has no `.str-chat__channel-list` ancestor. |
-| The channel list root carried `str-chat str-chat__channel-list` and `customClasses.channelList` | Inside `ChatView` the `str-chat ` scope comes from the `ChatView` root, and the channel list adds none of its own: a nested `.str-chat` would reset the tokens you override on the `ChatView` root. Outside `ChatView`, `ChannelNavigation`'s root renders `str-chat str-chat__channel-list`, and a bare `` wraps itself in `div.str-chat..str-chat__theme-scope` (`display: contents`). `customClasses.chat` replaces `str-chat` where the scope is applied; `customClasses.channelList` replaces `str-chat__channel-list` on `ChannelNavigation`'s root. App CSS that targeted the list root as a `.str-chat` element no longer matches it inside `ChatView`. |
-| `.str-chat__chat-view__channels` / `.str-chat__chat-view__threads` | Rendered only when `ChatView` is given `views`. With the built-in workspace layout, content sits in `.str-chat__chat-view__workspace-layout`, `.str-chat__chat-view__workspace-layout-slots`, `.str-chat__chat-view__slot` (`--hidden`) and `.str-chat__chat-view__slot-layer`. |
-| `.str-chat__thread--virtualized` on the thread container | Not applied. Scope by the list you render (`.str-chat__thread .str-chat__virtual-list`, or your own class). |
-| `.str-chat__quoted-message-preview` text inherited the surrounding colour | It sets `color: var(--str-chat__chat-text-incoming)`, and `--own` `var(--str-chat__chat-text-outgoing)`, matching the bubble backgrounds. In the default light theme your own quotes change to the outgoing bubble's text colour. Remove contrast workarounds written for v14; theme with those two tokens. |
-| `.str-chat__channel-list-item[aria-pressed=true]` and `.str-chat__thread-list-item[aria-pressed=true]` highlighted the active row | Only `[aria-selected=true]` is styled. A custom row UI sets `role="option"` and `aria-selected`, not `aria-pressed`: `aria-selected={active}` on a channel row, `aria-selected={isThreadActive(thread.id)}` (from `useWorkspaceNavigation()`) on a thread row |
+| v14 markup or class | v15 |
+| ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `div.str-chat__main-panel` (+ `--thread-open`), rendered by `Window` | Gone. `Channel` renders `div.str-chat__channel` as the content column (flex column with header, list, composer). Target `.str-chat__thread-container` / `.str-chat__thread` for the thread panel. The v14 getting-started CSS on `#root .str-chat__main-panel` matches nothing. |
+| `div.str-chat__container` inside `Channel` (or `customClasses.chatContainer`) | Gone, and `customClasses.chatContainer` is removed from the type. Use `customClasses.channel` for the `.str-chat__channel` column. |
+| `.str-chat__channel-list-inner`, `.str-chat__channel-list-inner__main` (from `ChannelListUI`, which held `role="listbox"`) | Gone. Target `.str-chat__channel-list .str-chat__infinite-scroll-paginator` (the `role="listbox"` scroll container, inside `ChannelNavigation`) and `.str-chat__infinite-scroll-paginator__content`. A bare `` has no `.str-chat__channel-list` ancestor. |
+| The channel list root carried `str-chat str-chat__channel-list` and `customClasses.channelList` | Inside `ChatView` the `str-chat ` scope comes from the `ChatView` root, and the channel list adds none of its own: a nested `.str-chat` would reset the tokens you override on the `ChatView` root. Outside `ChatView`, `ChannelNavigation`'s root renders `str-chat str-chat__channel-list`, and a bare `` wraps itself in `div.str-chat..str-chat__theme-scope` (`display: contents`). `customClasses.chat` replaces `str-chat` where the scope is applied; `customClasses.channelList` replaces `str-chat__channel-list` on `ChannelNavigation`'s root. App CSS that targeted the list root as a `.str-chat` element no longer matches it inside `ChatView`. |
+| `.str-chat__chat-view__channels` / `.str-chat__chat-view__threads` | Rendered only when `ChatView` is given `views`. With the built-in workspace layout, content sits in `.str-chat__chat-view__workspace-layout`, `.str-chat__chat-view__workspace-layout-slots`, `.str-chat__chat-view__slot` (`--hidden`) and `.str-chat__chat-view__slot-layer`. |
+| `.str-chat__thread--virtualized` on the thread container | Not applied. Scope by the list you render (`.str-chat__thread .str-chat__virtual-list`, or your own class). |
+| `.str-chat__quoted-message-preview` text inherited the surrounding colour | It sets `color: var(--str-chat__chat-text-incoming)`, and `--own` `var(--str-chat__chat-text-outgoing)`, matching the bubble backgrounds. In the default light theme your own quotes change to the outgoing bubble's text colour. Remove contrast workarounds written for v14; theme with those two tokens. |
+| `.str-chat__channel-list-item[aria-pressed=true]` and `.str-chat__thread-list-item[aria-pressed=true]` highlighted the active row | Only `[aria-selected=true]` is styled. A custom row UI sets `role="option"` and `aria-selected`, not `aria-pressed`: `aria-selected={active}` on a channel row, `aria-selected={isThreadActive(thread.id)}` (from `useWorkspaceNavigation()`) on a thread row |
+| `.str-chat__message--has-single-attachment` on a message that carries a poll | Not applied while the message carries a poll; such a message gets `.str-chat__message--has-poll`. The quoted message now renders above the poll ([section 12](#new-the-message-bubbles-content-is-a-configurable-set-the-quoted-message-renders-above-a-poll)) |
+| The first message of the channel's list had `padding-top: 4.5rem` (`.str-chat__main-panel .str-chat__ul .str-chat__li:first-of-type`) | No top padding. Remove app rules that compensated for it |
Find every app rule that uses a removed class. They also hide inside combinators and pseudo-classes, where a rule does more than style the element: `.create-channel-panel + .str-chat__main-panel { display: none }` hid the channel while another panel was open, and without it both render side by side. Nothing reports such a rule; it simply stops matching.
@@ -2832,86 +3078,123 @@ On narrow screens (`max-width: 767px`) the lists switch to an overlay sized by `
---
+## Verification
+
+Run after every section is applied.
+
+1. **The app's checks.** Read `package.json` `scripts` and run the typecheck (`tsc --noEmit`, `yarn types`, ...), lint, unit tests and production build. Compare each with the Phase 0 baseline; a new failure is a migration bug.
+2. **Leftover identifiers.** Every v14 name below was removed or renamed. In a plain-JS app this grep is the only thing that finds them. It must print nothing outside comments, changelogs and app identifiers that only share the name (record those in the report). Uses the `code` / `styles` helpers from [Detection](#detection).
+
+ ```bash
+ code '\b(WindowProps|ChannelStateContext|ChannelStateContextValue|ChannelStateProvider|useChannelStateContext|ChannelActionContext|ChannelActionContextValue|ChannelActionProvider|useChannelActionContext|ChannelNotifications|MarkReadWrapperOptions|RetrySendMessage|TypingContext|TypingContextValue|TypingProvider|useTypingContext|ChannelUnreadUiState|channelUnreadUiState|setChannelUnreadUiState|updateChannelUiUnreadState|useThreadsViewContext|ThreadsViewContextValue|ThreadAdapter|activeChatView|setActiveChatView|setActiveChannel|setActiveChannelOnMount|customActiveChannel|channelsQueryState|latestMessageDatesByChannels|useChannelEditMessageHandler|useChannelMentionsHandler|ChannelListUI|ChannelListUIProps|usePaginatedChannels|useConnectionRecoveredListener|CustomQueryChannelParams|CustomQueryChannelsFn|EffectiveQueryParams|moveChannelUpwards|findLastPinnedChannelIndex|extractSortValue|isChannelPinned|isChannelArchived|shouldConsiderPinnedChannels|shouldConsiderArchivedChannels|allowNewMessagesFromUnfilteredChannels|lockChannelOrder|showChannelSearch|sendChannelsToList|renderChannels|channelRenderFilterFn|customQueryChannels|recoveryThrottleIntervalMs|onMessageNewHandler|channelUpdateCount|useLegacyThreadContext|additionalMessageComposerProps|additionalMessageListProps|additionalVirtualizedMessageListProps|additionalParentMessageProps|enableDateSeparator|disableDateSeparator|HeaderStartContent|MESSAGE_ACTIONS|OPTIONAL_MESSAGE_ACTIONS|getMessageActions|MessageActionsArray|actionsEnabled|messageIsUnread|initializeOnMount|channelQueryOptions|markReadOnMount|activeUnreadHandler|skipMessageDataMemoization|allowConcurrentAudioPlayback|allowConcurrentPlayback|doSendMessageRequest|doUpdateMessageRequest|doDeleteMessageRequest|doMarkReadRequest|overrideSubmitHandler|preventClearingOnUnmount|loadMoreNewer|loadMoreThread|loadingMoreNewer|jumpToLatestMessage|jumpToFirstUnreadMessage|highlightedMessageId|threadSuppressAutoscroll|loadingMoreForJumpToChannelMessage|messageLimit|hasPendingUploadState|recoverStateOnReconnect|wsUrlParams|isLocalUnreadCountEnabled|StreamChatOptions|isLanguageSupported|SupportedTranslations|DateFormatterOptions|getTranslators|registerSetLanguageCallback|getAvailableLanguages|geti18Instance|deTranslations|enTranslations|esTranslations|frTranslations|hiTranslations|itTranslations|jaTranslations|koTranslations|nlTranslations|ptTranslations|ruTranslations|trTranslations|ChannelSort|UserSort|ReactionSort|WatcherState|unBlockUser|ReminderResponse|CommandResponse|chatContainer)\b|` / `pnpm why `) shows a single version of each, and `stream-chat` satisfies `peerDependencies['stream-chat']` of the installed `stream-chat-react`.
+4. **Runtime smoke test.** Start the app and check, with two users where a step needs another sender:
+ - The app loads with the channel it opened in v14 (or none, if the human decided so), and the browser console shows no "outside of ``" errors and no workspace-navigation warnings.
+ - Clicking a channel-list row opens that channel and marks the row active; a channel, message or user search result opens a watched channel with its messages.
+ - A message from the other user arrives live; the open channel is marked read; unread badges update.
+ - Sending works; a failed send stays in the list marked failed and can be retried.
+ - Replying opens the thread beside the channel; its close button works; the composer focuses where it did in v14.
+ - Inside `ChatView`: switching to threads and back keeps the open channel and does not reload the list.
+ - Scrolling up loads older messages and keeps the reading position; the scroll-to-latest button returns to the newest messages.
+ - Toggling the network shows the lost-connection notification; messages sent meanwhile appear once after reconnecting.
+ - Channel names and images, dates and date separators render (no empty names, no `Invalid Date`); a non-English UI is still translated.
+ - Logging out and in as another user throws no "pending disposal" error and shows the new user's channels.
+ - The layout matches v14: thread beside the channel, lists sized, custom CSS applied.
+ - Each feature the app customizes still works: message actions (pin, delete, mute, flag), attachments and the attachment menu, voice messages, polls, the document title.
+
+---
+
## Index of breaking changes
`BC-###` IDs refer to [`ai-docs/v15-breaking-changes.md`](https://github.com/GetStream/stream-chat-react/blob/master/ai-docs/v15-breaking-changes.md).
-| ID | Change | Section |
-| ------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
-| BC-001 | `stream-chat` ^10, dependencies, single `@stream-io/state-store`, new subpaths | [1](#packages) |
-| BC-002 | Timestamps are `TimestampNS` | [16](#16-dates-on-response-types-are-unix-nanosecond-numbers) |
-| BC-003 | `ChannelStateContext` / `ChannelActionContext` removed | [9](#channelstatecontext-and-channelactioncontext-are-removed) |
-| BC-004 | `TypingContext` removed | [9](#typingcontext-is-removed) |
-| BC-005 | `ComponentContext` slots removed | [10](#removed-componentcontext-slots) |
-| BC-006 | Context hooks take no `componentName` | [10](#context-hooks-take-no-componentname-argument) |
-| BC-007 | Required context hooks throw | [10](#required-context-hooks-throw-outside-their-provider) |
-| BC-008 | `useMessageComposerContext` takes no argument and throws | [10](#required-context-hooks-throw-outside-their-provider) |
-| BC-009 | `ChannelUnreadUiState` removed | [9](#the-unread-snapshot-channelunreaduistate-is-removed) |
-| BC-010 | `Window` removed | [3](#window-and-windowprops-are-removed) |
-| BC-011 | `ChatView` moved to `slot-layout` | [1](#imports-that-moved), [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
-| BC-012 | `ChatView` rebuilt around slots | [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
-| BC-013 | `setActiveChannel` / `channel` removed; workspace navigation | [2](#chatcontext-fields), [4](#4-selecting-channels-and-opening-threads-workspace-navigation) |
-| BC-014 | `ChatContext.channelsQueryState` removed | [2](#chatcontext-fields) |
-| BC-015 | `ChatContext.latestMessageDatesByChannels` removed | [2](#chatcontext-fields) |
-| BC-016 | `useChatContext` argument, throws, `channelManager` | [2](#chatcontext-fields), [10](#context-hooks-take-no-componentname-argument) |
-| BC-017 | `useChat` params and results; `Chat.defaultLanguage` | [2](#usechat-and-chatdefaultlanguage) |
-| BC-018 | `useCreateChatClient` `options` → `config` | [2](#usecreatechatclient-options--config) |
-| BC-019 | `Channel.channel` required, `EmptyPlaceholder` removed | [5](#channel-requires-channel-and-renders-no-placeholder-loading-or-error-state) |
-| BC-020 | `Channel` no longer watches; `getChannel` removed | [5](#channel-no-longer-watches-the-channel) |
-| BC-021 | `document.title`, `activeUnreadHandler` | [5](#channel-no-longer-writes-documenttitle) |
-| BC-022 | `markReadOnMount` removed | [5](#markreadonmount-is-removed-the-list-marks-read) |
-| BC-023 | `do*Request` props, `useChannelEditMessageHandler` | [14](#14-request-handlers) |
-| BC-024 | Attachment props moved to `Attachment` | [5](#props-that-moved-off-channel) |
-| BC-025 | Mention handlers moved to the lists | [5](#props-that-moved-off-channel) |
-| BC-026 | `skipMessageDataMemoization` removed | [5](#props-that-moved-off-channel) |
-| BC-027 | `Channel` lifecycle behavior | [5](#channel-lifecycle-activation-no-remount-no-reload-no-search-jump) |
-| BC-028 | `ChannelList` takes a `ChannelPaginator` | [6](#channellist-is-driven-by-a-channelpaginator) |
-| BC-029 | `ChannelListUI` removed | [6](#channellistui-is-removed-use-the-list-slots) |
-| BC-030 | `usePaginatedChannels`, `useConnectionRecoveredListener`, query types removed | [6](#channel-list-hooks-types-and-utilities-are-removed) |
-| BC-031 | Channel-list utilities removed | [6](#channel-list-hooks-types-and-utilities-are-removed) |
-| BC-032 | `ChannelListContext` fields removed | [6](#channellistcontext-lost-channels-hasnextpage-loadnextpage-and-setchannels) |
-| BC-033 | `ChannelListItem` props | [6](#channellistitem-props) |
-| BC-034 | `HeaderStartContent` → `StartContent` / `EndContent` | [8](#8-channel-and-thread-headers) |
-| BC-035 | `Thread` takes `thread`, renders children | [7](#thread-takes-a-thread-prop-and-renders-its-children) |
-| BC-036 | `ThreadHeader` props, `ComponentContext.ThreadHeader` | [7](#threadheader-lost-closethread-and-thread) |
-| BC-037 | `ThreadProvider` no longer mounts a `Channel` | [7](#threadprovider-no-longer-wraps-its-children-in-a-channel) |
-| BC-038 | `ThreadList` reload and selection | [7](#threadlist), [4](#4-selecting-channels-and-opening-threads-workspace-navigation) |
-| BC-039 | Search results not watched; selection through navigation | [4](#search-results) |
-| BC-040 | Lost-connection notification | [2](#the-lost-connection-notification) |
-| BC-041 | `messageActions` and related exports removed | [10](#messageactions-and-message_actions-are-removed) |
-| BC-042 | Message UI overrides on `MessageUI` | [10](#message-ui-overrides-consolidate-on-the-messageui-slot) |
-| BC-043 | `MessageContext.mutes` / `messageIsUnread` removed | [12](#messagecontextmutes-and-messageisunread-are-removed) |
-| BC-044 | `retrySendMessage`, `useRetryHandler` | [12](#retrysendmessage-and-useretryhandler) |
-| BC-045 | Opening a thread from a message | [4](#messageopenthread-and-the-lists-openthread) |
-| BC-046 | List pagination and state props | [11](#pagination-and-state-props-are-removed) |
-| BC-047 | `messages` prop removed | [11](#the-messages-prop-is-removed) |
-| BC-048 | `threadList` / `head` removed | [11](#threadlist-and-head-are-removed-lists-detect-the-thread) |
-| BC-049 | `withDateSeparator` | [11](#disabledateseparator--withdateseparator) |
-| BC-050 | `AIStates` from `stream-chat` | [12](#aistates-moved-to-stream-chat) |
-| BC-051 | One voice message at a time | [12](#allowconcurrentplayback-is-removed-one-voice-message-plays-at-a-time) |
-| BC-052 | `usePinHandler` no `canPin` | [12](#usepinhandler-no-longer-returns-canpin) |
-| BC-053 | `VirtuosoContext` reshaped | [11](#virtuosocontext-the-context-of-additionalvirtuosopropscomponents) |
-| BC-054 | `RenderMessagesOptions` | [11](#custom-rendermessages-gets-different-options) |
-| BC-055 | Low-level list hooks and utils | [11](#low-level-list-hooks-and-utils) |
-| BC-056 | `headerPosition` / `insertIntro` in nanoseconds | [11](#headerposition-and-insertintro-take-unix-nanoseconds) |
-| BC-057 | `DateSeparatorMessage` | [11](#dateseparatormessage) |
-| BC-058 | Standalone `Message` needs a channel | [12](#a-standalone-message-needs-a-channel-in-context) |
-| BC-059 | Gallery header timestamp | [12](#the-gallery-header-renders-the-messagetimestamp-override-for-items-that-carry-their-message) |
-| BC-060 | `overrideSubmitHandler` removed | [13](#overridesubmithandler-is-removed) |
-| BC-061 | `preventClearingOnUnmount` removed | [13](#preventclearingonunmount-is-removed) |
-| BC-062 | `handleSubmit` removed | [13](#handlesubmit-is-removed) |
-| BC-063 | Failed sends do not restore the composer | [13](#a-failed-send-or-edit-no-longer-restores-the-composer) |
-| BC-064 | `hasCustomDoUploadRequest` → `customCdn` | [13](#useattachmentmanagerstate-hascustomdouploadrequest--customcdn) |
-| BC-065 | `AttachmentSelector` follows resolved config | [13](#attachment-and-poll-availability) |
-| BC-066 | Upload helpers moved to `stream-chat` | [1](#imports-that-moved), [13](#upload-helpers-moved-to-stream-chat) |
-| BC-067 | English-only bundle, namespaced keys | [18](#18-i18n) |
-| BC-068 | Strict `t` | [18](#18-i18n) |
-| BC-069 | `Streami18n` API | [18](#18-i18n) |
-| BC-070 | Channel and navigation types follow v10 | [17](#17-other-stream-chat-v10-data-shapes) |
-| BC-071 | Message-area types follow v10 | [17](#17-other-stream-chat-v10-data-shapes) |
-| BC-072 | Composer types follow v10 | [17](#17-other-stream-chat-v10-data-shapes) |
-| BC-073 | `Window` / `str-chat__main-panel` / `str-chat__container` CSS | [19](#19-css-and-markup) |
-| BC-074 | Channel list CSS | [19](#19-css-and-markup) |
-| BC-075 | `ChatView` view wrappers | [19](#19-css-and-markup) |
-| BC-076 | `str-chat__thread--virtualized` | [19](#19-css-and-markup) |
-| BC-077 | Quote and edit preview colours | [19](#19-css-and-markup) |
+| ID | Change | Section |
+| ------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
+| BC-001 | `stream-chat` ^10, dependencies, single `@stream-io/state-store`, new subpaths | [1](#packages) |
+| BC-002 | Timestamps are `TimestampNS` | [16](#16-dates-on-response-types-are-unix-nanosecond-numbers) |
+| BC-003 | `ChannelStateContext` / `ChannelActionContext` removed | [9](#channelstatecontext-and-channelactioncontext-are-removed) |
+| BC-004 | `TypingContext` removed | [9](#typingcontext-is-removed) |
+| BC-005 | `ComponentContext` slots removed | [10](#removed-componentcontext-slots) |
+| BC-006 | Context hooks take no `componentName` | [10](#context-hooks-take-no-componentname-argument) |
+| BC-007 | Required context hooks throw | [10](#required-context-hooks-throw-outside-their-provider) |
+| BC-008 | `useMessageComposerContext` takes no argument and throws | [10](#required-context-hooks-throw-outside-their-provider) |
+| BC-009 | `ChannelUnreadUiState` removed | [9](#the-unread-snapshot-channelunreaduistate-is-removed) |
+| BC-010 | `Window` removed | [3](#window-and-windowprops-are-removed) |
+| BC-011 | `ChatView` moved to `slot-layout` | [1](#imports-that-moved), [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
+| BC-012 | `ChatView` rebuilt around slots | [3](#chatview-moved-to-stream-chat-reactslot-layout-and-was-rebuilt-around-slots) |
+| BC-013 | `setActiveChannel` / `channel` removed; workspace navigation | [2](#chatcontext-fields), [4](#4-selecting-channels-and-opening-threads-workspace-navigation) |
+| BC-014 | `ChatContext.channelsQueryState` removed | [2](#chatcontext-fields) |
+| BC-015 | `ChatContext.latestMessageDatesByChannels` removed | [2](#chatcontext-fields) |
+| BC-016 | `useChatContext` argument, throws, `channelManager` | [2](#chatcontext-fields), [10](#context-hooks-take-no-componentname-argument) |
+| BC-017 | `useChat` params and results; `Chat.defaultLanguage` | [2](#usechat-and-chatdefaultlanguage) |
+| BC-018 | `useCreateChatClient` `options` → `config` | [2](#usecreatechatclient-options--config) |
+| BC-019 | `Channel.channel` required, `EmptyPlaceholder` removed | [5](#channel-requires-channel-and-renders-no-placeholder-loading-or-error-state) |
+| BC-020 | `Channel` no longer watches; `getChannel` removed | [5](#channel-no-longer-watches-the-channel) |
+| BC-021 | `document.title`, `activeUnreadHandler` | [5](#channel-no-longer-writes-documenttitle) |
+| BC-022 | `markReadOnMount` removed | [5](#markreadonmount-is-removed-the-list-marks-read) |
+| BC-023 | `do*Request` props, `useChannelEditMessageHandler` | [14](#14-request-handlers) |
+| BC-024 | Attachment props moved to `Attachment` | [5](#props-that-moved-off-channel) |
+| BC-025 | Mention handlers moved to the lists | [5](#props-that-moved-off-channel) |
+| BC-026 | `skipMessageDataMemoization` removed | [5](#props-that-moved-off-channel) |
+| BC-027 | `Channel` lifecycle behavior | [5](#channel-lifecycle-activation-no-remount-no-reload-no-search-jump) |
+| BC-028 | `ChannelList` takes a `ChannelPaginator` | [6](#channellist-is-driven-by-a-channelpaginator) |
+| BC-029 | `ChannelListUI` removed | [6](#channellistui-is-removed-use-the-list-slots) |
+| BC-030 | `usePaginatedChannels`, `useConnectionRecoveredListener`, query types removed | [6](#channel-list-hooks-types-and-utilities-are-removed) |
+| BC-031 | Channel-list utilities removed | [6](#channel-list-hooks-types-and-utilities-are-removed) |
+| BC-032 | `ChannelListContext` fields removed | [6](#channellistcontext-lost-channels-hasnextpage-loadnextpage-and-setchannels) |
+| BC-033 | `ChannelListItem` props | [6](#channellistitem-props) |
+| BC-034 | `HeaderStartContent` → `StartContent` / `EndContent` | [8](#8-channel-and-thread-headers) |
+| BC-035 | `Thread` takes `thread`, renders children | [7](#thread-takes-a-thread-prop-and-renders-its-children) |
+| BC-036 | `ThreadHeader` props, `ComponentContext.ThreadHeader` | [7](#threadheader-lost-closethread-and-thread) |
+| BC-037 | `ThreadProvider` no longer mounts a `Channel` | [7](#threadprovider-no-longer-wraps-its-children-in-a-channel) |
+| BC-038 | `ThreadList` reload and selection | [7](#threadlist), [4](#4-selecting-channels-and-opening-threads-workspace-navigation) |
+| BC-039 | Search results not watched; selection through navigation | [4](#search-results) |
+| BC-040 | Lost-connection notification | [2](#the-lost-connection-notification) |
+| BC-041 | `messageActions` and related exports removed | [10](#messageactions-and-message_actions-are-removed) |
+| BC-042 | Message UI overrides on `MessageUI` | [10](#message-ui-overrides-consolidate-on-the-messageui-slot) |
+| BC-043 | `MessageContext.mutes` / `messageIsUnread` removed | [12](#messagecontextmutes-and-messageisunread-are-removed) |
+| BC-044 | `retrySendMessage`, `useRetryHandler` | [12](#retrysendmessage-and-useretryhandler) |
+| BC-045 | Opening a thread from a message | [4](#messageopenthread-and-the-lists-openthread) |
+| BC-046 | List pagination and state props | [11](#pagination-and-state-props-are-removed) |
+| BC-047 | `messages` prop removed | [11](#the-messages-prop-is-removed) |
+| BC-048 | `threadList` / `head` removed | [11](#threadlist-and-head-are-removed-lists-detect-the-thread) |
+| BC-049 | `withDateSeparator` | [11](#disabledateseparator--withdateseparator) |
+| BC-050 | `AIStates` from `stream-chat` | [12](#aistates-moved-to-stream-chat) |
+| BC-051 | One voice message at a time | [12](#allowconcurrentplayback-is-removed-one-voice-message-plays-at-a-time) |
+| BC-052 | `usePinHandler` no `canPin` | [12](#usepinhandler-no-longer-returns-canpin) |
+| BC-053 | `VirtuosoContext` reshaped | [11](#virtuosocontext-the-context-of-additionalvirtuosopropscomponents) |
+| BC-054 | `RenderMessagesOptions` | [11](#custom-rendermessages-gets-different-options) |
+| BC-055 | Low-level list hooks and utils | [11](#low-level-list-hooks-and-utils) |
+| BC-056 | `headerPosition` / `insertIntro` in nanoseconds | [11](#headerposition-and-insertintro-take-unix-nanoseconds) |
+| BC-057 | `DateSeparatorMessage` | [11](#dateseparatormessage) |
+| BC-058 | Standalone `Message` needs a channel | [12](#a-standalone-message-needs-a-channel-in-context) |
+| BC-059 | Gallery header timestamp | [12](#the-gallery-header-renders-the-messagetimestamp-override-for-items-that-carry-their-message) |
+| BC-060 | `overrideSubmitHandler` removed | [13](#overridesubmithandler-is-removed) |
+| BC-061 | `preventClearingOnUnmount` removed | [13](#preventclearingonunmount-is-removed) |
+| BC-062 | `handleSubmit` removed | [13](#handlesubmit-is-removed) |
+| BC-063 | Failed sends do not restore the composer | [13](#a-failed-send-or-edit-no-longer-restores-the-composer) |
+| BC-064 | `hasCustomDoUploadRequest` → `customCdn` | [13](#useattachmentmanagerstate-hascustomdouploadrequest--customcdn) |
+| BC-065 | `AttachmentSelector` follows resolved config | [13](#attachment-and-poll-availability) |
+| BC-066 | Upload helpers moved to `stream-chat` | [1](#imports-that-moved), [13](#upload-helpers-moved-to-stream-chat) |
+| BC-067 | English-only bundle, namespaced keys | [18](#18-i18n) |
+| BC-068 | Strict `t` | [18](#18-i18n) |
+| BC-069 | `Streami18n` API | [18](#18-i18n) |
+| BC-070 | Channel and navigation types follow v10 | [17](#17-other-stream-chat-v10-data-shapes) |
+| BC-071 | Message-area types follow v10 | [17](#17-other-stream-chat-v10-data-shapes) |
+| BC-072 | Composer types follow v10 | [17](#17-other-stream-chat-v10-data-shapes) |
+| BC-073 | `Window` / `str-chat__main-panel` / `str-chat__container` CSS | [19](#19-css-and-markup) |
+| BC-074 | Channel list CSS | [19](#19-css-and-markup) |
+| BC-075 | `ChatView` view wrappers | [19](#19-css-and-markup) |
+| BC-076 | `str-chat__thread--virtualized` | [19](#19-css-and-markup) |
+| BC-077 | Quote and edit preview colours | [19](#19-css-and-markup) |
+| BC-078 | `useUserRole().canEdit` is the permission alone | [10](#messageactions-and-message_actions-are-removed) |
+| BC-079 | Action rules moved onto `defaultMessageActionSet` items (`isVisible`) | [10](#messageactions-and-message_actions-are-removed) |
+| BC-080 | Quote above poll; `--has-poll` / `--has-single-attachment` | [12](#new-the-message-bubbles-content-is-a-configurable-set-the-quoted-message-renders-above-a-poll), [19](#19-css-and-markup) |
+| BC-081 | Custom preview slots mount only while their item is visible | [13](#new-composer-previews-are-a-configurable-set) |
+| BC-082 | Message action handlers reject | [12](#message-action-handlers-reject-when-they-fail) |
+| BC-083 | First message loses its top padding | [19](#19-css-and-markup) |
diff --git a/ai-docs/ai-migration.md b/ai-docs/ai-migration.md
index 557363c77..8d13a07eb 100644
--- a/ai-docs/ai-migration.md
+++ b/ai-docs/ai-migration.md
@@ -1,5 +1,7 @@
# stream-chat-react v13 → v14 AI Migration Guide
+> **Upgrading from v14 to v15?** This guide covers v13 → v14 only. Use [`ai-migration-v14-v15.md`](https://github.com/GetStream/stream-chat-react/blob/master/ai-docs/ai-migration-v14-v15.md) (raw: `https://raw.githubusercontent.com/GetStream/stream-chat-react/master/ai-docs/ai-migration-v14-v15.md`).
+
Execution-only guide for a coding agent migrating a third-party app from `stream-chat-react` v13 to v14. Run the phases in order; stop and fix each phase's errors before moving to the next. For evidence or edge cases, fall back to `./breaking-changes.md`.
## Source of Truth
diff --git a/ai-docs/v15-breaking-changes.md b/ai-docs/v15-breaking-changes.md
index daa58bbee..197f04b3c 100644
--- a/ai-docs/v15-breaking-changes.md
+++ b/ai-docs/v15-breaking-changes.md
@@ -1,6 +1,6 @@
# React v15 Breaking Changes
-Last updated: 2026-10-17
+Last updated: 2026-11-03
## Scope
@@ -16,7 +16,8 @@ This file tracks confirmed v14 to v15 breaking changes in the public API of `str
- Baseline: `release-v14` @ `a61c0c419` (`fix(Attachment): render giphy attachments in their own group with uni… (#3325)`)
- Branch point: `0fb5ac306` (`chore(release): 14.12.0`)
- Audited head: `master` @ `557ffcb3c` (`feat!: pass the stream-chat configuration tree through useCreateChatClient (#3321)`), resolved against `stream-chat@10.0.0-rc.22`
-- Future mining starting point: diff `557ffcb3c..HEAD` first, then re-run the surface diff below against `release-v14` and resolve every new gated change.
+- Staleness pass: `0100d98ce..1c02ba07f` (2026-11-03), checked commit by commit for public API and default-behavior changes; findings are BC-078 to BC-083 under [Later changes](#later-changes-0100d98ce1c02ba07f).
+- Future mining starting point: diff `1c02ba07f..HEAD` first, then re-run the surface diff below against `release-v14` and resolve every new gated change.
Evidence was gathered four ways, and each confirmed entry rests on at least one of them:
@@ -63,7 +64,7 @@ yarn api-surface check --doc ai-docs/v15-breaking-changes.md --diff diff.json
## Index
-77 entries.
+83 entries.
| ID | Title | Group |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
@@ -144,9 +145,17 @@ yarn api-surface check --doc ai-docs/v15-breaking-changes.md --diff diff.json
| [BC-075](#bc-075) | CSS, ChatView: `str-chat__chat-view__channels` / `__threads` render only in `views` mode | CSS and markup |
| [BC-076](#bc-076) | CSS, Thread: `str-chat__thread--virtualized` is no longer applied | CSS and markup |
| [BC-077](#bc-077) | quote and edit previews set their text colour from the matching bubble token | CSS and markup |
+| [BC-078](#bc-078) | `useUserRole(message).canEdit` is the update permission alone | Messages and message lists |
+| [BC-079](#bc-079) | the message-specific rules of `edit`, `copyMessageText` and `download` moved onto `defaultMessageActionSet` items as `isVisible` | Messages and message lists |
+| [BC-080](#bc-080) | the quoted message renders above a poll; poll messages get `str-chat__message--has-poll` and never `--has-single-attachment` | Messages and message lists |
+| [BC-081](#bc-081) | custom `AttachmentPreviewList`, `VoiceRecordingPreviewSlot` and `LinkPreviewList` mount only while their preview item is visible | Message composer |
+| [BC-082](#bc-082) | message action handlers reject when the request fails or when called without the message | Messages and message lists |
+| [BC-083](#bc-083) | CSS, the first message of a channel list loses its `4.5rem` top padding | CSS and markup |
## Package and runtime
+
+
### BC-001: package: `stream-chat` peer is `^10`; `dayjs` / `i18next` are no longer dependencies; `@stream-io/i18n` and `@stream-io/state-store` are added and must resolve to one copy; new subpath exports
- Status: confirmed
@@ -171,6 +180,8 @@ yarn api-surface check --doc ai-docs/v15-breaking-changes.md --diff diff.json
- `01-basics/02-installation.md` (peer and dependency list), `04-guides/03-translations.md` (dayjs locale imports), `04-guides/07-sdk-state-management.md` (`StateStore` import)
- Example needed: no
+
+
### BC-002: server timestamps are `TimestampNS` (branded unix-nanosecond number), not `Date`, in React types that carry response data
- Status: confirmed
@@ -224,6 +235,8 @@ export const DayDivider = ({ message }: { message: LocalMessage }) => {
## Contexts and customization
+
+
### BC-003: `ChannelStateContext` / `ChannelActionContext` and their hooks, providers and types are removed
- Status: confirmed
@@ -254,21 +267,21 @@ export const DayDivider = ({ message }: { message: LocalMessage }) => {
| `notifications` | none. Notifications go through `client.notifications`. |
| `suppressAutoscroll`, `threadSuppressAutoscroll`, `highlightedMessageId`, `loadingMoreForJumpToChannelMessage` | none (internal to the message lists) |
- | v14 (`useChannelActionContext`) | v15 |
- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
- | `loadMore` (older) / `loadMoreNewer` | `channel.messagePaginator.toTail()` / `.toHead()`. `MessageList` uses exactly these: `toTail` on scroll-to-top, `toHead` on scroll-to-bottom. |
- | `loadMoreThread` | `thread.messagePaginator.toTail()` |
- | `jumpToMessage(id)` / `jumpToLatestMessage()` / `jumpToFirstUnreadMessage()` | `channel.messagePaginator.jumpToMessage(id)` / `.jumpToTheLatestMessage()` / `.jumpToTheFirstUnreadMessage()` |
- | `sendMessage` | `messageComposer.send()` (or `useMessageComposerSubmitFn()`). Lower level: `channel.messageOperations.send({ localMessage, message, options })` |
- | `retrySendMessage` / `RetrySendMessage` | `channel.messageOperations.retry({ localMessage })`, or `handleRetry` from `useMessageContext()` |
- | `editMessage` | `messageComposer.update()` or `channel.messageOperations.update({ localMessage, options })` |
- | `deleteMessage` | `channel.messageOperations.delete({ localMessage, options })`, or `handleDelete` from `useMessageContext()` |
- | `updateMessage` / `removeMessage` (local-only list writes) | `channel.messagePaginator.ingestItem(message)` / `.removeItem({ id })`. Normally unnecessary: the LLC applies WS events itself. |
- | `markRead` / `MarkReadWrapperOptions` | `channel.markRead()`. The unread UI snapshot is separate state on the paginator; there is no `updateChannelUiUnreadState` option. |
- | `setChannelUnreadUiState` | `channel.messagePaginator.setUnreadSnapshot(partial)` |
- | `openThread` / `closeThread` | `useWorkspaceNavigation().openThread({ channel, message })` / `.closeThread(threadId)`. Inside a thread, `useCloseThread()`. |
- | `onMentionsClick` / `onMentionsHover` | the `onMentionsClick` / `onMentionsHover` props on `MessageList` / `Message` |
- | `dispatch` | none, removed (there is no React reducer) |
+ | v14 (`useChannelActionContext`) | v15 |
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+ | `loadMore` (older) / `loadMoreNewer` | `channel.messagePaginator.toTail()` / `.toHead()`. `MessageList` uses exactly these: `toTail` on scroll-to-top, `toHead` on scroll-to-bottom. |
+ | `loadMoreThread` | `thread.messagePaginator.toTail()` |
+ | `jumpToMessage(id)` / `jumpToLatestMessage()` / `jumpToFirstUnreadMessage()` | `channel.messagePaginator.jumpToMessage(id)` / `.jumpToTheLatestMessage()` / `.jumpToTheFirstUnreadMessage()` |
+ | `sendMessage` | `messageComposer.send()` (or `useMessageComposerSubmitFn()`). Lower level: `channel.messageOperations.send({ localMessage, message, options })` |
+ | `retrySendMessage` / `RetrySendMessage` | `channel.messageOperations.retry({ localMessage })`, or `handleRetry` from `useMessageContext()` |
+ | `editMessage` | `messageComposer.update()` or `channel.messageOperations.update({ localMessage, options })` |
+ | `deleteMessage` | `channel.messageOperations.delete({ localMessage, options })`, or `handleDelete` from `useMessageContext()` |
+ | `updateMessage` / `removeMessage` (local-only list writes) | `channel.messagePaginator.ingestItem(message)` / `.removeItem({ id })`. Normally unnecessary: the LLC applies WS events itself. |
+ | `markRead` / `MarkReadWrapperOptions` | `channel.markReadViaReporter()` (`channel.markRead()` is now the direct request: it skips the reporter and the `markReadRequest` handler). The unread UI snapshot is separate state on the paginator; there is no `updateChannelUiUnreadState` option. |
+ | `setChannelUnreadUiState` | `channel.messagePaginator.setUnreadSnapshot(partial)` |
+ | `openThread` / `closeThread` | `useWorkspaceNavigation().openThread({ channel, message })` / `.closeThread(threadId)`. Inside a thread, `useCloseThread()`. |
+ | `onMentionsClick` / `onMentionsHover` | the `onMentionsClick` / `onMentionsHover` props on `MessageList` / `Message` |
+ | `dispatch` | none, removed (there is no React reducer) |
Custom send/update/delete/markRead request overrides are registered on `client.config` (see "Per-component request-handler props removed" in `ai-migration-v14-v15.md`).
@@ -342,6 +355,8 @@ export const LoadOlder = () => {
};
```
+
+
### BC-004: `TypingContext`, `TypingProvider` and `useTypingContext` are removed
- Status: confirmed
@@ -391,6 +406,8 @@ export const TypingCount = () => {
};
```
+
+
### BC-005: `ComponentContext` slots `Message`, `ThreadHeader`, `HeaderStartContent` and `ChannelListUI` are removed
- Status: confirmed
@@ -487,6 +504,8 @@ export const Layout = ({
);
```
+
+
### BC-006: context hooks no longer accept a `componentName` argument
- Status: confirmed
@@ -529,6 +548,8 @@ export const CustomEmojiPicker = () => {
};
```
+
+
### BC-007: required context hooks throw outside their provider
- Status: confirmed
@@ -550,6 +571,8 @@ export const CustomEmojiPicker = () => {
- any cookbook page whose custom component is shown mounted outside its provider (not enumerable by grep)
- Example needed: no
+
+
### BC-008: `useMessageComposerContext` takes no argument and throws outside its provider
- Status: confirmed
@@ -573,6 +596,8 @@ export const CustomEmojiPicker = () => {
- `03-ui-cookbook/07-emoji_picker.md` (line 76, `useMessageComposerContext("CustomEmojiPicker")`)
- Example needed: no (one-line change; the guide's "Context hooks" section already shows it)
+
+
### BC-009: `ChannelUnreadUiState` type is removed
- Status: confirmed
@@ -593,6 +618,8 @@ export const CustomEmojiPicker = () => {
## Chat, ChatView and layout
+
+
### BC-010: `Window` and `WindowProps` were removed
- Status: confirmed
@@ -676,6 +703,8 @@ const ChannelPanel = ({
---
+
+
### BC-011: `ChatView` and its selector helpers moved to the `stream-chat-react/slot-layout` entry point
- Status: confirmed
@@ -699,6 +728,8 @@ const ChannelPanel = ({
---
+
+
### BC-012: `ChatView` was rebuilt around layout slots: `ChatView.Channels` / `ChatView.Threads` / `ChatView.ThreadAdapter`, `useThreadsViewContext` and `ThreadsViewContextValue` were removed, and `ChatViewContext` renamed its fields
- Status: confirmed
@@ -824,6 +855,8 @@ A threads view is declared the same way (a `{ id: 'threads', slots: [...] }` lay
---
+
+
### BC-013: `ChatContext.setActiveChannel` / `ChatContext.channel` were removed; selecting a channel or thread now goes through workspace navigation, which does nothing outside `ChatView`
- Status: confirmed
@@ -952,6 +985,8 @@ To open threads without `ChatView`, also override `openThread` (a target is a `T
---
+
+
### BC-014: `ChatContext.channelsQueryState` was removed
- Status: confirmed
@@ -970,6 +1005,8 @@ To open threads without `ChatView`, also override `openThread` (a target is a `T
---
+
+
### BC-015: `ChatContext.latestMessageDatesByChannels` was removed
- Status: confirmed
@@ -992,6 +1029,8 @@ To open threads without `ChatView`, also override `openThread` (a target is a `T
---
+
+
### BC-016: `useChatContext` lost its `componentName` argument, throws outside ``, and `ChatContextValue` gained a required `channelManager`
- Status: confirmed
@@ -1016,6 +1055,8 @@ To open threads without `ChatView`, also override `openThread` (a target is a `T
---
+
+
### BC-017: `useChat` lost its i18n params and return fields; `Chat.defaultLanguage` was removed
- Status: confirmed
@@ -1041,6 +1082,8 @@ To open threads without `ChatView`, also override `openThread` (a target is a `T
---
+
+
### BC-018: `useCreateChatClient`'s `options` was renamed to `config` and takes the stream-chat configuration tree
- Status: confirmed
@@ -1092,6 +1135,8 @@ const client = useCreateChatClient({
## Channel
+
+
### BC-019: `ChannelProps.channel` is required and `EmptyPlaceholder` was removed; `Channel` renders no loading, error or empty state
- Status: confirmed
@@ -1149,6 +1194,8 @@ import {
---
+
+
### BC-020: `Channel` no longer watches the channel: `initializeOnMount` and `channelQueryOptions` were removed, and so was `getChannel`
- Status: confirmed
@@ -1209,6 +1256,8 @@ After (v15):
const channel = await client.channel('messaging', 'general').watch();
```
+
+
### BC-021: `Channel` no longer writes `document.title`; `activeUnreadHandler` was removed
- Status: confirmed
@@ -1228,6 +1277,8 @@ const channel = await client.channel('messaging', 'general').watch();
---
+
+
### BC-022: `markReadOnMount` was removed; `Channel` no longer marks the channel read when it mounts
- Status: confirmed
@@ -1248,6 +1299,8 @@ const channel = await client.channel('messaging', 'general').watch();
---
+
+
### BC-023: `doSendMessageRequest`, `doUpdateMessageRequest`, `doDeleteMessageRequest`, `doMarkReadRequest` and `useChannelEditMessageHandler` were removed
- Status: confirmed
@@ -1301,6 +1354,8 @@ client.config.set({
---
+
+
### BC-024: `giphyVersion`, `imageAttachmentSizeHandler`, `videoAttachmentSizeHandler` and `shouldGenerateVideoThumbnail` moved from `Channel` to `Attachment`
- Status: confirmed
@@ -1349,6 +1404,8 @@ Note: a bare `AttachmentContextProvider` above the list does not reach message a
---
+
+
### BC-025: `Channel.onMentionsClick` / `onMentionsHover` and `useChannelMentionsHandler` were removed
- Status: confirmed
@@ -1384,6 +1441,8 @@ After (v15):
---
+
+
### BC-026: `Channel.skipMessageDataMemoization` was removed
- Status: confirmed
@@ -1402,6 +1461,8 @@ After (v15):
---
+
+
### BC-027: `Channel` behavior changes with the same signature: no remount per channel, activation, no reload on reconnect, no search jump
- Status: confirmed
@@ -1422,12 +1483,14 @@ After (v15):
- Docs impact:
- `02-ui-components/04-channel/01-channel.md`
- Example needed: no
-- Guide check: `ai-migration-v14-v15.md` "`` declares the channel active, and owns its message window" still says `Channel` "calls `channel.reload()` on `connection.recovered`" and tells custom surfaces to do it. That was removed by `3098a90a6`; the reload half of that section is outdated.
+- Guide check: fixed. The guide (section 5, "Channel lifecycle") says the client reloads active channels after a reconnect (`client.connectionRecovery`, stream-chat-js `ConnectionRecoveryManager`) and that a manual `connection.recovered` reload would reload twice. Earlier drafts told custom surfaces to reload there (removed by `3098a90a6`).
---
## Channel list
+
+
### BC-028: `ChannelList` is driven by a `ChannelPaginator`: `paginator` is required and every query, event and render prop was removed
- Status: confirmed
@@ -1509,6 +1572,8 @@ A single list can still be rendered on its own with `
+
### BC-029: `ChannelListUI`, `ChannelListUIProps` and `ComponentContext.ChannelListUI` were removed
- Status: confirmed
@@ -1561,6 +1626,8 @@ Note: these slots are generic "paginated list" slots; in v15 only `ChannelList`
---
+
+
### BC-030: `usePaginatedChannels`, `useConnectionRecoveredListener` and the `CustomQueryChannelParams` / `CustomQueryChannelsFn` / `EffectiveQueryParams` types were removed
- Status: confirmed
@@ -1582,6 +1649,8 @@ Note: these slots are generic "paginated list" slots; in v15 only `ChannelList`
---
+
+
### BC-031: the channel-list utilities were removed: `moveChannelUpwards`, `findLastPinnedChannelIndex`, `extractSortValue`, `isChannelPinned`, `isChannelArchived`, `shouldConsiderPinnedChannels`, `shouldConsiderArchivedChannels`
- Status: confirmed
@@ -1600,6 +1669,8 @@ Note: these slots are generic "paginated list" slots; in v15 only `ChannelList`
---
+
+
### BC-032: `ChannelListContext` lost `channels`, `hasNextPage`, `loadNextPage` and `setChannels`
- Status: confirmed
@@ -1640,6 +1711,8 @@ const { paginator } = useChannelListContext();
---
+
+
### BC-033: `ChannelListItem` / `ChannelListItemUI` / `ChannelListItemTimestamp` props changed: `lastMessage` -> `previewedMessage`, `latestMessagePreview` and `getLatestMessagePreview` removed, selection props removed
- Status: confirmed
@@ -1719,6 +1792,8 @@ Guide check: `ai-migration-v14-v15.md` "`ChannelListItem` `getLatestMessagePrevi
## Headers, threads and search
+
+
### BC-034: `ComponentContext.HeaderStartContent` was removed; `ChannelHeader` and `ThreadHeader` take `StartContent` / `EndContent` and render navigation buttons by default
- Status: confirmed
@@ -1764,6 +1839,8 @@ const HeaderStart = () => (
---
+
+
### BC-035: `Thread` takes a required `thread` prop and renders its children; its composition props and `useLegacyThreadContext` were removed
- Status: confirmed
@@ -1813,6 +1890,8 @@ Guide check: `ai-migration-v14-v15.md` "`Thread` renders children" says `Thread`
---
+
+
### BC-036: `ThreadHeader` lost its `closeThread` and `thread` props; `ComponentContext.ThreadHeader` was removed
- Status: confirmed
@@ -1884,6 +1963,8 @@ const CustomThreadHeader = () => {
---
+
+
### BC-037: `ThreadProvider` no longer wraps its children in a `Channel`
- Status: confirmed
@@ -1902,6 +1983,8 @@ const CustomThreadHeader = () => {
---
+
+
### BC-038: `ThreadList` reloads the thread list on every mount and selects through workspace navigation
- Status: confirmed
@@ -1923,6 +2006,8 @@ const CustomThreadHeader = () => {
---
+
+
### BC-039: search results are no longer watched, and selecting one opens it through workspace navigation
- Status: confirmed
@@ -1944,6 +2029,8 @@ const CustomThreadHeader = () => {
---
+
+
### BC-040: the lost-connection notification reads connection stores and changes copy for socket drops
- Status: confirmed
@@ -1967,6 +2054,8 @@ const CustomThreadHeader = () => {
## Messages and message lists
+
+
### BC-041: `messageActions` prop, `MESSAGE_ACTIONS`, `OPTIONAL_MESSAGE_ACTIONS`, `getMessageActions`, `MessageActionsArray`, `Capabilities` and `MessageContext.getMessageActions` / `actionsEnabled` removed
- Status: confirmed
@@ -2031,6 +2120,8 @@ export const List = () => (
Note: `MessageActionSetItem` is a union, and its `QuickDropdownToggleActionSetItem` member has no `type` field (`master:src/components/MessageActions/MessageActions.tsx:55`–`:63`). So `filter(({ type }) => …)`, as written in the existing `ai-migration-v14-v15.md` section, should not type-check (`TS2339`). Hence the `'type' in item` guard above. This was reasoned from the type, not compiled.
+
+
### BC-042: message UI component overrides consolidate on the `MessageUI` slot (`ComponentContext.Message` and the `Message` prop removed)
- Status: confirmed
@@ -2099,6 +2190,8 @@ export const Lists = () => (
);
```
+
+
### BC-043: `MessageContext.mutes` and `MessageContext.messageIsUnread` removed
- Status: confirmed
@@ -2120,6 +2213,8 @@ export const Lists = () => (
- `02-ui-components/08-message/02-message_context.md` (`messageIsUnread`, `mutes`)
- Example needed: no
+
+
### BC-044: `retrySendMessage` prop removed; `useRetryHandler` takes no argument and returns a params-object handler
- Status: confirmed
@@ -2172,6 +2267,8 @@ export const RetryButton = () => {
};
```
+
+
### BC-045: opening a thread from a message goes through workspace navigation; `openThread` is gone from the lists and does nothing without a navigation provider
- Status: confirmed
@@ -2276,6 +2373,8 @@ export const ChannelWithThread = ({ channel }: { channel: StreamChannel }) => {
(Types and exports were checked against V15 source. The snippet has not been run in an app.)
+
+
### BC-046: list pagination and state props removed; the lists read the message paginator
- Status: confirmed
@@ -2308,6 +2407,8 @@ export const ChannelWithThread = ({ channel }: { channel: StreamChannel }) => {
- `02-ui-components/07-message-list/02-virtualized_list.md` (`hasMore`, `hasMoreNewer`, `loadMoreNewer`, `messageLimit`, `highlightedMessageId`, `channelUnreadUiState`)
- Example needed: no
+
+
### BC-047: `MessageList.messages` / `VirtualizedMessageList.messages` are removed
- Status: confirmed
@@ -2326,6 +2427,8 @@ export const ChannelWithThread = ({ channel }: { channel: StreamChannel }) => {
- `02-ui-components/07-message-list/01-message_list.md:52` and `02-ui-components/07-message-list/02-virtualized_list.md:38` (`messages={customMessages}`)
- Example needed: no
+
+
### BC-048: thread-specific list props `threadList` and `head` removed; lists detect the thread from context
- Status: confirmed
@@ -2356,6 +2459,8 @@ export const ChannelWithThread = ({ channel }: { channel: StreamChannel }) => {
- `02-ui-components/08-message/01-message.md`, `02-ui-components/08-message/02-message_context.md`, `03-ui-cookbook/04-message/01-message_ui.md` mention `threadList`, which is still a `Message` / `MessageContext` field; only the list-level prop is gone, so check each mention
- Example needed: no (see BC-004 for a custom `TypingIndicator`)
+
+
### BC-049: `disableDateSeparator` renamed to `withDateSeparator` (inverted)
- Status: confirmed
@@ -2403,6 +2508,8 @@ export const Lists = () => (
);
```
+
+
### BC-050: `AIStates` is exported by `stream-chat`, not `stream-chat-react`
- Status: confirmed
@@ -2450,6 +2557,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
};
```
+
+
### BC-051: `WithAudioPlayback.allowConcurrentPlayback` removed; one voice message plays at a time, app-wide
- Status: confirmed
@@ -2470,6 +2579,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- `02-ui-components/04-channel/01-channel.md` (`allowConcurrentAudioPlayback`)
- Example needed: no (delete the prop)
+
+
### BC-052: `usePinHandler` no longer returns `canPin`
- Status: confirmed
@@ -2486,6 +2597,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- `02-ui-components/08-message/04-message_hooks.md` (`usePinHandler`, `useUserRole`)
- Example needed: no
+
+
### BC-053: `VirtuosoContext` (context of `additionalVirtuosoProps.components`) reshaped
- Status: confirmed
@@ -2503,6 +2616,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- none found (`VirtuosoContext` is not documented in v14 docs)
- Example needed: no
+
+
### BC-054: custom `renderMessages` receives a different `RenderMessagesOptions`; `ChannelUnreadUiState` removed
- Status: confirmed
@@ -2523,6 +2638,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- `04-guides/05-channel_read_state.md`, `02-ui-components/07-message-list/02-virtualized_list.md` (`channelUnreadUiState`)
- Example needed: no
+
+
### BC-055: exported low-level MessageList hooks and utils changed signature
- Status: confirmed
@@ -2544,6 +2661,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- none found
- Example needed: no
+
+
### BC-056: `MessageList.headerPosition` / `insertIntro` take unix nanoseconds (`TimestampNS`)
- Status: confirmed
@@ -2561,6 +2680,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- `02-ui-components/07-message-list/01-message_list.md` (`headerPosition`)
- Example needed: no
+
+
### BC-057: `DateSeparatorMessage` (in the `RenderedMessage` union) lost `type`; `unread` is optional
- Status: confirmed
@@ -2577,6 +2698,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- none found
- Example needed: no
+
+
### BC-058: a standalone `` needs a channel in context; `highlighted` is no longer derived from channel state
- Status: confirmed
@@ -2597,6 +2720,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- none found for a standalone `Message`
- Example needed: no (the examples hunk is the reference)
+
+
### BC-059: the gallery header renders the `MessageTimestamp` override only for items that carry their message
- Status: confirmed
@@ -2618,6 +2743,8 @@ export const Stop = ({ channel }: { channel: Channel }) => {
## Message composer
+
+
### BC-060: `MessageComposer.overrideSubmitHandler` was removed
- Status: confirmed
@@ -2688,6 +2815,8 @@ client.config.set({
export const Composer = () => ;
```
+
+
### BC-061: `MessageComposer.preventClearingOnUnmount` was removed; a supplied composer is never cleared on unmount
- Status: confirmed
@@ -2737,6 +2866,8 @@ export const InlineEditor = ({ composer }: { composer: Composer }) => (
// When the edit is abandoned, discard it yourself: composer.clear();
```
+
+
### BC-062: `handleSubmit` was removed from `MessageComposerContext` and `useMessageComposerBindings`; submit with `useMessageComposerSubmitFn()` or `messageComposer.send()` / `update()`
- Status: confirmed
@@ -2810,6 +2941,8 @@ export const CompactInput = () => {
};
```
+
+
### BC-063: a failed send or edit no longer restores the composer
- Status: confirmed
@@ -2832,6 +2965,8 @@ export const CompactInput = () => {
- none found in v14 docs (the restore-on-failure behavior was undocumented)
- Example needed: no
+
+
### BC-064: `useAttachmentManagerState().hasCustomDoUploadRequest` was replaced by `customCdn`
- Status: confirmed
@@ -2856,6 +2991,8 @@ export const CompactInput = () => {
- `02-ui-components/09-message-composer/10-message_composer.md` (lines 319-331, `useAttachmentManagerState` section)
- Example needed: no (the guide's two-line snippet is accurate)
+
+
### BC-065: `AttachmentSelector` actions follow the composer's resolved configuration, and `customCdn` decides whether upload permission applies
- Status: confirmed
@@ -2885,6 +3022,8 @@ export const CompactInput = () => {
- Example needed: no
- Note for the guide: `ai-migration-v14-v15.md` § "Attachment/poll availability" says that without `customCdn` "the File action disappears for users without the `upload-file` capability". It already did in v14, since the v14 selector checked `channelCapabilities['upload-file']` directly. The v14→v15 differences are the two bullets under the **File** action above.
+
+
### BC-066: `getAttachmentPreviewUrl`, `hasPendingUploadState` and `isUploadConfirmationPending` moved to `stream-chat`
- Status: confirmed
@@ -2930,6 +3069,8 @@ import {
## i18n
+
+
### BC-067: English-only bundle and namespaced keys. Built-in dictionaries, `isLanguageSupported` and `SupportedTranslations` are removed
- Status: confirmed
@@ -2955,6 +3096,8 @@ import {
- `04-guides/03-translations.md`, `04-guides/08-date-time-formatting.md`, `04-guides/13-notifications.md`, `04-guides/18-accessibility.md`, `02-ui-components/03-chat/01-chat.md` (`SupportedTranslations`), `03-ui-cookbook/13-accessibility/02-custom-announcements.md`
- Example needed: no (`i18n-v15-migration.md` already carries the examples; do not list keys here)
+
+
### BC-068: `t` from `useTranslationContext()` is strictly typed. `t('English text')` and `t(someString)` no longer compile
- Status: confirmed
@@ -3006,6 +3149,8 @@ export const RemoveButton = ({ label }: { label: string }) => {
};
```
+
+
### BC-069: `Streami18n` public API reshaped: members removed, `setLanguage` returns `void`, `t` is read-only, formatter and date-parser types changed
- Status: confirmed
@@ -3069,6 +3214,8 @@ const hasFr = i18n.registeredLanguages.has('fr');
## Types that follow stream-chat v10 (umbrella entries)
+
+
### BC-070: values typed with stream-chat types changed shape in channel/navigation hooks and contexts (umbrella)
- Status: confirmed
@@ -3091,6 +3238,8 @@ const hasFr = i18n.registeredLanguages.has('fr');
---
+
+
### BC-071: message-area props typed with `stream-chat` types change shape with `stream-chat` v10 (umbrella)
- Status: confirmed
@@ -3149,6 +3298,8 @@ export const DeleteForMe = () => {
};
```
+
+
### BC-072: composer prop and type changes that follow stream-chat v10 types (umbrella)
- Status: confirmed
@@ -3181,6 +3332,8 @@ export const DeleteForMe = () => {
## CSS and markup
+
+
### BC-073: CSS, channel layout wrappers: `Window` and the `str-chat__main-panel` / `str-chat__container` wrappers are removed
- Status: confirmed
@@ -3204,6 +3357,8 @@ export const DeleteForMe = () => {
- `02-ui-components/13-window.md` (whole page obsolete), `02-ui-components/01-getting_started.md` (CSS `#root .str-chat__main-panel` and `Window` usage), `02-ui-components/03-chat/01-chat.md` (`chatContainer -> str-chat__container`), and every page whose snippet wraps the list in `` (`02-ui-components/04-channel/01-channel.md`, `02-ui-components/08-message/13-date_separator.md`, …)
- Example needed: no
+
+
### BC-074: CSS, channel list: `ChannelListUI` wrappers removed; the theme scope moves to `ChatView`, with the channel list carrying its own only outside it
- Status: confirmed
@@ -3229,6 +3384,8 @@ export const DeleteForMe = () => {
- `02-ui-components/05-channel-list/06-channel-list-infinite-scroll.md`, `06-release-guides/01-upgrade-to-v14.md` (`str-chat__channel-list-inner`), `02-ui-components/02-theming/01-themingv2.md:172-173` (`customClasses.channelList`)
- Example needed: no
+
+
### BC-075: CSS, ChatView: `str-chat__chat-view__channels` / `__threads` render only in `views` mode
- Status: confirmed
@@ -3245,6 +3402,8 @@ export const DeleteForMe = () => {
- `02-ui-components/11-chat-view.md` (if it shows CSS on these wrappers; not found by grep)
- Example needed: no
+
+
### BC-076: CSS, Thread: `str-chat__thread--virtualized` is no longer applied
- Status: confirmed
@@ -3261,6 +3420,8 @@ export const DeleteForMe = () => {
- none found
- Example needed: no
+
+
### BC-077: quote and edit previews set their text colour from the matching bubble token
- Status: confirmed
@@ -3275,6 +3436,116 @@ export const DeleteForMe = () => {
- `02-ui-components/02-theming/01-themingv2.md` (line 152 text-colour note)
- Example needed: no (the guide section is accurate)
+## Later changes (`0100d98ce..1c02ba07f`)
+
+Found by the staleness pass of 2026-11-03: commits that landed on `master` after this file was last updated and change public behavior relative to `release-v14`. Additive exports from the same range (`ChannelHeader` `Title` / `Subtitle` with `ChannelHeaderTitle`, `ChannelHeaderSubtitle`, `useChannelHeaderOnlineStatus`; `MessageComposerPreviews`, `defaultMessageComposerPreviewSet`, `useMessageComposerPreviewSetFilter`, `useMessageComposerPreviewState`, `EditedMessagePreview`, `GeolocationPreview`; `MessageBubbleContent`, `defaultMessageBubbleContentSet`; `PollCreationDialogControls`) are not breaks and are listed only in the guide.
+
+
+
+### BC-078: `useUserRole(message).canEdit` is the update permission alone
+
+- Status: confirmed
+- Area: message hooks
+- User impact: silent behavior change. v14 `canEdit` was false for a poll message and a giphy. v15 returns the permission only (`update-any-message`, or `update-own-message` on the user's own message), so a custom edit button gated on it now shows on poll messages and giphies.
+- Old API:
+ - `useUserRole(message).canEdit` = `!message.poll && message.command !== 'giphy' && (update-any-message || own && update-own-message)`
+- Replacement: add `!message.poll_id && message.command !== 'giphy'` where the old rule is wanted.
+- Evidence:
+ - `release-v14:src/components/Message/hooks/useUserRole.ts:35-38`
+ - `master:src/components/Message/hooks/useUserRole.ts:29-31`
+ - commit: `1c02ba07f` (PR #3365, `BREAKING CHANGE` footer)
+- Docs impact:
+ - `02-ui-components/08-message/04-message_hooks.md` (`useUserRole`)
+- Example needed: no
+
+
+
+### BC-079: the message-specific rules of `edit`, `copyMessageText` and `download` moved onto `defaultMessageActionSet` items as `isVisible`
+
+- Status: confirmed
+- Area: message actions
+- User impact: silent behavior change for apps that pass their own `messageActionSet`. `useBaseMessageActionSetFilter` no longer hides `edit` on poll messages and giphies, `copyMessageText` on messages without text or `download` on messages without a downloadable attachment; the default items carry those rules as `isVisible`. A hand-written item without `isVisible` shows wherever the base filter allows it. Item rules now also apply with `disableBaseMessageActionSetFilter` (which returned the set unfiltered) and to messages that failed to send for a network reason, so a network-failed poll message or giphy no longer offers Edit.
+- Old API:
+ - `useBaseMessageActionSetFilter` rules for `edit`, `copyMessageText`, `download` (`release-v14:src/components/MessageActions/hooks/useBaseMessageActionSetFilter.ts:79-100`)
+- Replacement: `MessageActionSetItem.isVisible({ isMyMessage, isThreadReply, message })`; copy the default item's `isVisible` into a hand-written item to keep the rule.
+- Evidence:
+ - `master:src/components/MessageActions/MessageActions.defaults.tsx:834,845,857`
+ - `master:src/components/MessageActions/hooks/useBaseMessageActionSetFilter.ts`
+ - commit: `1c02ba07f` (PR #3365, `BREAKING CHANGE` footer)
+- Docs impact:
+ - `02-ui-components/08-message/09-message-actions.md`
+- Example needed: no (the guide has one)
+
+
+
+### BC-080: the quoted message renders above a poll; poll messages get `str-chat__message--has-poll` and never `--has-single-attachment`
+
+- Status: confirmed
+- Area: message rendering, CSS
+- User impact: visual change. v14 rendered the poll above the quoted message inside the bubble; v15 renders the quote first (`defaultMessageBubbleContentSet`: `quotedMessage`, `poll`, `attachments`, `text`). The message root gains `str-chat__message--has-poll` while the message carries a poll and no longer gets `str-chat__message--has-single-attachment` then. App CSS keyed on `--has-single-attachment` stops matching poll messages.
+- Old API:
+ - `release-v14:src/components/Message/MessageUI.tsx:134` (`hasSingleAttachment`), `:246-247` (poll, then quote)
+- Replacement: reorder with a custom `messageBubbleContentSet` installed through the `MessageBubbleContent` slot; target `.str-chat__message--has-poll`.
+- Evidence:
+ - `master:src/components/Message/MessageUI.tsx:108-109,130-132`
+ - `master:src/components/Message/MessageBubbleContent.defaults.tsx`
+ - commit: `1c02ba07f` (PR #3365, `BREAKING CHANGE` footer)
+- Docs impact:
+ - `02-ui-components/08-message/01-message_ui.md`
+- Example needed: no (the guide has one)
+
+
+
+### BC-081: custom `AttachmentPreviewList`, `VoiceRecordingPreviewSlot` and `LinkPreviewList` mount only while their preview item is visible
+
+- Status: confirmed
+- Area: message composer
+- User impact: silent behavior change. v14's `MessageComposerUI` mounted all three slots whenever `.str-chat__message-composer-previews` rendered. v15 renders `defaultMessageComposerPreviewSet` through `MessageComposerPreviews`: `attachments` shows while the composer holds an attachment other than a voice recording, `voiceRecording` while it holds a voice recording, `linkPreviews` while a link preview is loading or loaded. A custom slot that also rendered something else (the location, a poll, an empty or upload state) stops mounting when its item is hidden.
+- Old API:
+ - `release-v14:src/components/MessageComposer/MessageComposerUI.tsx:68-72,108`
+- Replacement: replace the item in a custom `messageComposerPreviewSet` with one whose `isVisible` covers what the component renders, installed through the `MessageComposerPreviews` slot.
+- Evidence:
+ - `master:src/components/MessageComposer/MessageComposerPreviews.defaults.tsx:92-121`
+ - `master:src/components/MessageComposer/MessageComposerUI.tsx:42,63`
+ - commit: `1c02ba07f` (PR #3365, `BREAKING CHANGE` footer)
+- Docs impact:
+ - `02-ui-components/09-message-composer/` (attachment previews)
+- Example needed: no (the guide has one)
+
+
+
+### BC-082: message action handlers reject when the request fails or when called without the message
+
+- Status: confirmed
+- Area: message hooks
+- User impact: runtime change for custom message UIs. `handlePin` / `usePinHandler` caught a failed request in v14.13 (rolling back silently) and now rolls back and rejects with the request's error. `handleDelete`, `handlePin`, `handleMute`, `handleFlag` and `handleMarkUnread` (and their `use*Handler` hooks) called without the message or client now throw instead of logging `console.warn` / returning. A custom button that calls one of them without `catch` gets an unhandled rejection; the default `MessageActions` show the error notification instead of the success one.
+- Old API:
+ - `v14.13.2:src/components/Message/hooks/usePinHandler.ts:32,48` (`catch` without rethrow)
+ - `release-v14:src/components/Message/hooks/useMuteHandler.ts:22-24`, `useFlagHandler.ts:18-19`, `useMarkUnreadHandler.ts:12-13`, `useDeleteHandler.ts:17-27`
+- Replacement: catch the rejection in custom UI (`handlePin(event).catch(...)`). `missingUseMuteHandlerParamsWarning` / `missingUseFlagHandlerParameterWarning` are kept and are now the error messages.
+- Evidence:
+ - `master:src/components/Message/hooks/usePinHandler.ts:81,109,137`, `useMuteHandler.ts:33,59,83`, `useDeleteHandler.ts:30-32`, `useFlagHandler.ts:18`, `useMarkUnreadHandler.ts:24`
+ - commits: `d88dbf02a` (PR #3360), `290d2dbaf` (PR #3362); v14 counterpart for pin only: `cf6c8f9ab` (PR #3359, unreleased on `release-v14` at the time of writing)
+- Docs impact:
+ - `02-ui-components/08-message/04-message_hooks.md`
+- Example needed: no
+
+
+
+### BC-083: CSS, the first message of a channel list loses its `4.5rem` top padding
+
+- Status: confirmed
+- Area: message list styling
+- User impact: visual change. v14 gave the first `.str-chat__li` of the channel's message list (`.str-chat__main-panel .str-chat__ul`) `padding-top: 4.5rem`, reset when a date separator came first, to leave room for the floating date separator. v15 has no such rule (the master branch moved it to `.str-chat__channel` and then removed it); the floating separator shows only while scrolling. App CSS that compensated for the padding now over-corrects.
+- Old API:
+ - `release-v14:src/components/MessageList/styling/MessageList.scss:61-71` (`.str-chat__main-panel .str-chat__ul .str-chat__li:first-of-type`)
+- Replacement: none; remove compensating rules.
+- Evidence:
+ - commit: `ec1413d20` (PR #3370), which deletes the rule; `master:src/components/MessageList/styling/MessageList.scss` has no `first-of-type` padding
+- Docs impact:
+ - none
+- Example needed: no
+
## Pending (open PRs)
- `GetStream/stream-chat-react#3306` `feat: adopt TS generator changes` (open, targets `master`). It changes the request shapes for sending, editing and deleting messages, reactions, reminders and poll-option suggestions to follow `GetStream/stream-chat-js#1896`. If it merges before the stable release, re-run the surface diff and update the request-handler entries (search this file for `sendMessageRequest`).
@@ -3444,7 +3715,7 @@ Non-gated items checked and ruled out: `Chat.isMessageAIGenerated`, `SummarizedM
| Context hooks: no `componentName` argument / Required contexts throw | BC-016, BC-007 (rows for `useChatContext`, `useChatViewContext`, `useChannelListItemContext`, `useSearchContext`, `useSearchSourceResultsContext`, `useDialogManager`, `useContextMenuContext`) |
| Related type changes (`ChatViewContext` has no default) | BC-012 |
| Per-component request-handler props removed | BC-023. Inaccurate: says removed "from both `` and ``"; v14 `Thread` never had them (`release-v14:src/components/Thread/Thread.tsx:26-45`); they were v15-only on `Thread` |
-| `` declares the channel active, and owns its message window | BC-027. Outdated: the `connection.recovered` -> `channel.reload()` part was removed by `3098a90a6` |
+| `` declares the channel active, and owns its message window | BC-027. Was outdated (the `connection.recovered` -> `channel.reload()` part was removed by `3098a90a6`); fixed in the guide |
| The core v15 shift / Removed `ChannelStateContext` / `ChannelActionContext` | ruled-out: contexts area |
#### `!` commits (and BREAKING CHANGE bodies) touching this area
@@ -3830,7 +4101,7 @@ Inaccuracies found in the existing guides while auditing. Fix them when the guid
- "`ChatContext.latestMessageDatesByChannels` -> removed" names `useIsCooldownActive`, which was not exported; `fix/v15-dead-surface-cleanup` exports it, so the guide is now right.
- "`ChannelListItem` `getLatestMessagePreview` prop -> removed" says the util was removed (it is still exported) and suggests overriding through `` / `` props that do not exist. Drop that mention; `ComponentProvider` (which it also names) works but replaces the parent context, so prefer `WithComponents`, which merges over it.
- "`Thread` renders children" says `Thread` registers the thread with the `ThreadManager`; it does not. `ThreadHeaderProps` also keeps `StartContent` / `EndContent`, not only `overrideTitle`.
- - "`` declares the channel active" still tells readers to reload on `connection.recovered`; the client reloads active channels itself since `3098a90a6`.
+ - Fixed: "`` declares the channel active" told readers to reload on `connection.recovered`; the client reloads active channels itself since `3098a90a6`.
- "`ChatContext.setActiveChannel` -> removed" says `ChannelListItemUI` selects through `open(...)`; it uses `useWorkspaceNavigation().openChannel`.
- "Peer-dependency gate before release" describes a `portal:` pin that no longer exists.
- The `messageActions` snippet filters `defaultMessageActionSet` by `type`, but the dropdown-toggle item has no `type`; guard with `'type' in item`. The same section says passing `messageActions={['delete']}` "in v15 changed nothing"; it means v14.
@@ -3839,3 +4110,5 @@ Inaccuracies found in the existing guides while auditing. Fix them when the guid
- `AGENTS.md` (as contributor guidance) and the `GetStream/stream-chat-react#3237` description point to `useMessagePaginator()`. It was not exported from `stream-chat-react`; `fix/v15-dead-surface-cleanup` exports it from `src/index.ts`, so both are now right.
- Group the guide by user task, as in the v14 guide: imports to change, props to remove, override keys to rename, behavior to re-wire (workspace navigation, channel initialization, request handlers).
- The silent breaks deserve top billing because nothing fails to compile: selection and thread opening outside `ChatView` (BC-045 / BC-013 above), `Channel` no longer watching the channel, failed sends no longer restoring the composer, and required context hooks throwing.
+- At the stable v15 release, re-pin the guide's links to this repository (`blob/master`, `raw.githubusercontent.com/.../master`) to the release tag, and the stream-chat-js links (`release-v10`) to the matching `stream-chat` tag.
+- `AGENTS.md` points to a guide heading "Per-component request-handler props removed"; the guide calls that section "14. Request handlers" (`#14-request-handlers`).