Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion examples/ai-chatbot/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ examples/ai-chatbot/

### 18. **useSendAIMessage.ts**

- The send path shared by `MessageInputBar` and voice mode: creates a new conversation with `channel.ensureWatched()`, starts the AI agent when it is not watching yet, sends through `composer.send()` (with attachments), records the message against the rate limit and summarizes a new conversation in the background (the summary never delays or fails the send)
- The send path shared by `MessageInputBar` and voice mode: creates a new conversation with `channel.watch()`, starts the AI agent when it is not watching yet, sends through `composer.send()` (with attachments), records the message against the rate limit and summarizes a new conversation in the background (the summary never delays or fails the send)
- Also holds `availableModels`, `defaultPlatformModel` and `isWatchedByAI`

### 19. **VoiceMode**
Expand Down
2 changes: 1 addition & 1 deletion examples/ai-chatbot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"next": "^16.2.4",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "10.0.0-rc.24",
"stream-chat": "10.0.0-rc.25",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
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
7 changes: 3 additions & 4 deletions examples/ai-chatbot/src/components/useSendAIMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ export const useSendAIMessage = () => {
composer.textComposer.setText(text);

// 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.split('|');

Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"emoji-mart": "^5.6.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "10.0.0-rc.24",
"stream-chat": "10.0.0-rc.25",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
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