diff --git a/ai-docs/ai-migration-v14-v15.md b/ai-docs/ai-migration-v14-v15.md
index 874ce05ca..be0579b67 100644
--- a/ai-docs/ai-migration-v14-v15.md
+++ b/ai-docs/ai-migration-v14-v15.md
@@ -49,40 +49,41 @@ Then run the [silent-break checklist](#breaks-that-do-not-fail-to-compile) once
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)) |
-| 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)) |
+| 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)) |
---
@@ -222,14 +223,14 @@ export const useChatParts = (client: StreamChat, i18nInstance?: Streami18n) => {
### `ChatContext` fields
-| v14 `useChatContext()` field | v15 |
-| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `channel` (the active channel) | Inside the channel subtree: `useChannel()`. Elsewhere: `useWorkspaceNavigation().openChannels`, or `useSlotChannel({ slot })` from `stream-chat-react/slot-layout`. See [section 4](#4-selecting-channels-and-opening-threads-workspace-navigation). |
-| `setActiveChannel` | `useWorkspaceNavigation().openChannel(channel, { event })`. See [section 4](#4-selecting-channels-and-opening-threads-workspace-navigation). |
-| `channelsQueryState` | The channel list's own query state: `useChannelPaginatorState(paginator)` returns `{ channels, hasMoreHead, hasMoreTail, isLoading, lastQueryError }`; `hasMoreTail` says whether `paginator.toTail()` has another page. `ChannelList` reports `lastQueryError` through `client.notifications`. |
-| `latestMessageDatesByChannels` | Slow-mode cooldown: `useCooldownRemaining()` or `useStateStore(channel.cooldownTimer.state, selector)`. The channel's latest message: `channel.messagePaginator.aggregateState` (`latestMessage`). When the current user last posted: not available; track it from `message.new` yourself. |
-| `mutes` | Unchanged, but typed `UserMuteResponse[]` (was `Mute[]`). |
-| `getAppSettings` | Returns `ReturnType`. |
+| v14 `useChatContext()` field | v15 |
+| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `channel` (the active channel) | Inside the channel subtree: `useChannel()`. Elsewhere: `useWorkspaceNavigation().openChannels`, or `useSlotChannel({ slot })` from `stream-chat-react/slot-layout`, and only below the navigation provider: a component rendered next to `ChatView` instead of inside it always gets `openChannels: []`. See [section 4](#inside-chatview). |
+| `setActiveChannel` | `useWorkspaceNavigation().openChannel(channel, { event })`. See [section 4](#4-selecting-channels-and-opening-threads-workspace-navigation). |
+| `channelsQueryState` | The channel list's own query state: `useChannelPaginatorState(paginator)` returns `{ channels, hasMoreHead, hasMoreTail, isLoading, lastQueryError }`; `hasMoreTail` says whether `paginator.toTail()` has another page. `ChannelList` reports `lastQueryError` through `client.notifications`. |
+| `latestMessageDatesByChannels` | Slow-mode cooldown: `useCooldownRemaining()` or `useStateStore(channel.cooldownTimer.state, selector)`. The channel's latest message: `channel.messagePaginator.aggregateState` (`latestMessage`). When the current user last posted: not available; track it from `message.new` yourself. |
+| `mutes` | Unchanged, but typed `UserMuteResponse[]` (was `Mute[]`). |
+| `getAppSettings` | Returns `ReturnType`. |
`latestMessageDatesByChannels` never held the channel's latest message date: it was filtered to the current user's own messages, to compute slow-mode cooldown, a job `channel.cooldownTimer` now owns. `channelsQueryState` used to drive a loading and error state inside `Channel`; `Channel` renders none now (see [section 5](#channel-requires-channel-and-renders-no-placeholder-loading-or-error-state)).
@@ -354,7 +355,9 @@ import {
Slot,
} from 'stream-chat-react/slot-layout';
-// Module scope: both feed the ChatView layout controller and must be referentially stable.
+// `layouts` (and `resolveDuplicateEntity`, if you pass it) must be referentially stable: a new value
+// re-creates the layout controller and closes everything that was open. Keep `slotRenderers` stable
+// too: a new object re-renders every `useWorkspaceNavigation()` consumer. `views` may be built in render.
const layouts = [{ id: 'channels' as const, slots: ['left', 'right'] }];
const slotRenderers: ChatViewSlotRenderers = {
@@ -421,7 +424,9 @@ import {
ThreadSlot,
} from 'stream-chat-react/slot-layout';
-// Module scope: layouts, renderers and views must be referentially stable.
+// Module scope here because nothing depends on props. `layouts` must be stable (a new array closes
+// everything that was open); keep `slotRenderers` stable too (`useMemo` when it needs props).
+// `views` can be built in render.
const layouts = [
{ id: 'channels' as const, slots: ['channel', 'channel-thread'] },
{ id: 'threads' as const, slots: ['thread'] },
@@ -540,6 +545,22 @@ The channel-detail "send direct message" member action and the pinned-message en
Nothing to wire. `ChatView` provides the navigation and binds what is opened into a layout slot. To customize it (for example, open beside the current channel on ⌘/ctrl-click), pass `deriveWorkspaceNavigation`, which receives the SDK-derived navigation and returns the members to override. It must be referentially stable.
+The same holds for your own components. Anything that reads `useWorkspaceNavigation()` (an "in a call" panel, a header badge for the open channel) has to render inside `ChatView`. Pass it as a `ChatView` child in a `views` layout: children render inside the provider, before the active view, and stay mounted when the view switches. The built-in `layout="nav-rail-entity-list-workspace"` does not render `children`. `openChannels` lists the active view's channels, so it is empty while the threads view is shown.
+
+```tsx
+const CallPanel = () => {
+ const { openChannels } = useWorkspaceNavigation();
+ const channel = openChannels[0];
+ return channel ? : null;
+};
+
+
+
+
+
+;
+```
+
### Without `ChatView`
Hold what is open in your own state and provide `WorkspaceNavigationProvider` with an object built from `defaultWorkspaceNavigation`. Every member you do not override keeps its inert default (`false`, a no-op, an empty list). Override:
@@ -964,7 +985,7 @@ export const Sidebar = () => ;
```
- **Create and register the paginator above `ChatView`**, not in the component that renders the list. `ChatView` unmounts the inactive view ([section 3](#where-an-opened-entity-lands)), so a paginator created inside `views.channels` is re-created and re-queried every time the user comes back from the threads view. A paginator that outlives the view keeps its loaded pages, and a remounted `ChannelList` queries only a paginator that was never loaded.
-- `ChannelNavigation` finds every registered paginator through `client.channelManager`, so it takes no prop. Pass the paginator down only to code that needs it, such as `useOpenFirstChannel` [below](#v14-list-defaults-that-changed). If `views` then depends on it, build `views` with `useMemo`, since it must stay referentially stable.
+- `ChannelNavigation` finds every registered paginator through `client.channelManager`, so it takes no prop. Pass the paginator down only to code that needs it, such as `useOpenFirstChannel` [below](#v14-list-defaults-that-changed). `views` may take it as a prop directly; it does not need to be referentially stable.
A single list can still be rendered on its own with ``; it has no header or search. Use `` for all registered lists without the header.
@@ -1138,13 +1159,13 @@ export const ChannelCount = () => {
### `ChannelListItem` props
-| v14 | v15 |
-| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `lastMessage` (on `ChannelListItemUIProps` and `ChannelListItemTimestamp`) | `previewedMessage`. `ChannelListItem` also takes `previewedMessage` to preview a specific message (a search result previewing the match); it defaults to `channel.messagePaginator.aggregateState.lastMessage`. |
-| `latestMessagePreview` (`ChannelListItemUIProps`), `getLatestMessagePreview` (`ChannelListItem` and `ChannelList` props) | Render ``, or override the `SummarizedMessagePreview` slot with `WithComponents` (`SummarizedMessagePreviewProps`: `{ latestMessage, messageDeliveryStatus, participantCount }`). |
-| `setActiveChannel`, `watchers` | `useWorkspaceNavigation().openChannel(channel, { event })`, or the `onSelect` prop ([section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) |
-| `channelUpdateCount`, `key` | None; the item subscribes to the channel's stores |
-| `active` | Unchanged prop; it now defaults to `useWorkspaceNavigation().isChannelActive(cid)`. `ChannelListItem` passes the resolved value to `ChannelListItemUI` as `active`, so a custom row UI reads `props.active` to highlight itself |
+| v14 | v15 |
+| ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `lastMessage` (on `ChannelListItemUIProps` and `ChannelListItemTimestamp`) | `previewedMessage`. `ChannelListItem` also takes `previewedMessage` to preview a specific message (a search result previewing the match); it defaults to `channel.messagePaginator.aggregateState.lastMessage`. |
+| `latestMessagePreview` (`ChannelListItemUIProps`), `getLatestMessagePreview` (`ChannelListItem` and `ChannelList` props) | Render ``, or override the `SummarizedMessagePreview` slot with `WithComponents` (`SummarizedMessagePreviewProps`: `{ latestMessage, messageDeliveryStatus, participantCount }`). |
+| `setActiveChannel`, `watchers` | `useWorkspaceNavigation().openChannel(channel, { event })`, or the `onSelect` prop ([section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) |
+| `channelUpdateCount`, `key` | None; the item subscribes to the channel's stores |
+| `active` | Unchanged prop; it now defaults to `useWorkspaceNavigation().isChannelActive(cid)`. `ChannelListItem` passes the resolved value to `ChannelListItemUI` as `active`. The SDK's CSS highlights `.str-chat__channel-list-item[aria-selected=true]` only (v14 also matched `[aria-pressed=true]`), so a custom row that keeps the SDK class sets `role="option"` and `aria-selected={active}` |
The `getLatestMessagePreview` and `getLatestMessagePreviewText` utilities are still exported; only their `t` / `userLanguage` parameter types changed. Prefer `WithComponents` for the slot override: it merges over the parent context, while `ComponentProvider` replaces it. Neither `Chat` nor `Channel` takes component props.
@@ -1173,6 +1194,7 @@ import {
} from 'stream-chat-react';
export const CustomItem = ({
+ active,
channel,
displayTitle,
messageDeliveryStatus,
@@ -1180,7 +1202,12 @@ export const CustomItem = ({
}: ChannelListItemUIProps) => {
const { openChannel } = useWorkspaceNavigation();
return (
-