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 AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const App = () => {
For a full-featured chat interface:

```tsx
import type { ChannelFilters, ChannelOptions, ChannelSort, User } from 'stream-chat';
import type { ChannelFilters, ChannelOptions, SortParamRequest, User } from 'stream-chat';
import {
Chat,
Channel,
Expand All @@ -86,7 +86,7 @@ const user: User = {
image: `https://getstream.io/random_png/?name=${userName}`,
};

const sort: ChannelSort = { last_message_at: -1 };
const sort: SortParamRequest[] = [{ direction: -1, field: 'last_message_at' }];
const filters: ChannelFilters = {
type: 'messaging',
members: { $in: [userId] },
Expand Down
4 changes: 2 additions & 2 deletions ai-docs/ai-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Authoritative locations, in order of preference:
1. `node_modules/stream-chat-react/dist/types/index.d.ts` — public type surface. Fastest way to confirm a symbol exists, check a prop signature, or see an override-key name. (The SDK emits `.d.ts` only; there is no `.d.cts`.)
2. `node_modules/stream-chat-react/package.json` — `exports` map and peer dependencies.
3. `node_modules/stream-chat-react/dist/es/` and `dist/cjs/` — transpiled JS when runtime behavior matters more than types.
4. `node_modules/stream-chat/dist/types/index.d.ts` — core client types (channel capabilities, event names, `ReactionSort`, etc.).
4. `node_modules/stream-chat/dist/types/index.d.ts` — core client types (channel capabilities, event names, `SortParamRequest`, etc.).
5. `node_modules/stream-chat-react/dist/css/index.css` — default class names and CSS variables when auditing selectors.

Required workflow:
Expand Down Expand Up @@ -166,7 +166,7 @@ For richer rendering, override `QuotedMessage` or `QuotedMessagePreview` in `Wit
- `QuotedMessagePreviewHeader` → `QuotedMessagePreviewUI`
- `CardAudio` → inline the audio card UI in your own component
- `attachmentTypeIconMap` → inline your own map or use `SummarizedMessagePreview`
- `ReactionDetailsComparator`, `sortReactionDetails` prop → `reactionDetailsSort` with `ReactionSort`
- `ReactionDetailsComparator`, `sortReactionDetails` prop → `reactionDetailsSort` with `SortParamRequest[]`
- `SimpleReactionsList` → `MessageReactions` or a custom compact list
- Standalone icons (`ActionsIcon`, `ReactionIcon`, `ThreadIcon`, `MessageErrorIcon`, `CloseIcon`, `SendIcon`, `MicIcon`, `MessageSentIcon`, `MessageDeliveredIcon`, `RetryIcon`, `DownloadIcon`, `LinkIcon`) → public `Icons` set (e.g. `IconXmark`, `IconCheckmark1Small`, `IconDoubleCheckmark1Small`) or higher-level components (`SendButton`, `MessageStatus`, `MessageActions`)
- `useChannelDeletedListener`, `useNotificationMessageNewListener`, `useMobileNavigation`, siblings → no shim; remove the calls (`ChannelList` handles these events internally)
Expand Down
4 changes: 2 additions & 2 deletions ai-docs/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1995,14 +1995,14 @@ Only confirmed items should move from this file into the migration guide.
- `f06846da:src/components/Reactions/hooks/useProcessReactions.tsx:12` through `:14` still accepted `reaction_counts` and `reactionOptions`
- `f06846da:src/components/Reactions/types.ts:14` still exported `ReactionDetailsComparator`
- New API:
- `src/components/Message/types.ts:83` and `src/context/MessageContext.tsx:107` now expose `reactionDetailsSort?: ReactionSort`
- `src/components/Message/types.ts:83` and `src/context/MessageContext.tsx:107` now expose `reactionDetailsSort?: SortParamRequest[]`
- `src/components/MessageList/MessageList.tsx:496` and `src/components/MessageList/VirtualizedMessageList.tsx:86` now forward `reactionDetailsSort`
- `src/components/Reactions/MessageReactions.tsx:26` through `:40` accept `reaction_groups`, `reactionDetailsSort`, and the narrowed current props only
- `src/components/Reactions/MessageReactionsDetail.tsx:19` through `:26` accept `reactionDetailsSort` and `reactionGroups`, with no `sort` / `sortReactionDetails` migration path
- `src/components/Reactions/hooks/useProcessReactions.tsx:10` through `:13` now accept only `own_reactions`, `reaction_groups`, `reactions`, and `sortReactions`
- `src/components/Reactions/types.ts` no longer exports `ReactionDetailsComparator`
- Replacement:
- replace `sortReactionDetails` with `reactionDetailsSort` and pass a server-side `ReactionSort` object instead of a client comparator
- replace `sortReactionDetails` with `reactionDetailsSort` and pass a server-side `SortParamRequest[]` array instead of a client comparator
- replace `reaction_counts` with `reaction_groups`
- move `reactionOptions` configuration to `<WithComponents overrides={{ reactionOptions }}>`
- update any custom `useProcessReactions` wrappers to the narrower parameter type
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.5",
"stream-chat": "10.0.0-rc.6",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions examples/tutorial/src/3-channel-list/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import type { ChannelFilters, ChannelSort, ClientUser } from 'stream-chat';
import type { ChannelFilters, ClientUser, SortParamRequest } from 'stream-chat';
import { ChannelPaginator } from 'stream-chat';
import {
Channel,
Expand All @@ -22,7 +22,7 @@ const user: ClientUser = {
image: `https://getstream.io/random_png/?name=${userName}`,
};

