Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3619235
feat!: adopt the thread paginator and ThreadManager API from stream-c…
MartinCupela Oct 1, 2026
5d495c9
feat!: read channels from the channel store instead of client.activeC…
MartinCupela Oct 1, 2026
e4b9e18
feat!: get channels through client.channelManager.ensure()
MartinCupela Oct 2, 2026
64d9c80
chore(examples): get channels through channelManager.ensure() in docs…
MartinCupela Oct 2, 2026
9202bd5
feat: watch opened channel search results and tie search subscription…
MartinCupela Oct 5, 2026
757897c
chore(examples): add a channel store debug dialog to the vite app
MartinCupela Oct 5, 2026
736e833
chore(examples): add a user profile dialog to the vite app
MartinCupela Oct 5, 2026
75b0a81
fix(ChannelListItem): re-derive preview info from channel state inste…
MartinCupela Oct 5, 2026
4775573
docs: client.channel() is a shorthand for channelManager.ensure() in …
MartinCupela Oct 6, 2026
803b317
fix: pass path parameters separately to stream-chat v10 API calls
MartinCupela Oct 7, 2026
09bf3fb
chore(examples): add a new conversation dialog to the vite app
MartinCupela Oct 7, 2026
35894d4
fix(Modal): keep scrolling over the overlay from reaching the page
MartinCupela Oct 7, 2026
d6f6a88
feat(ChannelDetail): disable the details button until the channel is …
MartinCupela Oct 7, 2026
e5330e0
feat(SlotLayout): move a slot to the instance that superseded its cha…
MartinCupela Oct 7, 2026
87ac163
feat(SlotLayout): add DisposedChannelRelease to empty a slot whose ch…
MartinCupela Oct 7, 2026
07bb6cf
feat(SlotLayout): show an empty-state placeholder when no slot has co…
MartinCupela Oct 7, 2026
64d684a
chore(examples): improve the new conversation dialog
MartinCupela Oct 7, 2026
b63b590
chore(examples): add CDN upload destinations, with a mock CDN
MartinCupela Oct 7, 2026
a84983c
chore(examples): empty disposed channel slots and show the empty plac…
MartinCupela Oct 7, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,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.channel('messaging', channelId);
const channel = client.channelManager.ensure({ id: channelId, type: 'messaging' });
await channel.watch();
```

Expand Down
17 changes: 11 additions & 6 deletions ai-docs/ai-migration-v14-v15.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,16 @@ dictionary: [`i18n-v15-migration.md`](./i18n-v15-migration.md).
```tsx
import { getChannel } from 'stream-chat-react';

const channel = client.channel('messaging', id, { members, custom });
const channel = client.channelManager.ensure({
data: { custom, members },
id,
type: 'messaging',
});
if (!channel.initialized) await getChannel({ channel, client });
setChannel(channel);
```

Two separate guards, both worth keeping. `client.channel()` returns the cached instance, which may already be loaded, so `initialized` skips a query that is not needed. And prefer `getChannel` over a bare `channel.watch()` when one _is_ needed: it de-duplicates concurrent calls for the same channel (keyed on the sorted member list while a channel has no id yet), so an effect that runs twice, or two components opening the same channel, still produce one query. That de-duplication used to live inside `Channel`.
Two separate guards, both worth keeping. `client.channelManager.ensure()` (which `client.channel()` calls in stream-chat v10) returns the stored instance, which may already be loaded, so `initialized` skips a query that is not needed. And prefer `getChannel` over a bare `channel.watch()` when one _is_ needed: it de-duplicates concurrent calls for the same channel (keyed on the sorted member list while a channel has no id yet), so an effect that runs twice, or two components opening the same channel, still produce one query. That de-duplication used to live inside `Channel`.

- **A direct message identified by members** β†’ `getChannel({ client, type: 'messaging', members })` builds, watches and returns the instance.
- **`channelQueryOptions`** β†’ pass them to the watch you now own: `getChannel({ channel, client, options })`.
Expand Down Expand Up @@ -909,8 +913,9 @@ reads `muteStatus` instead of subscribing to `notification.channel_mutes_updated

### `<Channel>` declares the channel active, and owns its message window

`<Channel>` now calls `channel.activate()` on mount and `channel.deactivate()` on unmount (refcounted,
so several consumers can hold one instance), and calls `channel.reload()` on `connection.recovered`.
`<Channel>` now calls `channel.activate()` on mount and the release function it returns on unmount
(refcounted, so several consumers can hold one instance), and calls `channel.reload()` on
`connection.recovered`.

