Skip to content
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Each is a `StateStore`. Components read them with `useStateStore(store, selector
└─ <MessageComposer>
```

`Channel` takes a single `channel` prop (`src/components/Channel/Channel.tsx`). It activates the channel on mount, deactivates it on unmount, seeds the paginator's unread snapshot, and re-queries on `user.deleted` — nothing else. It does not query the channel for you: initialize it first (`channel.ensureWatched()` sends one watch for overlapping calls, and none for a channel already watched). There is no `Window` component in v15; compose layout yourself or through `ChatView` slots.
`Channel` takes a single `channel` prop (`src/components/Channel/Channel.tsx`). It activates the channel on mount, deactivates it on unmount, seeds the paginator's unread snapshot, and re-queries on `user.deleted` — nothing else. It does not query the channel for you: initialize it first (`channel.watch()` sends one watch for overlapping calls, and none for a channel already watched and loaded). There is no `Window` component in v15; compose layout yourself or through `ChatView` slots.

Read the bound channel with `useChannel()` (`src/context/useChannel.ts`, thread's channel first, then the `Channel` subtree's) and the active message list with `useMessagePaginator()` (`src/hooks/useMessagePaginator.ts`, thread first, then channel). `useMessagePaginator()` is also exported from the package root, so integrator-facing docs and examples can use it too.

