From a2b4ccb87d2130c2dae42cbee0d907affcb5ddcc Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 20 Aug 2026 08:36:42 -0500 Subject: [PATCH 1/6] fix: adapt to removed stream-chat type aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `stream-chat` collapsed the twelve `*Sort` aliases, which were all exactly `SortParamRequest[]` — twelve names for one type — and retired the `APIResponse`-based response aliases in favour of the generated response types. * `ReactionSort` -> `SortParamRequest[]` across 13 sites in 5 files. Note the brackets: the alias *was* the array, so this is not `SortParamRequest`. Four of the sites are public API and change the emitted `.d.ts`: `MessageContextValue.handleFetchReactions` and `.reactionDetailsSort`, `MessageProps.reactionDetailsSort`, `MessageReactionsDetailProps.sort`, and `FetchReactionsOptions.sort`. The type is structurally identical, so no consumer code needs to change — only the name they import if they annotate it themselves. * `ChannelSort` -> `SortParamRequest[]` in the tutorial and vite examples. * `SendFileAPIResponse` -> `Awaited>` in the two test files that annotated upload spies, following the `ReturnType<…>` idiom already used for `markRead` in 7842c1740 so these track the method rather than a name. Deriving from the method also drops `SendFileAPIResponse`'s claim that `file` is required — the generated `FileUploadResponse.file` is optional. That was always true of the real response; the alias was hiding it. * `useSendMessageFn` no longer casts `message` to `MessageRequest`. `compose()` used to widen it to `MessageRequest | UpdatedMessage` and the cast narrowed it back; the union is gone, so the cast and its explanatory comment are both dead. Docs: `AI.md` also had a stale `const sort: ChannelSort = { last_message_at: -1 }` — the v9 keyed-object form, which stopped being valid when sort became an array. Corrected alongside the rename. `specs/message-pagination/decisions.md` referenced `channel.getReplies(...)`, which `stream-chat` removed in favour of `client.getReplies()`. Verified: `tsc -p tsconfig.lib.json` clean, 2828 tests passing, eslint clean on every touched file. Note on `yarn types`: the root `tsconfig.json` is solution-style (`"files": []` plus `references`), so `tsc --noEmit` without `--build` typechecks nothing and the script passes unconditionally. The real check is `tsc -p tsconfig.lib.json`. Left alone here as it is a pre-existing issue unrelated to this change. Note on `yarn types:tests`: already failing before this change with ~1216 errors, none of them from this migration — `ChannelAPIResponse`, `QueryChannelAPIResponse`, `MuteChannelAPIResponse`, `AppSettingsAPIResponse` and several stream-chat-react-local context types were removed by earlier v10 work and never migrated in the test tree. This change takes that count down by 2 and adds none. Co-Authored-By: Claude Opus 5 --- AI.md | 4 ++-- ai-docs/ai-migration.md | 4 ++-- ai-docs/breaking-changes.md | 4 ++-- examples/tutorial/src/3-channel-list/App.tsx | 4 ++-- examples/vite/src/App.tsx | 4 ++-- specs/message-pagination/decisions.md | 2 +- .../AudioRecorder/__tests__/AudioRecorder.test.tsx | 7 +++---- src/components/Message/hooks/useReactionsFetcher.ts | 6 +++--- src/components/Message/types.ts | 4 ++-- .../MessageComposer/__tests__/MessageInput.test.tsx | 5 ++--- src/components/MessageComposer/hooks/useSendMessageFn.ts | 7 ++----- src/components/Reactions/MessageReactionsDetail.tsx | 8 +++++--- src/components/Reactions/hooks/useFetchReactions.ts | 4 ++-- src/context/MessageContext.tsx | 6 +++--- 14 files changed, 33 insertions(+), 36 deletions(-) diff --git a/AI.md b/AI.md index 0fdb9ed29d..e60c8fba37 100644 --- a/AI.md +++ b/AI.md @@ -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, @@ -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] }, diff --git a/ai-docs/ai-migration.md b/ai-docs/ai-migration.md index de785441b2..557363c77a 100644 --- a/ai-docs/ai-migration.md +++ b/ai-docs/ai-migration.md @@ -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: @@ -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) diff --git a/ai-docs/breaking-changes.md b/ai-docs/breaking-changes.md index f98bb95cb1..600a484ebc 100644 --- a/ai-docs/breaking-changes.md +++ b/ai-docs/breaking-changes.md @@ -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 `` - update any custom `useProcessReactions` wrappers to the narrower parameter type diff --git a/examples/tutorial/src/3-channel-list/App.tsx b/examples/tutorial/src/3-channel-list/App.tsx index c3969057ec..0265c6fde3 100644 --- a/examples/tutorial/src/3-channel-list/App.tsx +++ b/examples/tutorial/src/3-channel-list/App.tsx @@ -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, @@ -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] }, diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index 81ea621049..bc42924711 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -9,8 +9,8 @@ import { import type { ChannelFilters, ChannelPaginatorRequestOptions, - ChannelSort, LocalMessage, + SortParamRequest, TextComposerMiddleware, } from 'stream-chat'; import { @@ -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' }, diff --git a/specs/message-pagination/decisions.md b/specs/message-pagination/decisions.md index b2df061362..28300961e8 100644 --- a/specs/message-pagination/decisions.md +++ b/specs/message-pagination/decisions.md @@ -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`. diff --git a/src/components/MediaRecorder/AudioRecorder/__tests__/AudioRecorder.test.tsx b/src/components/MediaRecorder/AudioRecorder/__tests__/AudioRecorder.test.tsx index 8627523ef9..2378937fab 100644 --- a/src/components/MediaRecorder/AudioRecorder/__tests__/AudioRecorder.test.tsx +++ b/src/components/MediaRecorder/AudioRecorder/__tests__/AudioRecorder.test.tsx @@ -50,7 +50,6 @@ import type { AppSettingsAPIResponse, Attachment, LocalAttachment, - SendFileAPIResponse, } from '../../../../../../stream-chat-js/src'; import type { MessageComposerContextValue } from '../../../../context'; @@ -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 }, diff --git a/src/components/Message/hooks/useReactionsFetcher.ts b/src/components/Message/hooks/useReactionsFetcher.ts index 80908c6ea6..03c0c064b6 100644 --- a/src/components/Message/hooks/useReactionsFetcher.ts +++ b/src/components/Message/hooks/useReactionsFetcher.ts @@ -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'; @@ -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), ); } @@ -22,7 +22,7 @@ async function fetchMessageReactions( client: StreamChat, messageId: string, reactionType?: ReactionType, - sort?: ReactionSort, + sort?: SortParamRequest[], ) { const reactions: ReactionResponse[] = []; const limit = 25; diff --git a/src/components/Message/types.ts b/src/components/Message/types.ts index 1ac688a7d0..f71655c1b9 100644 --- a/src/components/Message/types.ts +++ b/src/components/Message/types.ts @@ -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'; @@ -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[]; /** diff --git a/src/components/MessageComposer/__tests__/MessageInput.test.tsx b/src/components/MessageComposer/__tests__/MessageInput.test.tsx index ffa9840d2f..94c581f3c3 100644 --- a/src/components/MessageComposer/__tests__/MessageInput.test.tsx +++ b/src/components/MessageComposer/__tests__/MessageInput.test.tsx @@ -13,7 +13,6 @@ import type { LocalAttachment, LocalMessage, SearchSourceState, - SendFileAPIResponse, StreamChat, TextComposerSuggestion, UserResponse, @@ -329,12 +328,12 @@ const setup = async ({ channelData }: { channelData?: GenerateChannelOptions } = customUser: user, }); const sendImageSpy = vi.spyOn(customChannel, 'uploadImage').mockResolvedValueOnce( - fromPartial({ + fromPartial>>({ file: fileUploadUrl, }), ); const sendFileSpy = vi.spyOn(customChannel, 'uploadFile').mockResolvedValueOnce( - fromPartial({ + fromPartial>>({ file: fileUploadUrl, }), ); diff --git a/src/components/MessageComposer/hooks/useSendMessageFn.ts b/src/components/MessageComposer/hooks/useSendMessageFn.ts index c281750d12..e455432f3d 100644 --- a/src/components/MessageComposer/hooks/useSendMessageFn.ts +++ b/src/components/MessageComposer/hooks/useSendMessageFn.ts @@ -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) => { @@ -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, }); diff --git a/src/components/Reactions/MessageReactionsDetail.tsx b/src/components/Reactions/MessageReactionsDetail.tsx index 4461912b81..b4ce7143b7 100644 --- a/src/components/Reactions/MessageReactionsDetail.tsx +++ b/src/components/Reactions/MessageReactionsDetail.tsx @@ -12,7 +12,7 @@ import { useMessageContext, useTranslationContext, } from '../../context'; -import type { ReactionSort } from 'stream-chat'; +import type { SortParamRequest } from 'stream-chat'; import { defaultReactionOptions, getHasExtendedReactions } from './reactionOptions'; import type { useProcessReactions } from './hooks/useProcessReactions'; import { IconEmojiAdd } from '../Icons'; @@ -24,12 +24,14 @@ export type MessageReactionsDetailProps = Partial< reactions: ReactionSummary[]; selectedReactionType: ReactionType | null; onSelectedReactionTypeChange?: (reactionType: ReactionType | null) => void; - sort?: ReactionSort; + sort?: SortParamRequest[]; totalReactionCount?: number; reactionGroups?: ReturnType['reactionGroups']; } & ReactionSelectorProps; -const defaultReactionDetailsSort: ReactionSort = [{ direction: -1, field: 'created_at' }]; +const defaultReactionDetailsSort: SortParamRequest[] = [ + { direction: -1, field: 'created_at' }, +]; export const MessageReactionsDetailLoadingIndicator = () => { const elements = useMemo( diff --git a/src/components/Reactions/hooks/useFetchReactions.ts b/src/components/Reactions/hooks/useFetchReactions.ts index cefb06aed4..316db39616 100644 --- a/src/components/Reactions/hooks/useFetchReactions.ts +++ b/src/components/Reactions/hooks/useFetchReactions.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import type { ReactionResponse, ReactionSort } from 'stream-chat'; +import type { ReactionResponse, SortParamRequest } from 'stream-chat'; import type { MessageContextValue } from '../../../context'; import { useMessageContext, useTranslationContext } from '../../../context'; import { useNotificationApi } from '../../Notifications'; @@ -10,7 +10,7 @@ export interface FetchReactionsOptions { reactionType: ReactionType | null; shouldFetch: boolean; handleFetchReactions?: MessageContextValue['handleFetchReactions']; - sort?: ReactionSort; + sort?: SortParamRequest[]; } export function useFetchReactions(options: FetchReactionsOptions) { diff --git a/src/context/MessageContext.tsx b/src/context/MessageContext.tsx index 0119ff16e7..c20e8e1d73 100644 --- a/src/context/MessageContext.tsx +++ b/src/context/MessageContext.tsx @@ -5,7 +5,7 @@ import type { DeleteMessageOptions, LocalMessage, ReactionResponse, - ReactionSort, + SortParamRequest, UserResponse, } from 'stream-chat'; @@ -33,7 +33,7 @@ export type MessageContextValue = { /** Function to fetch the message reactions */ handleFetchReactions: ( reactionType?: ReactionType, - sort?: ReactionSort, + sort?: SortParamRequest[], ) => Promise>; /** Function to flag a message in a Channel */ handleFlag: ReactEventHandler; @@ -95,7 +95,7 @@ export type MessageContextValue = { /** DOMRect object for parent MessageList component */ messageListRect?: DOMRect; /** Sort options to provide to a reactions query */ - reactionDetailsSort?: ReactionSort; + reactionDetailsSort?: SortParamRequest[]; /** A list of users that have read this Message */ readBy?: UserResponse[]; /** When set, shows the sender avatar in a grid layout. Values: true | 'incoming' | 'outgoing'. */ From 3693ba9725228cc7fbd6966270fb7dfc26ecf5e1 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 20 Aug 2026 12:49:41 -0500 Subject: [PATCH 2/6] fix: call moderation through client.moderation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `stream-chat` removed the hand-written `/moderation/*` methods from `StreamChat` in favour of the generated V2 moderation API, reachable as `client.moderation`. * `client.muteUser(id)` -> `client.moderation.mute({ target_ids: [id] })` * `client.unmuteUser(id)` -> `client.moderation.unmute({ target_ids: [id] })` * `client.flagMessage(id)` -> `client.moderation.flagMessage(id)` V2 mute/unmute take `target_ids` as an array, hence the wrapping. `flagMessage` keeps its `(messageId)` shape because the moderation wrapper supplies the entity type, and `reason` is optional there. Call sites: `useMuteHandler`, `useFlagHandler`, and the mute toggles in `ChannelManagementActions.defaults` and `ChannelMemberActions.defaults`. `channel.banUser` / `channel.unbanUser` are unchanged — `Channel` keeps those wrappers, and `unbanUser` has no generated equivalent so it stays on v1 upstream. No public API change: none of these responses were read, and no exported type or prop signature moves. Tests: the spies moved from `client.muteUser` / `client.unmuteUser` / `client.flagMessage` to their `client.moderation` counterparts, and the mute assertions expect `{ target_ids: [id] }` rather than a bare id. The hoisted mock client in `ChannelManagementActions.defaults.test` grows a `moderation` object for the same reason. Verified: `tsc -p tsconfig.lib.json` clean, 2828 tests passing, eslint clean on every touched file. Co-Authored-By: Claude Opus 5 --- .../Message/__tests__/Message.test.tsx | 20 +++++++++---------- .../hooks/__tests__/useFlagHandler.test.tsx | 11 ++++++---- .../hooks/__tests__/useMuteHandler.test.tsx | 12 +++++------ .../Message/hooks/useFlagHandler.ts | 2 +- .../Message/hooks/useMuteHandler.ts | 4 ++-- .../ChannelManagementActions.defaults.tsx | 8 ++++---- .../ChannelMemberActions.defaults.tsx | 8 ++++---- ...ChannelManagementActions.defaults.test.tsx | 9 ++++----- 8 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/components/Message/__tests__/Message.test.tsx b/src/components/Message/__tests__/Message.test.tsx index 4c5fbf8e03..47c72c7a57 100644 --- a/src/components/Message/__tests__/Message.test.tsx +++ b/src/components/Message/__tests__/Message.test.tsx @@ -476,7 +476,7 @@ describe(' 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({ @@ -490,14 +490,14 @@ describe(' 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({ @@ -511,7 +511,7 @@ describe(' 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 () => { @@ -519,7 +519,7 @@ describe(' component', () => { 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({ @@ -535,14 +535,14 @@ describe(' 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({ @@ -558,7 +558,7 @@ describe(' component', () => { await context.handleMute(mouseEventMock); - expect(unmuteUser).toHaveBeenCalledWith(bob.id); + expect(unmuteUser).toHaveBeenCalledWith({ target_ids: [bob.id] }); }); it.each([ @@ -731,7 +731,7 @@ describe(' 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({ @@ -751,7 +751,7 @@ describe(' 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({ diff --git a/src/components/Message/hooks/__tests__/useFlagHandler.test.tsx b/src/components/Message/hooks/__tests__/useFlagHandler.test.tsx index 9601fb0b0c..ecfd04084b 100644 --- a/src/components/Message/hooks/__tests__/useFlagHandler.test.tsx +++ b/src/components/Message/hooks/__tests__/useFlagHandler.test.tsx @@ -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 / 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; @@ -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); @@ -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'); diff --git a/src/components/Message/hooks/__tests__/useMuteHandler.test.tsx b/src/components/Message/hooks/__tests__/useMuteHandler.test.tsx index 22aad6f908..bb8f9aa467 100644 --- a/src/components/Message/hooks/__tests__/useMuteHandler.test.tsx +++ b/src/components/Message/hooks/__tests__/useMuteHandler.test.tsx @@ -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 }) => ( @@ -65,7 +65,7 @@ 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 () => { @@ -73,7 +73,7 @@ describe('useHandleMute custom hook', () => { 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'); }); @@ -84,7 +84,7 @@ describe('useHandleMute custom hook', () => { mutes: [fromPartial({ 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 () => { @@ -94,7 +94,7 @@ describe('useHandleMute custom hook', () => { mutes: [fromPartial({ 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'); }); }); diff --git a/src/components/Message/hooks/useFlagHandler.ts b/src/components/Message/hooks/useFlagHandler.ts index ad8cdda91f..a7187ba2b0 100644 --- a/src/components/Message/hooks/useFlagHandler.ts +++ b/src/components/Message/hooks/useFlagHandler.ts @@ -19,6 +19,6 @@ export const useFlagHandler = (message?: LocalMessage): ReactEventHandler => { return; } - await client.flagMessage(message.id); + await client.moderation.flagMessage(message.id); }; }; diff --git a/src/components/Message/hooks/useMuteHandler.ts b/src/components/Message/hooks/useMuteHandler.ts index 133564da8c..0149571349 100644 --- a/src/components/Message/hooks/useMuteHandler.ts +++ b/src/components/Message/hooks/useMuteHandler.ts @@ -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 = @@ -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 = diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx index 3cc2024d1a..f705a52654 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx @@ -339,8 +339,8 @@ const UserMuteAction = () => { if (!targetUserId) return; if (!nextMuted) { - return client - .unmuteUser(targetUserId) + return client.moderation + .unmute({ target_ids: [targetUserId] }) .then(() => addNotification({ context: { channel }, @@ -373,8 +373,8 @@ const UserMuteAction = () => { }); } - return client - .muteUser(targetUserId) + return client.moderation + .mute({ target_ids: [targetUserId] }) .then(() => addNotification({ context: { channel }, diff --git a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx index db1a5aba54..b7bbe1273a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx @@ -301,8 +301,8 @@ const UserMuteAction = () => { if (!userId) return; if (!nextMuted) { - return client - .unmuteUser(userId) + return client.moderation + .unmute({ target_ids: [userId] }) .then(() => addNotification({ context: { channel }, @@ -334,8 +334,8 @@ const UserMuteAction = () => { }); } - return client - .muteUser(userId) + return client.moderation + .mute({ target_ids: [userId] }) .then(() => addNotification({ context: { channel }, diff --git a/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx b/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx index 1563a99ce9..46d9e98317 100644 --- a/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx +++ b/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx @@ -85,9 +85,8 @@ const mocks = vi.hoisted(() => { const client = { blockedUsers, blockUser, - muteUser, + moderation: { mute: muteUser, unmute: unmuteUser }, unblockUser, - unmuteUser, user: { id: 'own-user' }, userID: 'own-user', }; @@ -282,7 +281,7 @@ describe('DefaultChannelManagementActions', () => { await advanceDebounce(); - expect(mocks.muteUser).toHaveBeenCalledWith('other-user'); + expect(mocks.muteUser).toHaveBeenCalledWith({ target_ids: ['other-user'] }); expect(screen.getByRole('button', { name: 'Mute user' })).toHaveAttribute( 'aria-pressed', 'false', @@ -328,7 +327,7 @@ describe('DefaultChannelManagementActions', () => { await advanceDebounce(); - expect(mocks.unmuteUser).toHaveBeenCalledWith('other-user'); + expect(mocks.unmuteUser).toHaveBeenCalledWith({ target_ids: ['other-user'] }); expect(mocks.muteUser).not.toHaveBeenCalled(); expect(screen.getByRole('button', { name: 'Mute user' })).toHaveAttribute( 'aria-pressed', @@ -356,7 +355,7 @@ describe('DefaultChannelManagementActions', () => { await advanceDebounce(); - expect(mocks.muteUser).toHaveBeenCalledWith('other-user'); + expect(mocks.muteUser).toHaveBeenCalledWith({ target_ids: ['other-user'] }); expect(mocks.addNotification).toHaveBeenCalledWith( expect.objectContaining({ message: 'User muted', From 967a62487715a1d8b13fe2310def0dd4122aa5b1 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 20 Aug 2026 15:05:31 -0500 Subject: [PATCH 3/6] refactor: adopt the derived VotingVisibility type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `VotingVisibility` is no longer an enum in stream-chat — it is `NonNullable`, so it is not a runtime value any more. Member names matched their string values, so `VotingVisibility.anonymous` becomes `'anonymous'`. Co-Authored-By: Claude Opus 5 --- .../Poll/PollCreationDialog/PollCreationDialog.tsx | 5 +---- src/components/Poll/__tests__/PollOptionList.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx index 65dd51adcb..a9dcb750c0 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import type { PollComposerState } from 'stream-chat'; -import { VotingVisibility } from 'stream-chat'; import { MultipleAnswersField } from './MultipleAnswersField'; import { NameField } from './NameField'; import { OptionFieldSet } from './OptionFieldSet'; @@ -61,9 +60,7 @@ export const PollCreationDialog = ({ close }: PollCreationDialogProps) => { id='voting_visibility' onChange={(e) => pollComposer.updateFields({ - voting_visibility: e.target.checked - ? VotingVisibility.anonymous - : VotingVisibility.public, + voting_visibility: e.target.checked ? 'anonymous' : 'public', }) } title={t('poll.creationDialog.anonymousPoll.title', 'Anonymous Poll')} diff --git a/src/components/Poll/__tests__/PollOptionList.test.tsx b/src/components/Poll/__tests__/PollOptionList.test.tsx index 300ad4739a..86276b78dd 100644 --- a/src/components/Poll/__tests__/PollOptionList.test.tsx +++ b/src/components/Poll/__tests__/PollOptionList.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Poll, StateStore, VotingVisibility } from 'stream-chat'; +import { Poll, StateStore } from 'stream-chat'; import type { Channel, OwnCapabilitiesState, StreamChat } from 'stream-chat'; import { fromPartial } from '@total-typescript/shoehorn'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; @@ -268,7 +268,7 @@ describe('PollOptionList', () => { }); it('does not renders voter avatars with options for anonymous poll', () => { - const pollData = generatePoll({ voting_visibility: VotingVisibility.anonymous }); + const pollData = generatePoll({ voting_visibility: 'anonymous' }); const { container } = renderComponent({ poll: new Poll({ client: fromPartial({}), poll: pollData }), }); From 14b0d919b07ddf423b98e75742ba5f9eaa809f8e Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 21 Aug 2026 09:50:35 -0500 Subject: [PATCH 4/6] feat: upgrade stream-chat-js version --- examples/tutorial/package.json | 2 +- examples/vite/package.json | 2 +- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json index 4cadb1aff6..f474857bf2 100644 --- a/examples/tutorial/package.json +++ b/examples/tutorial/package.json @@ -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": { diff --git a/examples/vite/package.json b/examples/vite/package.json index afd74e868c..9ba6229636 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -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": { diff --git a/package.json b/package.json index 04919beae0..ff27bbdefa 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", diff --git a/yarn.lock b/yarn.lock index f10c2ac902..9080fd59b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1829,7 +1829,7 @@ __metadata: emoji-mart: "npm:^5.6.0" react: "npm:^19.2.6" react-dom: "npm:^19.2.6" - stream-chat: "npm:10.0.0-rc.5" + stream-chat: "npm:10.0.0-rc.6" stream-chat-react: "workspace:^" typescript: "npm:^6.0.3" vite: "npm:^8.1.3" @@ -1856,7 +1856,7 @@ __metadata: react: "npm:^19.2.6" react-dom: "npm:^19.2.6" sass: "npm:^1.100.0" - stream-chat: "npm:10.0.0-rc.5" + stream-chat: "npm:10.0.0-rc.6" stream-chat-react: "workspace:^" typescript: "npm:^6.0.3" vite: "npm:^8.1.3" @@ -9516,7 +9516,7 @@ __metadata: remark-parse: "npm:^11.0.0" sass: "npm:^1.100.0" semantic-release: "npm:^25.0.3" - stream-chat: "npm:10.0.0-rc.5" + stream-chat: "npm:10.0.0-rc.6" typescript: "npm:^6.0.3" typescript-eslint: "npm:^8.59.4" unified: "npm:^11.0.5" @@ -9534,7 +9534,7 @@ __metadata: 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 dependenciesMeta: "@parcel/watcher": built: true @@ -9560,9 +9560,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:10.0.0-rc.5": - version: 10.0.0-rc.5 - resolution: "stream-chat@npm:10.0.0-rc.5" +"stream-chat@npm:10.0.0-rc.6": + version: 10.0.0-rc.6 + resolution: "stream-chat@npm:10.0.0-rc.6" dependencies: "@stream-io/logger": "npm:^2.0.0" axios: "npm:^1.19.0" @@ -9574,7 +9574,7 @@ __metadata: built: true husky: built: true - checksum: 10c0/e04a95a1ede0a8eaca72cf31a3b0c2bfd22f78812c4fb7dff225ad44f99502dd7b00edbe2bc2cf712dd53e4997ca796212d7adb0be709e249e0fcfd917d75c01 + checksum: 10c0/206296ac6492e75abebc67d7532a3117bd5f6669b5d5ac12167c0e29948d60fd1d6fedc0c3abc185a3b8aaa0163e2d002d35e9f6eb7e205eee9ac6e82d7a70a0 languageName: node linkType: hard From 528eb3e6c8e7c40123575642726b762b8a8a169e Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 21 Aug 2026 10:01:14 -0500 Subject: [PATCH 5/6] fix: daysjs module error --- examples/vite/vite.config.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/examples/vite/vite.config.ts b/examples/vite/vite.config.ts index 324fc1dc92..927955e2b7 100644 --- a/examples/vite/vite.config.ts +++ b/examples/vite/vite.config.ts @@ -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: { From 46080bd9d620a3ee56c2b74c24d5b625e388afd0 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 21 Aug 2026 10:07:04 -0500 Subject: [PATCH 6/6] fix: build issue for vite example --- .../ActionsMenu/ServerSideClientPromptDialog/serverSideClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/vite/src/AppSettings/ActionsMenu/ServerSideClientPromptDialog/serverSideClient.ts b/examples/vite/src/AppSettings/ActionsMenu/ServerSideClientPromptDialog/serverSideClient.ts index da480dd06c..fe3960e733 100644 --- a/examples/vite/src/AppSettings/ActionsMenu/ServerSideClientPromptDialog/serverSideClient.ts +++ b/examples/vite/src/AppSettings/ActionsMenu/ServerSideClientPromptDialog/serverSideClient.ts @@ -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; client.tokenManager.token = token; client.tokenManager.type = 'static';