From 39be35056d8459699948ed4a5a1547cd91189119 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 29 Oct 2026 12:17:42 +0100 Subject: [PATCH 1/2] docs(ai-migration): cover gaps found migrating the stream-video sample apps - list width variables default to 100% and crowd out the slot beside them - the active channel row is styled through aria-selected only - app components reading the open channel must render inside ChatView - only layouts has to be referentially stable - v14 i18n keys with no key-map entry (live, Create, Submit, ...) --- ai-docs/ai-migration-v14-v15.md | 145 ++++++++++++++++++++------------ ai-docs/i18n-v15-migration.md | 17 +++- 2 files changed, 107 insertions(+), 55 deletions(-) diff --git a/ai-docs/ai-migration-v14-v15.md b/ai-docs/ai-migration-v14-v15.md index 874ce05ca..6c96ba1d5 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)) | +| Inside `ChatView`, 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,8 @@ import { Slot, } from 'stream-chat-react/slot-layout'; -// Module scope: both feed the ChatView layout controller and must be referentially stable. +// `layouts` must be referentially stable: a new array re-creates the layout controller and closes +// everything that was open. `slotRenderers` and `views` may be built in render. const layouts = [{ id: 'channels' as const, slots: ['left', 'right'] }]; const slotRenderers: ChatViewSlotRenderers = { @@ -421,7 +423,8 @@ 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. Only `layouts` must be stable; +// `views` and `slotRenderers` can also be built in render when they need props. const layouts = [ { id: 'channels' as const, slots: ['channel', 'channel-thread'] }, { id: 'threads' as const, slots: ['thread'] }, @@ -540,6 +543,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: children render inside the provider, before the active view, and stay mounted when the view switches. `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 +983,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; only `layouts` must stay 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 +1157,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 +1192,7 @@ import { } from 'stream-chat-react'; export const CustomItem = ({ + active, channel, displayTitle, messageDeliveryStatus, @@ -1180,7 +1200,12 @@ export const CustomItem = ({ }: ChannelListItemUIProps) => { const { openChannel } = useWorkspaceNavigation(); return ( -