Expand Down Expand Up @@ -282,7 +282,7 @@ const {
// Manual setup when you need control over the API responses
const client = await getTestClientWithUser({ id: 'test-user' });
useMockedApis(client, [getOrCreateChannelApi(mockedChannelData)]);
const channel = client.channelManager.ensure({ id: channelId, type: 'messaging' });
const channel = client.channel('messaging', channelId);
await channel.watch();
```

Expand Down
36 changes: 18 additions & 18 deletions ai-docs/ai-migration-v14-v15.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions ai-docs/instance-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const App = () => (
```

**Not in an effect.** Some configuration is read once when an instance is constructed, and channels
are constructed by `client.channelManager.ensure()` / `client.queryChannels()` — which an app typically calls before
are constructed by `client.channel()` / `client.queryChannels()` — which an app typically calls before
or during the same commit that mounts `<Chat>`. Registering from `useEffect` runs after that, so those
values would arrive too late for instances that already exist.

Expand Down Expand Up @@ -120,17 +120,17 @@ does differ, so the per-parent slices override the shared one field by field.

### `pageSize` is not the initial query's `messages.limit`

`channel.ensureWatched({ messages: { limit: 30 } })` sizes the **initial** channel query, which the code
`channel.watch({ messages: { limit: 30 } })` sizes the **initial** channel query, which the code
that opens the channel now sends (`<Channel>` no longer queries, and its `channelQueryOptions` prop is
removed). The paginator's `pageSize` sizes **every subsequent page** — what a scroll-up fetches. They
are different numbers and you usually want both:

```tsx
chatClient.config.set({ channel: { messagePaginator: { pageSize: 50 } } });

const channel = await chatClient.channelManager
.ensure({ id: 'general', type: 'messaging' })
.ensureWatched({ messages: { limit: 30 } });
const channel = await chatClient
.channel('messaging', 'general')
.watch({ messages: { limit: 30 } });

<Channel channel={channel}>…</Channel>;
```
Expand Down
29 changes: 12 additions & 17 deletions ai-docs/v15-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ A threads view is declared the same way (a `{ id: 'threads', slots: [...] }` lay
- Without `ChatView`: hold the open channel in your own state and provide `WorkspaceNavigationProvider` (main entry) with an object built from `defaultWorkspaceNavigation`, overriding at least `openChannel`, `isChannelActive` and `openChannels` (and `openThread` / `openThreads` / `isThreadActive` if you render threads). Alternatively pass `onSelect` to `ChannelListItem` / search result items.
- Reading "the active channel": inside the channel subtree use `useChannel()`; elsewhere `useWorkspaceNavigation().openChannels` (main entry) or `useSlotChannel({ slot })` (`slot-layout`).
- Opening a channel programmatically: `useWorkspaceNavigation().openChannel(channel)`; to also list it without a re-query, `client.channelManager.ingestChannel(channel)` (stream-chat).
- The `watchers` argument: query the channel yourself (`channel.ensureWatched({ watchers: { limit } })` or `channel.query(...)`, stream-chat) before opening it.
- The `watchers` argument: query the channel yourself (`channel.watch({ watchers: { limit } })` or `channel.query(...)`, stream-chat) before opening it.
- Evidence:
- `release-v14:src/context/ChatContext.tsx:45-49` (`setActiveChannel`), `:54` (`channel?: Channel`); `release-v14:src/components/Chat/hooks/useChat.ts:116-131,137-144`
- `master:src/context/ChatContext.tsx:27-46` (no `channel` / `setActiveChannel`); `master:src/components/Chat/hooks/useChat.ts:68-71`
Expand Down Expand Up @@ -1160,11 +1160,11 @@ import {
- `ChannelProps.initializeOnMount?: boolean` (default `true`: `Channel` called `channel.watch()`)
- `ChannelProps.channelQueryOptions?: ChannelQueryOptions`
- `getChannel({ client, channel?, id?, members?, options?, type? })`
- Replacement: initialize the channel before handing it to `Channel`. App code that already calls `await channel.watch(options)` on a channel created by id needs no change (`client.channel()` and `channel.watch()` keep their roles; `client.channel()` is shorthand for `client.channelManager.ensure({ type, id, data })`). A members-only channel (`client.channel(type, { members })`) must be rendered from the instance `ensureWatched()` resolves with: the created instance can be superseded by a stored instance for the server-assigned cid (`supersededBy`, `watchStatus: NotWatching`, no events). Code that relied on `Channel` to watch adds the call: `channel.watch(options)`, or `await channel.ensureWatched(options)` (stream-chat; sends nothing for an already-watched channel and joins an in-flight watch with the same options). Channels delivered by `ChannelList` arrive watched. v10 also changed the input shape (`members` as `{ user_id }` objects, custom fields under `custom`); that part is a type change only.
- Replacement: initialize the channel before handing it to `Channel`. App code that already calls `await channel.watch(options)` on a channel created by id needs no change (`client.channel()` and `channel.watch()` keep their roles; `client.channel()` is the public way to get a channel; it calls the internal `channelManager.ensure()`). A members-only channel (`client.channel(type, { members })`) must be rendered from the instance `watch()` resolves with: the created instance can be superseded by a stored instance for the server-assigned cid (`supersededBy`, `watchStatus: NotWatching`, no events). Code that relied on `Channel` to watch adds the call: `await channel.watch(options)` (stream-chat; sends nothing for a channel already watched and loaded and joins an in-flight watch with the same options). Channels delivered by `ChannelList` arrive watched. v10 also changed the input shape (`members` as `{ user_id }` objects, custom fields under `custom`); that part is a type change only.
- Evidence:
- `release-v14:src/components/Channel/Channel.tsx:107,141`; `release-v14:src/utils/getChannel.ts:34`
- `master:src/components/Channel/Channel.tsx:17-23` (prop doc: "Initialize it before passing it in; `Channel` does not query"); `master:src/utils/getChannel.ts` removed
- `node_modules/stream-chat/dist/types/channel.d.ts:747` (`ensureWatched(options?: ChannelGetOrCreateRequest): Promise<Channel>`); `node_modules/stream-chat/dist/types/ChannelManager.d.ts:157` (`ensure`)
- `node_modules/stream-chat/dist/types/channel.d.ts` (`watch(options?: ChannelGetOrCreateRequest, requestOptions?: StreamRequestOptions): Promise<Channel>`); `node_modules/stream-chat/dist/types/ChannelManager.d.ts:157` (`ensure`)
- commits: `088c795c1` (PR #3286); `a41eed56f feat!: replace StreamChat.activeChannels with entity item index for loaded channels` (PR #3310, removes `getChannel`)
- corpus: `docs/02-ui-components__05-channel-list__02-channel_list_context__2.tsx:11` TS2724 (`getChannel`)
- Docs impact:
Expand All @@ -1184,17 +1184,14 @@ const channel = client.channel('messaging', { members: [userId, otherUserId] });
After (v15):

```tsx
const channel = await client.channelManager
.ensure({
data: { members: [{ user_id: userId }, { user_id: otherUserId }] },
type: 'messaging',
})
.ensureWatched({ messages: { limit: 30 } });
const channel = await client
.channel('messaging', { members: [{ user_id: userId }, { user_id: otherUserId }] })
.watch({ messages: { limit: 30 } });

<Channel channel={channel}>{/* ... */}</Channel>;
```

`ensureWatched` takes stream-chat's `ChannelGetOrCreateRequest` (`messages`, `members`, `watchers`, `presence`, `state`, `watch`, `data`, ...), and `data.members` is `ChannelMemberRequest[]` (`{ user_id }` objects) in stream-chat v10 (LLC change; the React SDK builds DMs the same way, `SearchResultItem.tsx:222`).
`watch` takes stream-chat's `ChannelGetOrCreateRequest` (`messages`, `members`, `watchers`, `presence`, `state`, `watch`, `data`, ...), and `data.members` is `ChannelMemberRequest[]` (`{ user_id }` objects) in stream-chat v10 (LLC change; the React SDK builds DMs the same way, `SearchResultItem.tsx:222`).

---

Expand All @@ -1209,9 +1206,7 @@ const channel = await getChannel({ client, id: 'general', type: 'messaging' });
After (v15):

```ts
const channel = await client.channelManager
.ensure({ id: 'general', type: 'messaging' })
.ensureWatched();
const channel = await client.channel('messaging', 'general').watch();
```

### BC-021: `Channel` no longer writes `document.title`; `activeUnreadHandler` was removed
Expand Down Expand Up @@ -1933,15 +1928,15 @@ const CustomThreadHeader = () => {
- Status: confirmed
- Area: search
- User impact: silent.
- stream-chat v10 queries channel results (and the channels message search fetches) with `watch: false`. The SDK result items watch on open (`ensureWatched()`, or `jumpToMessage(id, { watchChannel: true })` for messages). A custom search result component that only opened the channel now opens an unwatched channel (no events, possibly empty).
- stream-chat v10 queries channel results (and the channels message search fetches) with `watch: false`. The SDK result items watch on open (`watch()`, or `jumpToMessage(id, { watchChannel: true })` for messages). A custom search result component that only opened the channel now opens an unwatched channel (no events, possibly empty).
- default result items call `openChannel` (workspace navigation: no-op outside `ChatView`, BC-013) and `client.channelManager.ingestChannel(channel)` instead of `setActiveChannel` + `ChannelListContext.setChannels`.
- selecting a message result jumps with `channel.messagePaginator.jumpToMessage(...)`; `Channel` no longer performs the jump (BC-027). The "jumped here" marker reads `messagePaginator.messageFocusSignal`, not `SearchController._internalState.focusedMessage`.
- Old API:
- default `ChannelSearchResultItem` / `MessageSearchResultItem` / `UserSearchResultItem` behavior
- Replacement: in a custom result component, call `channel.ensureWatched()` (or `jumpToMessage(id, { watchChannel: true })`) when opening; open via `useWorkspaceNavigation().openChannel(channel, { event })`. The default items gained an optional `onSelect` prop that replaces their selection handler.
- Replacement: in a custom result component, call `channel.watch()` (or `jumpToMessage(id, { watchChannel: true })`) when opening; open via `useWorkspaceNavigation().openChannel(channel, { event })`. The default items gained an optional `onSelect` prop that replaces their selection handler.
- Evidence:
- `release-v14:src/components/Search/SearchResults/SearchResultItem.tsx:23-30,45-74,99-114`
- `master:src/components/Search/SearchResults/SearchResultItem.tsx:66-105` (`ChannelSearchResultItem`: `openChannel`, `ensureWatched`, `ingestChannel`), `:117-180` (`MessageSearchResultItem`: `jumpToMessage(item.id, { watchChannel: true })`), `:199-243`
- `master:src/components/Search/SearchResults/SearchResultItem.tsx:66-105` (`ChannelSearchResultItem`: `openChannel`, `watch`, `ingestChannel`), `:117-180` (`MessageSearchResultItem`: `jumpToMessage(item.id, { watchChannel: true })`), `:199-243`
- commit: `a41eed56f` (PR #3310; "BREAKING CHANGE: channel search results ... are no longer watched; a custom search result component watches the channel when it is opened"); `088c795c1` (PR #3286)
- Docs impact:
- `02-ui-components/05-channel-list/05-channel_search.md`, `02-ui-components/05-channel-list/08-advanced-search.md`, `03-ui-cookbook/02-channel-list/02-channel_search.md`
Expand Down Expand Up @@ -2590,7 +2585,7 @@ export const Stop = ({ channel }: { channel: Channel }) => {
- Old API:
- `<ChannelActionProvider><ChannelStateProvider><Message … /></…></…>`, or `<Message>` with no channel context
- implicit `highlighted` from `useChannelStateContext().highlightedMessageId`
- Replacement: wrap it in `<ChannelInstanceProvider value={{ channel }}>` (any real `stream-chat` `Channel`, e.g. `client.channelManager.ensure({ id, type })`), or render it inside `<Channel channel={…}>` / `<Thread thread={…}>`. Pass `highlighted` explicitly when needed (`MessageProps.highlighted?: boolean`).
- Replacement: wrap it in `<ChannelInstanceProvider value={{ channel }}>` (any real `stream-chat` `Channel`, e.g. `client.channel(type, id)`), or render it inside `<Channel channel={…}>` / `<Thread thread={…}>`. Pass `highlighted` explicitly when needed (`MessageProps.highlighted?: boolean`).
- Evidence:
- `master:src/context/useChannel.ts` throws when neither a thread nor `ChannelInstanceContext` supplies a channel
- `master:src/components/Message/hooks/useOpenThreadHandler.ts:16` and `master:src/components/Message/hooks/useRetryHandler.ts:11` call `useChannel()` during every `Message` render
Expand Down
13 changes: 4 additions & 9 deletions examples/ai-chatbot/src/components/AIChatApp/AIChatApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,13 @@ const ChatContent = ({
}, [channel?.id, searchParams, router]);

// Load initial channel from URL on mount. `Channel` no longer queries the channel it is given,
// so it is watched here first. `channelManager.ensure()` returns the stored instance for the cid,
// and `ensureWatched()` de-duplicates concurrent watches of it.
// so it is watched here first. `client.channel()` returns the stored instance for the cid,
// and `watch()` sends nothing for it once watched and loaded, and joins a watch in flight.
useEffect(() => {
if (initialChannelId && client && !channel) {
const loadChannel = async () => {
const targetChannel = client.channelManager.ensure({
id: initialChannelId,
type: 'messaging',
});
if (!targetChannel.initialized) {
await targetChannel.ensureWatched();
}
const targetChannel = client.channel('messaging', initialChannelId);
await targetChannel.watch();
setActiveChannel(targetChannel);
};
loadChannel().catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,9 @@ export const MessageInputBar = () => {
composer.textComposer.setText(message as string);

// A new conversation exists only locally until now; `Channel` no longer creates it.
// Both setup steps run before `send()`, so a failure here leaves the draft untouched.
if (!channel.initialized) {
await channel.ensureWatched();
}
// `watch()` sends nothing for a conversation already watched and loaded. Both setup steps
// run before `send()`, so a failure here leaves the draft untouched.
await channel.watch();

const [platform, model] = (platformModel as string).split('|');

Expand Down
28 changes: 11 additions & 17 deletions examples/tutorial/src/3-core-component-setup/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,23 @@ const App = () => {
if (!client) return;

const initChannel = async () => {
const channel = client.channelManager.ensure({
data: {
members: [userId],
// custom channel fields live under `custom` since v10
custom: {
image: 'https://getstream.io/random_png/?name=react',
name: 'Talk about React',
},
const channel = client.channel('messaging', 'custom_channel_id', {
members: [userId],
// custom channel fields live under `custom` since v10
custom: {
image: 'https://getstream.io/random_png/?name=react',
name: 'Talk about React',
},
id: 'custom_channel_id',
type: 'messaging',
});

// `Channel` binds a channel to its subtree; it does not query one. Whoever supplies the
// channel initializes it.
//
// `client.channelManager.ensure()` returns the stored instance for this cid, so a re-run of this effect
// can hand back a channel that is already loaded -- query only when it is not. When a query
// is needed, `channel.ensureWatched()` de-duplicates concurrent calls for the same channel, so two
// overlapping runs still produce a single request.
if (!channel.initialized) {
await channel.ensureWatched();
}
// `client.channel()` returns the stored instance for this cid, so a re-run of
// this effect can hand back a channel that is already loaded. `channel.watch()` sends nothing
// for a channel already watched and loaded, and joins a watch in flight, so two overlapping
// runs still produce a single request.
await channel.watch();

setChannel(channel);
};
Expand Down
24 changes: 9 additions & 15 deletions examples/tutorial/src/6-custom-ui-components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,25 +214,19 @@ const App = () => {
if (!client) return;

const initChannel = async () => {
const channel = client.channelManager.ensure({
data: {
members: [userId],
// custom channel fields live under `custom` since v10
custom: {
image: 'https://getstream.io/random_png/?name=react-v14',
name: 'Talk about React',
},
const channel = client.channel('messaging', 'react-tutorial', {
members: [userId],
// custom channel fields live under `custom` since v10
custom: {
image: 'https://getstream.io/random_png/?name=react-v14',
name: 'Talk about React',
},
id: 'react-tutorial',
type: 'messaging',
});

// `Channel` binds a channel to its subtree; it does not query one, so initializing is the
// caller's job. The cached instance may already be loaded, so query only when it is not --
// and when a query is needed, `channel.ensureWatched()` de-duplicates calls that overlap in time.
if (!channel.initialized) {
await channel.ensureWatched();
}
// caller's job. `channel.watch()` sends nothing for a channel already watched and loaded, and
// joins a watch in flight, so a re-run of this effect still sends one request at most.
await channel.watch();
setIsReady(true);
};

Expand Down
Loading
Loading