These two go together and **a custom channel surface must do both**. While a channel is active, the
client deliberately skips re-seeding its message list on channel-list hydration and on reconnect β€” its
Expand All @@ -919,8 +924,8 @@ Nothing in the client calls `reload()` for you:

```ts
useEffect(() => {
channel.activate();
return () => channel.deactivate();
const release = channel.activate();
return release;
}, [channel]);

client.on('connection.recovered', () => {
Expand Down
4 changes: 2 additions & 2 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.channel()` / `client.queryChannels()` β€” which an app typically calls before
are constructed by `client.channelManager.ensure()` / `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 @@ -198,7 +198,7 @@ this cannot. If behaviour has to differ between channels, branch inside a single

```ts
sendMessageRequest: async ({ localMessage, message, options }) => {
const channel = chatClient.channel(...);
const channel = chatClient.channelManager.ensure({ ... });
return isSupportChannel(localMessage.cid)
? { message: await sendViaProxy(message, options) }
: { message: await sendNormally(message, options) };
Expand Down
18 changes: 11 additions & 7 deletions examples/tutorial/src/3-core-component-setup/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,23 @@ const App = () => {
if (!client) return;

const initChannel = async () => {
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',
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',
},
},
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.channel()` returns the cached instance for this cid, so a re-run of this effect
// `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, `getChannel` de-duplicates concurrent calls for the same channel, so two
// overlapping runs still produce a single request.
Expand Down
16 changes: 10 additions & 6 deletions examples/tutorial/src/6-custom-ui-components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,17 @@ const App = () => {
if (!client) return;

const initChannel = async () => {
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',
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',
},
},
id: 'react-tutorial',
type: 'messaging',
});

// `Channel` binds a channel to its subtree; it does not query one, so initializing is the
Expand Down
16 changes: 10 additions & 6 deletions examples/tutorial/src/7-emoji-picker/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,17 @@ const App = () => {
if (!client) return;

const initChannel = async () => {
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',
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',
},
},
id: 'react-tutorial',
type: 'messaging',
});

