Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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
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
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
6 changes: 3 additions & 3 deletions src/components/Message/hooks/useReactionsFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useStableCallback } from '../../../utils/useStableCallback';
import type {
LocalMessage,
ReactionResponse,
ReactionSort,
SortParamRequest,
StreamChat,
} from 'stream-chat';
import type { ReactionType } from '../../Reactions/types';
Expand All @@ -13,7 +13,7 @@ export const MAX_MESSAGE_REACTIONS_TO_FETCH = 1000;
export function useReactionsFetcher(message: LocalMessage) {
const { client } = useChatContext();

return useStableCallback((reactionType?: ReactionType, sort?: ReactionSort) =>
return useStableCallback((reactionType?: ReactionType, sort?: SortParamRequest[]) =>
fetchMessageReactions(client, message.id, reactionType, sort),
);
}
Expand All @@ -22,7 +22,7 @@ async function fetchMessageReactions(
client: StreamChat,
messageId: string,
reactionType?: ReactionType,
sort?: ReactionSort,
sort?: SortParamRequest[],
) {
const reactions: ReactionResponse[] = [];
const limit = 25;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Message/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BaseSyntheticEvent } from 'react';
import type { LocalMessage, ReactionSort, UserResponse } from 'stream-chat';
import type { LocalMessage, SortParamRequest, UserResponse } from 'stream-chat';

import type { UserEventHandler } from './hooks';
import type { CustomMentionHandler } from './hooks/useMentionsHandler';
Expand Down Expand Up @@ -55,7 +55,7 @@ export type MessageProps = {
/** Custom open-thread handler; overrides the default ChatView-navigation thread opening */
openThread?: (message: LocalMessage, event: BaseSyntheticEvent) => void;
/** Sort options to provide to a reactions query */
reactionDetailsSort?: ReactionSort;
reactionDetailsSort?: SortParamRequest[];
/** A list of users that have read this Message if the message is the last one and was posted by my user */
readBy?: UserResponse[];
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
LocalAttachment,
LocalMessage,
SearchSourceState,
SendFileAPIResponse,
StreamChat,
TextComposerSuggestion,
UserResponse,
Expand Down Expand Up @@ -329,12 +328,12 @@ const setup = async ({ channelData }: { channelData?: GenerateChannelOptions } =
customUser: user,
});
const sendImageSpy = vi.spyOn(customChannel, 'uploadImage').mockResolvedValueOnce(
fromPartial<SendFileAPIResponse>({
fromPartial<Awaited<ReturnType<ChannelType['uploadImage']>>>({
file: fileUploadUrl,
}),
);
const sendFileSpy = vi.spyOn(customChannel, 'uploadFile').mockResolvedValueOnce(
fromPartial<SendFileAPIResponse>({
fromPartial<Awaited<ReturnType<ChannelType['uploadFile']>>>({
file: fileUploadUrl,
}),
);
Expand Down
7 changes: 2 additions & 5 deletions src/components/MessageComposer/hooks/useSendMessageFn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTranslationContext } from '../../../context/TranslationContext';
import { useMessageComposerController } from '..';
import { useChannel, useThreadContext } from '../../..';
import { MessageComposer, type MessageRequest } from 'stream-chat';
import { MessageComposer } from 'stream-chat';
import { useStableCallback } from '../../../utils/useStableCallback';

const takeStateSnapshot = (messageComposer: MessageComposer) => {
Expand Down Expand Up @@ -68,10 +68,7 @@ export const useSendMessageFn = () => {

await (thread ?? channel).sendMessageWithLocalUpdate({
localMessage,
// `useSendMessageFn` only runs for new messages; edits go through a separate
// update handler. `compose()` widens `message` to `MessageRequest | UpdatedMessage`,
// but in this path it is always a `MessageRequest`.
message: message as MessageRequest,
message,
options: sendOptions,
});

Expand Down
Loading
Loading