const sort: ChannelSort = [{ direction: -1, field: 'last_message_at' }];
const sort: SortParamRequest[] = [{ direction: -1, field: 'last_message_at' }];
const filters: ChannelFilters = {
type: 'messaging',
members: { $in: [userId] },
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"modern-normalize": "^3.0.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "10.0.0-rc.5",
"stream-chat": "10.0.0-rc.6",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import type {
ChannelFilters,
ChannelPaginatorRequestOptions,
ChannelSort,
LocalMessage,
SortParamRequest,
TextComposerMiddleware,
} from 'stream-chat';
import {
Expand Down Expand Up @@ -128,7 +128,7 @@ const requestOptions: ChannelPaginatorRequestOptions = {
state: true,
};

const sort: ChannelSort = [
const sort: SortParamRequest[] = [
{ direction: -1, field: 'pinned_at' },
{ direction: -1, field: 'last_message_at' },
{ direction: -1, field: 'updated_at' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export const createServerSideClient = async ({
// Not `getInstance` — that would hand back (and mutate) the app's user-authenticated singleton.
const client = new StreamChat(apiKey, { allowServerSideConnect: true });

client.tokenManager.secret = secret;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@szuperaz secret was needed to perform the server-side client calls. Will this be still possible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, tested it

Screenshot 2026-08-21 at 10 24 26

client.tokenManager.token = token;
client.tokenManager.type = 'static';

Expand Down
19 changes: 18 additions & 1 deletion examples/vite/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,24 @@ export default defineConfig(({ mode }) => {
// Keep local `stream-chat` out of Vite's prebundle so the browser loads the
// SDK build directly and DevTools can follow its sourcemaps back to source files.
// Its local ESM build still imports a few CommonJS deps that need Vite interop.
include: ['base64-js', 'form-data', 'isomorphic-ws', 'axios'],
include: [
'base64-js',
'form-data',
'isomorphic-ws',
'axios',
// The shared i18n layer in `stream-chat` pulls in dayjs, which ships as a
// UMD bundle with no ESM entry. Excluded deps are not crawled by the
// dep scanner, so these have to be named explicitly to get CJS interop.
'dayjs',
'dayjs/plugin/calendar.js',
'dayjs/plugin/duration.js',
'dayjs/plugin/localeData.js',
'dayjs/plugin/localizedFormat.js',
'dayjs/plugin/relativeTime.js',
'dayjs/plugin/timezone.js',
'dayjs/plugin/updateLocale.js',
'dayjs/plugin/utc.js',
],
exclude: localStreamChatEntry ? ['stream-chat'] : [],
},
server: {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"modern-normalize": "^3.0.1",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0",
"stream-chat": "10.0.0-rc.5"
"stream-chat": "10.0.0-rc.6"
},
"peerDependenciesMeta": {
"@breezystack/lamejs": {
Expand Down Expand Up @@ -199,7 +199,7 @@
"react-dom": "^19.2.6",
"sass": "^1.100.0",
"semantic-release": "^25.0.3",
"stream-chat": "10.0.0-rc.5",
"stream-chat": "10.0.0-rc.6",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
"vite": "^8.1.3",
Expand Down
2 changes: 1 addition & 1 deletion specs/message-pagination/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Cross-repo sequencing is required; React branch depends on upstream JS behavior
Extend `MessagePaginator` with optional `parentMessageId`:

- when absent, query channel messages (`channel.query({ messages: ... })`) as before;
- when present, query thread replies (`channel.getReplies(parentMessageId, ...)`);
- when present, query thread replies (`client.getReplies({ parent_id, ... })`);
- include `parent_id` in client-side filters only for thread mode.

`Thread` now constructs `MessagePaginator` with `parentMessageId: thread.id`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import type {
AppSettingsAPIResponse,
Attachment,
LocalAttachment,
SendFileAPIResponse,
} from '../../../../../../stream-chat-js/src';
import type { MessageComposerContextValue } from '../../../../context';

Expand Down Expand Up @@ -402,9 +401,9 @@ describe('MessageInput', () => {
});

vi.spyOn(client, 'getAppSettings').mockResolvedValue({} as AppSettingsAPIResponse);
vi.spyOn(channel, 'uploadFile').mockResolvedValue({
file: fileObjectURL,
} as SendFileAPIResponse);
vi.spyOn(channel, 'uploadFile').mockResolvedValue(
fromPartial({ file: fileObjectURL }),
);

await renderComponent({
channelStateCtx: { channel },
Expand Down
20 changes: 10 additions & 10 deletions src/components/Message/__tests__/Message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ describe('<Message /> component', () => {
const client = await getTestClientWithUser(alice);
const muteUser = vi.fn(() => Promise.resolve());
// @ts-expect-error - mock implementation has simplified signature
vi.spyOn(client, 'muteUser').mockImplementation(muteUser);
vi.spyOn(client.moderation, 'mute').mockImplementation(muteUser);
let context: MessageContextValue;

await renderComponent({
Expand All @@ -490,14 +490,14 @@ describe('<Message /> component', () => {

await context.handleMute(mouseEventMock);

expect(muteUser).toHaveBeenCalledWith(bob.id);
expect(muteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
});

it('should throw when muting a user fails', async () => {
const message = generateMessage({ user: bob });
const client = await getTestClientWithUser(alice);
const muteUser = vi.fn(() => Promise.reject(new Error('mute failed')));
vi.spyOn(client, 'muteUser').mockImplementation(muteUser);
vi.spyOn(client.moderation, 'mute').mockImplementation(muteUser);
let context: MessageContextValue;

await renderComponent({
Expand All @@ -511,15 +511,15 @@ describe('<Message /> component', () => {

await context.handleMute(mouseEventMock);

expect(muteUser).toHaveBeenCalledWith(bob.id);
expect(muteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
});

it('should allow to unmute a user when it is successful', async () => {
const message = generateMessage({ user: bob });
const client = await getTestClientWithUser(alice);
const unmuteUser = vi.fn(() => Promise.resolve());
// @ts-expect-error - mock implementation has simplified signature
vi.spyOn(client, 'unmuteUser').mockImplementation(unmuteUser);
vi.spyOn(client.moderation, 'unmute').mockImplementation(unmuteUser);
let context: MessageContextValue;

await renderComponent({
Expand All @@ -535,14 +535,14 @@ describe('<Message /> component', () => {

await context.handleMute(mouseEventMock);

expect(unmuteUser).toHaveBeenCalledWith(bob.id);
expect(unmuteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
});

it('should throw when unmuting a user fails', async () => {
const message = generateMessage({ user: bob });
const client = await getTestClientWithUser(alice);
const unmuteUser = vi.fn(() => Promise.reject(new Error('unmute failed')));
vi.spyOn(client, 'unmuteUser').mockImplementation(unmuteUser);
vi.spyOn(client.moderation, 'unmute').mockImplementation(unmuteUser);
let context: MessageContextValue;

await renderComponent({
Expand All @@ -558,7 +558,7 @@ describe('<Message /> component', () => {

await context.handleMute(mouseEventMock);

expect(unmuteUser).toHaveBeenCalledWith(bob.id);
expect(unmuteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
});

it.each([
Expand Down Expand Up @@ -731,7 +731,7 @@ describe('<Message /> component', () => {
const client = await getTestClientWithUser(alice);
const flagMessage = vi.fn(() => Promise.resolve());
// @ts-expect-error - mock implementation has simplified signature
vi.spyOn(client, 'flagMessage').mockImplementation(flagMessage);
vi.spyOn(client.moderation, 'flagMessage').mockImplementation(flagMessage);
let context: MessageContextValue;

await renderComponent({
Expand All @@ -751,7 +751,7 @@ describe('<Message /> component', () => {
const message = generateMessage();
const client = await getTestClientWithUser(alice);
const flagMessage = vi.fn(() => Promise.reject(new Error('flag failed')));
vi.spyOn(client, 'flagMessage').mockImplementation(flagMessage);
vi.spyOn(client.moderation, 'flagMessage').mockImplementation(flagMessage);
let context: MessageContextValue;

await renderComponent({
Expand Down
11 changes: 7 additions & 4 deletions src/components/Message/hooks/__tests__/useFlagHandler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { Channel } from '../../../Channel';
import { Chat } from '../../../Chat';

// MERGE-RECONCILE (test migration): the master merge removed ChannelStateContext.
// `useFlagHandler` reads the client from ChatContext and flags through `client.flagMessage`.
// `useFlagHandler` reads the client from ChatContext and flags through
// `client.moderation.flagMessage`.
// The wrapper now uses the real <Chat>/<Channel> providers and assertions spy on
// `client.flagMessage` instead of stubbing it on a mocked client.
// `client.moderation.flagMessage` instead of stubbing it on a mocked client.

let channel: ChannelType;
let client: StreamChat;
Expand Down Expand Up @@ -61,7 +62,9 @@ describe('useHandleFlag custom hook', () => {

it('should allow to flag a message when it is successful', async () => {
const message = generateMessage() as unknown as LocalMessage;
const flagSpy = vi.spyOn(client, 'flagMessage').mockResolvedValue(fromPartial({}));
const flagSpy = vi
.spyOn(client.moderation, 'flagMessage')
.mockResolvedValue(fromPartial({}));
const handleFlag = await renderUseHandleFlagHook(message);
await handleFlag(mouseEventMock);
expect(flagSpy).toHaveBeenCalledWith(message.id);
Expand All @@ -70,7 +73,7 @@ describe('useHandleFlag custom hook', () => {
it('should throw when flagging fails', async () => {
const message = generateMessage() as unknown as LocalMessage;
const flagSpy = vi
.spyOn(client, 'flagMessage')
.spyOn(client.moderation, 'flagMessage')
.mockRejectedValue(new Error('flag failed'));
const handleFlag = await renderUseHandleFlagHook(message);
await expect(handleFlag(mouseEventMock)).rejects.toThrow('flag failed');
Expand Down
12 changes: 6 additions & 6 deletions src/components/Message/hooks/__tests__/useMuteHandler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ async function renderUseHandleMuteHook(
{ mutes = [] as Mute[] }: { mutes?: Mute[] } = {},
) {
const client = await getTestClientWithUser(alice);
client.muteUser = muteUser;
client.unmuteUser = unmuteUser;
client.moderation.mute = muteUser;
client.moderation.unmute = unmuteUser;
client.mutedUsersStore.partialNext({ mutedUsers: mutes });

const wrapper = ({ children }: { children?: React.ReactNode }) => (
Expand Down Expand Up @@ -65,15 +65,15 @@ describe('useHandleMute custom hook', () => {
const message = generateMessage({ user: bob }) as MessageResponse & LocalMessage;
const handleMute = await renderUseHandleMuteHook(message);
await handleMute(mouseEventMock);
expect(muteUser).toHaveBeenCalledWith(bob.id);
expect(muteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
});

it('should notify (and not throw) when muting a user fails', async () => {
const message = generateMessage({ user: bob }) as MessageResponse & LocalMessage;
muteUser.mockImplementationOnce(() => Promise.reject(new Error('mute failed')));
const handleMute = await renderUseHandleMuteHook(message);
await expect(handleMute(mouseEventMock)).resolves.toBeUndefined();
expect(muteUser).toHaveBeenCalledWith(bob.id);
expect(muteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
expect(notify).toHaveBeenCalledWith(expect.any(String), 'error');
});

Expand All @@ -84,7 +84,7 @@ describe('useHandleMute custom hook', () => {
mutes: [fromPartial<Mute>({ target: { id: bob.id } })],
});
await handleMute(mouseEventMock);
expect(unmuteUser).toHaveBeenCalledWith(bob.id);
expect(unmuteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
});

it('should notify (and not throw) when unmuting a user fails', async () => {
Expand All @@ -94,7 +94,7 @@ describe('useHandleMute custom hook', () => {
mutes: [fromPartial<Mute>({ target: { id: bob.id } })],
});
await expect(handleMute(mouseEventMock)).resolves.toBeUndefined();
expect(unmuteUser).toHaveBeenCalledWith(bob.id);
expect(unmuteUser).toHaveBeenCalledWith({ target_ids: [bob.id] });
expect(notify).toHaveBeenCalledWith(expect.any(String), 'error');
});
});
2 changes: 1 addition & 1 deletion src/components/Message/hooks/useFlagHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export const useFlagHandler = (message?: LocalMessage): ReactEventHandler => {
return;
}

await client.flagMessage(message.id);
await client.moderation.flagMessage(message.id);
};
};
4 changes: 2 additions & 2 deletions src/components/Message/hooks/useMuteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const useMuteHandler = (

if (!isUserMuted(message, mutes)) {
try {
await client.muteUser(message.user.id);
await client.moderation.mute({ target_ids: [message.user.id] });

if (!notify) return;
const successMessage =
Expand All @@ -62,7 +62,7 @@ export const useMuteHandler = (
}
} else {
try {
await client.unmuteUser(message.user.id);
await client.moderation.unmute({ target_ids: [message.user.id] });

if (!notify) return;
const successMessage =
Expand Down
Loading