// `Channel` binds a channel to its subtree; it does not query one, so initializing is the
Expand Down
14 changes: 9 additions & 5 deletions examples/tutorial/src/optional-custom-attachment-type/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,16 @@ const App = () => {
if (!client) return;

const initChannel = async () => {
const channel = client.channel('messaging', 'react-tutorial-products', {
members: [userId],
custom: {
image: 'https://getstream.io/random_png/?name=products',
name: 'Product recommendations',
const channel = client.channelManager.ensure({
data: {
members: [userId],
custom: {
image: 'https://getstream.io/random_png/?name=products',
name: 'Product recommendations',
},
},
id: 'react-tutorial-products',
type: 'messaging',
});

// `Channel` binds a channel to its subtree; it does not query one, so initializing is the
Expand Down
14 changes: 9 additions & 5 deletions examples/tutorial/src/optional-livestream/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,16 @@ const App = () => {
if (!chatClient) return;

const initChannel = async () => {
const spaceChannel = chatClient.channel('livestream', 'spacex', {
// custom channel fields live under `custom` since v10
custom: {
image: 'https://goo.gl/Zefkbx',
name: 'SpaceX launch discussion',
const spaceChannel = chatClient.channelManager.ensure({
data: {
// custom channel fields live under `custom` since v10
custom: {
image: 'https://goo.gl/Zefkbx',
name: 'SpaceX launch discussion',
},
},
id: 'spacex',
type: 'livestream',
});

// `Channel` binds a channel to its subtree; it does not query one, so initializing is the
Expand Down
3 changes: 3 additions & 0 deletions examples/vite/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ dist-ssr
*.sln
*.sw?
.vercel

# files stored by the mock CDN (mockCdnPlugin.ts)
.mock-cdn
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ async function initSession(
if (createChannel) {
await page.evaluate(`(async () => {
var client = window.client;
var ch = client.channel('messaging', ${JSON.stringify(CHANNEL_ID)}, {
var ch = client.channelManager.ensure({ type: 'messaging', id: ${JSON.stringify(CHANNEL_ID)}, data: {
name: 'Attachment Actions Demo',
members: [${JSON.stringify(USER_A)}, ${JSON.stringify(USER_B)}],
});
} });
await ch.create();
})()`);
console.log(` πŸ“¦ Channel "${CHANNEL_ID}" created`);
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/docs-playwright/screenshot-misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ async function createChannel(

await page.evaluate(`(async () => {
var client = window.client;
var ch = client.channel('messaging', ${JSON.stringify(channelId)}, {
var ch = client.channelManager.ensure({ type: 'messaging', id: ${JSON.stringify(channelId)}, data: {
name: 'Demo',
members: [${JSON.stringify(USER_A)}, ${JSON.stringify(USER_B)}],
});
} });
await ch.create();
})()`);

Expand Down
4 changes: 2 additions & 2 deletions examples/vite/docs-playwright/screenshot-reactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ async function initSession(
if (createChannel) {
await page.evaluate(`(async () => {
var client = window.client;
var ch = client.channel('messaging', ${JSON.stringify(CHANNEL_ID)}, {
var ch = client.channelManager.ensure({ type: 'messaging', id: ${JSON.stringify(CHANNEL_ID)}, data: {
name: 'Reactions Demo',
members: [${JSON.stringify(USER_A)}, ${JSON.stringify(USER_B)}],
});
} });
await ch.create();
})()`);
console.log(` πŸ“¦ Channel "${CHANNEL_ID}" created`);
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/docs-playwright/screenshot-system-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ async function initSession(
if (createChannel) {
await page.evaluate(`(async () => {
var client = window.client;
var ch = client.channel('messaging', ${JSON.stringify(CHANNEL_ID)}, {
var ch = client.channelManager.ensure({ type: 'messaging', id: ${JSON.stringify(CHANNEL_ID)}, data: {
name: 'System Message Demo',
members: [${JSON.stringify(USER_A)}, ${JSON.stringify(USER_B)}],
});
} });
await ch.create();
})()`);
console.log(` πŸ“¦ Channel "${CHANNEL_ID}" created`);
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/docs-playwright/screenshot-variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ async function initSession(
if (createChannel) {
await page.evaluate(`(async () => {
var client = window.client;
var ch = client.channel('messaging', ${JSON.stringify(CHANNEL_ID)}, {
var ch = client.channelManager.ensure({ type: 'messaging', id: ${JSON.stringify(CHANNEL_ID)}, data: {
name: 'Cookbook Screenshots',
members: [${JSON.stringify(USER_A)}, ${JSON.stringify(USER_B)}],
});
} });
await ch.create();
})()`);
console.log(` πŸ“¦ Channel "${CHANNEL_ID}" created`);
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/docs-playwright/screenshot-voice-recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ async function initSession(
if (createChannel) {
await page.evaluate(`(async () => {
var client = window.client;
var ch = client.channel('messaging', ${JSON.stringify(CHANNEL_ID)}, {
var ch = client.channelManager.ensure({ type: 'messaging', id: ${JSON.stringify(CHANNEL_ID)}, data: {
name: 'Voice Recording Demo',
members: [${JSON.stringify(USER_A)}, ${JSON.stringify(USER_B)}],
});
} });
await ch.create();
})()`);
console.log(` πŸ“¦ Channel "${CHANNEL_ID}" created`);
Expand Down
25 changes: 18 additions & 7 deletions examples/vite/docs-playwright/seed-channel-and-screenshot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -627,17 +627,22 @@ async function run() {
const key = Object.keys(el).find((k) => k.startsWith('__reactFiber'));
let fiber = el[key];
while (fiber) {
if (fiber.memoizedProps?.client?.channel) return fiber.memoizedProps.client;
if (fiber.memoizedProps?.client?.channelManager)
return fiber.memoizedProps.client;
fiber = fiber.return;
}
return null;
};
const client = findClient();
if (!client) return { ok: false, reason: 'no client' };
try {
const ch = client.channel('messaging', channelId, {
members: [userA, userB],
name: 'Design redesign β€” v14',
const ch = client.channelManager.ensure({
type: 'messaging',
id: channelId,
data: {
members: [userA, userB],
name: 'Design redesign β€” v14',
},
});
await ch.watch();
const existingCount = (ch.state.messages || []).length;
Expand Down Expand Up @@ -713,7 +718,7 @@ async function run() {
let fiber = el[key];
let client = null;
while (fiber) {
if (fiber.memoizedProps?.client?.channel) {
if (fiber.memoizedProps?.client?.channelManager) {
client = fiber.memoizedProps.client;
break;
}
Expand Down Expand Up @@ -756,13 +761,19 @@ async function run() {
const results = [];
for (const [channelId, data] of Object.entries(channelImages)) {
try {
const ch = client.channel('public', channelId);
const ch = client.channelManager.ensure({
type: 'public',
id: channelId,
});
await ch.updatePartial({ set: { image: data.image } });
results.push(channelId + ': ok');
} catch (e) {
// Try messaging type too
try {
const ch = client.channel('messaging', channelId);
const ch = client.channelManager.ensure({
type: 'messaging',
id: channelId,
});
await ch.updatePartial({ set: { image: data.image } });
results.push(channelId + ': ok (messaging)');
} catch (e2) {
Expand Down
Loading
Loading