diff --git a/ai-docs/ai-migration-v9-to-v10.md b/ai-docs/ai-migration-v9-to-v10.md index 8996ad8aad..3336ec1909 100644 --- a/ai-docs/ai-migration-v9-to-v10.md +++ b/ai-docs/ai-migration-v9-to-v10.md @@ -1068,8 +1068,10 @@ params object rather than the prop's positional arguments: - v9 prop: `doUpdateMessageRequest(channelId, localMessage, options)` - v10 handler: `updateMessageRequest({ localMessage, options })` → `{ message }` -If you need the old `{ id, message }` request shape inside your handler, derive it with -`localMessageToNewMessagePayload(localMessage)` — that is what the SDK's adapter used to do. +If your handler calls the client itself, derive the request with +`localMessageToNewMessagePayload(localMessage)` — that is what the SDK's adapter used to do — and +pass the message id as its own argument: +`client.updateMessage({ id: localMessage.id }, { message: localMessageToNewMessagePayload(localMessage), ...options })`. `doSendMessageRequest` is removed too — see §13.1 for its `sendMessageRequest` replacement. @@ -1142,8 +1144,20 @@ Highlights that hit integrator code: `Role`→`RoleName`, `Logger`→`Sink`, `*SortBase`→`*Sort`, `TranslationLanguages`→`TranslationLanguage`. - **`Event` is a discriminated union** — narrow with `EventPayload<'the.type'>`, or drop the `: Event` annotation so `client.on('x', cb)` narrows automatically. -- **Method signatures collapsed to single objects** — `channel.sendReaction({ id, reaction, ... })`, - `deleteReaction({ id, type })`, `sendMessage({ message, ... })`, `queryChannels(request)`; +- **Method signatures collapsed to request objects, with path parameters split out** — a method whose + endpoint has URL path parameters takes them as a separate first argument, + `method(pathParams, request?)`: `channel.sendReaction({ id }, { reaction, ... })`, + `client.deleteMessage({ id }, { hard })`, `client.queryReactions({ id }, { filter, sort, limit })`; + one whose arguments are all path parameters takes just those (`deleteReaction({ id, type })`), + but keeps an unused request slot so request options stay third — pass `undefined` there: + `channel.deleteReaction({ id, type }, undefined, { signal })`, + `client.getMessage({ id }, undefined, { signal })`. Methods that take no arguments of their own + (`client.getAppSettings`, `channel.pin` / `unpin` / `archive` / `unarchive`, + `channel.disableSlowMode`, `channel.stopAIResponse`) likewise take request options second: + `channel.pin(undefined, { signal })`. Calls that pass no request options are unchanged. + Methods without path parameters take a single request object (`sendMessage({ message, ... })`, + `queryChannels(request)`). A body field left in the path-params argument through a spread is not a + compile error — the client throws at runtime instead; `client.uploadImage({ file: { uri, name, type } })` for RN image upload — the `file` field takes a browser `File`/`Blob` or an RN `{ uri, name, type }` descriptor, so the MIME type still has to be explicit, it just lives on the descriptor now; the same shape applies to `client.uploadFile` diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 95fd1197e7..7d4e1edbb1 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -51,7 +51,7 @@ "react-native-teleport": "^1.1.12", "react-native-web": "^0.21.2", "react-native-worklets": "0.11.1", - "stream-chat": "^10.0.0-rc.17", + "stream-chat": "^10.0.0-rc.18", "stream-chat-expo": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 57f72b897a..5b0a22ee2e 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -65,7 +65,7 @@ "react-native-teleport": "^1.1.12", "react-native-video": "^6.19.2", "react-native-worklets": "^0.12.1", - "stream-chat": "^10.0.0-rc.17", + "stream-chat": "^10.0.0-rc.18", "stream-chat-react-native": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/SampleApp/src/components/Reminders/ReminderItem.tsx b/examples/SampleApp/src/components/Reminders/ReminderItem.tsx index ce9d99c6ef..81e4ad3230 100644 --- a/examples/SampleApp/src/components/Reminders/ReminderItem.tsx +++ b/examples/SampleApp/src/components/Reminders/ReminderItem.tsx @@ -68,10 +68,10 @@ export const ReminderItem = ( const buttons: AlertButton[] = client.reminders.scheduledOffsetsMs.map((offsetMs) => ({ text: t('duration.remindMe', { milliseconds: offsetMs }), onPress: async () => { - await client.reminders.upsertReminder({ - message_id: item.message_id, - remind_at: new Date(Date.now() + offsetMs), - }); + await client.reminders.upsertReminder( + { message_id: item.message_id }, + { remind_at: new Date(Date.now() + offsetMs) }, + ); }, style: 'default', })); @@ -79,10 +79,10 @@ export const ReminderItem = ( buttons.push({ text: 'Clear Due Date', onPress: async () => { - await client.reminders.upsertReminder({ - message_id: item.message_id, - remind_at: undefined, - }); + await client.reminders.upsertReminder( + { message_id: item.message_id }, + { remind_at: undefined }, + ); }, style: 'default', }); diff --git a/examples/SampleApp/src/utils/DraftsManager.ts b/examples/SampleApp/src/utils/DraftsManager.ts index fee1eb84c0..dc9d4fda6b 100644 --- a/examples/SampleApp/src/utils/DraftsManager.ts +++ b/examples/SampleApp/src/utils/DraftsManager.ts @@ -1,17 +1,8 @@ import { StateStore } from '@stream-io/state-store'; -import { DraftResponse, QueryDraftsRequest, SortParamRequest, StreamChat } from 'stream-chat'; +import { DraftResponse, QueryDraftsRequest, StreamChat } from 'stream-chat'; import { WithSubscriptions } from './WithSubscription'; -export type QueryDraftOptions = { - limit?: number; - next?: string; - prev?: string; - filter?: QueryDraftsRequest['filter']; - sort?: SortParamRequest[]; - user_id?: string; -}; - const MAX_QUERY_DRAFTS_LIMIT = 25; export const DRAFT_MANAGER_INITIAL_STATE = { @@ -237,7 +228,7 @@ export class DraftsManager extends WithSubscriptions { } }; - public queryDrafts = async (options: QueryDraftOptions = {}) => { + public queryDrafts = async (options: QueryDraftsRequest = {}) => { const response = await this.client.queryDrafts({ limit: MAX_QUERY_DRAFTS_LIMIT, ...options, @@ -245,7 +236,7 @@ export class DraftsManager extends WithSubscriptions { return response; }; - public loadNextPage = async (options: QueryDraftOptions = {}) => { + public loadNextPage = async (options: QueryDraftsRequest = {}) => { const { pagination } = this.state.getLatestValue(); if (pagination.isLoadingNext || !pagination.nextCursor) { diff --git a/examples/SampleApp/src/utils/messageActions.tsx b/examples/SampleApp/src/utils/messageActions.tsx index f9b96a61df..3bf84043b1 100644 --- a/examples/SampleApp/src/utils/messageActions.tsx +++ b/examples/SampleApp/src/utils/messageActions.tsx @@ -36,7 +36,7 @@ export function channelMessageActions({ // if (reminder) { // await chatClient.reminders.deleteReminder(reminder.id); // } else { - // await chatClient.reminders.createReminder({ messageId: params.message.id }); + // await chatClient.reminders.createReminder({ message_id: params.message.id }); // } // dismissOverlay(); // } catch (error) { @@ -75,10 +75,10 @@ export function channelMessageActions({ // text: t('duration.remindMe', { milliseconds: offsetMs }), // onPress: () => { // chatClient.reminders - // .upsertReminder({ - // messageId: params.message.id, - // remind_at: new Date(Date.now() + offsetMs), - // }) + // .upsertReminder( + // { message_id: params.message.id }, + // { remind_at: new Date(Date.now() + offsetMs) }, + // ) // .catch((_error) => { // console.error('Error creating reminder:', _error); // }); diff --git a/package/package.json b/package/package.json index 34de6a9e73..f9fd2f3a8d 100644 --- a/package/package.json +++ b/package/package.json @@ -80,7 +80,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^2.0.0", - "stream-chat": "^10.0.0-rc.17", + "stream-chat": "^10.0.0-rc.18", "use-sync-external-store": "^1.7.0" }, "peerDependencies": { diff --git a/package/src/__tests__/offline-support/optimistic-update.tsx b/package/src/__tests__/offline-support/optimistic-update.tsx index a001ff643c..99d1660d68 100644 --- a/package/src/__tests__/offline-support/optimistic-update.tsx +++ b/package/src/__tests__/offline-support/optimistic-update.tsx @@ -345,7 +345,8 @@ export const OptimisticUpdates = () => { const pendingTaskType = pendingTasksRows?.[0]?.type; const pendingTaskPayload = JSON.parse((pendingTasksRows?.[0]?.payload as string) || '{}'); expect(pendingTaskType).toBe('delete-message'); - expect(pendingTaskPayload[0].id).toBe(message.id); + // The payload is `client.deleteMessage`'s argument list: the id alone is the first argument. + expect(pendingTaskPayload[0]).toEqual({ id: message.id }); }); }); @@ -406,7 +407,9 @@ export const OptimisticUpdates = () => { const pendingTaskType = pendingTasksRows?.[0]?.type; const pendingTaskPayload = JSON.parse((pendingTasksRows?.[0]?.payload as string) || '{}'); expect(pendingTaskType).toBe('send-reaction'); - expect(pendingTaskPayload[0].id).toBe(targetMessage.id); + // `[{ id }, { reaction, ... }]` - the reaction travels in the request, not with the id. + expect(pendingTaskPayload[0]).toEqual({ id: targetMessage.id }); + expect(pendingTaskPayload[1].reaction.type).toBe(reaction.type); }); }); @@ -589,11 +592,12 @@ export const OptimisticUpdates = () => { localMessage: LocalMessage; options?: unknown; }) => { - // The LLC hands over a `localMessage`; the prop received the `updateMessage` request - // shape `{ id, message }`. Rebuilt so the queued pending-task payload is unchanged. - const updatedMessage = { - id: localMessage.id, + // The LLC hands over a `localMessage`; the queued pending-task payload is the + // `client.updateMessage` argument list, `[{ id }, { message, ...options }]`. + const pathParams = { id: localMessage.id }; + const request = { message: localMessageToNewMessagePayload(localMessage), + ...(options as object | undefined), }; const editedMessage = { ...message, @@ -605,7 +609,7 @@ export const OptimisticUpdates = () => { channelId: channel.id, channelType: channel.type, messageId: message.id, - payload: [updatedMessage, options], + payload: [pathParams, request], type: 'update-message', }); // A complete offline update handler persists the optimistic edit to the DB (so it @@ -623,12 +627,7 @@ export const OptimisticUpdates = () => { render( - + { await flushMountEffects(); @@ -1228,6 +1227,14 @@ export const OptimisticUpdates = () => { expect(deleteMessageSpy).toHaveBeenCalled(); expect(sendReactionSpy).toHaveBeenCalled(); }); + // Replayed from the SQLite row: the split argument list survives the JSON round trip, with + // the (empty, soft-delete) options in the request slot rather than merged into the id. + expect(deleteMessageSpy.mock.calls[0][0]).toEqual({ id: message.id }); + expect(deleteMessageSpy.mock.calls[0][1]).toEqual({}); + expect(sendReactionSpy.mock.calls[0][0]).toEqual({ id: message.id }); + expect(sendReactionSpy.mock.calls[0][1]).toEqual( + expect.objectContaining({ reaction: expect.objectContaining({ type: reaction.type }) }), + ); }); // This is a separate test so CallbackEffectWithContext does not need to be modified in order diff --git a/package/src/components/MessageMenu/hooks/useFetchReactions.ts b/package/src/components/MessageMenu/hooks/useFetchReactions.ts index 3af0d8c0b7..b60fe60035 100644 --- a/package/src/components/MessageMenu/hooks/useFetchReactions.ts +++ b/package/src/components/MessageMenu/hooks/useFetchReactions.ts @@ -111,13 +111,10 @@ export const useFetchReactions = ({ return; } try { - const response = await client.queryReactions({ - filter: reactionType ? { type: reactionType } : undefined, - id: messageId, - limit, - next, - sort, - }); + const response = await client.queryReactions( + { id: messageId }, + { filter: reactionType ? { type: reactionType } : undefined, limit, next, sort }, + ); if (response) { setNext(response.next); diff --git a/yarn.lock b/yarn.lock index 2309364b01..6d4b05ac87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6610,7 +6610,7 @@ __metadata: react-native-teleport: "npm:^1.1.12" react-native-web: "npm:^0.21.2" react-native-worklets: "npm:0.11.1" - stream-chat: "npm:^10.0.0-rc.17" + stream-chat: "npm:^10.0.0-rc.18" stream-chat-expo: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:6.0.3" @@ -17608,7 +17608,7 @@ __metadata: react-native-teleport: "npm:^1.1.12" react-native-video: "npm:^6.19.2" react-native-worklets: "npm:^0.12.1" - stream-chat: "npm:^10.0.0-rc.17" + stream-chat: "npm:^10.0.0-rc.18" stream-chat-react-native: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:6.0.3" @@ -18352,7 +18352,7 @@ __metadata: react-native-worklets: "npm:^0.12.1" react-test-renderer: "npm:19.2.3" rimraf: "npm:^6.0.1" - stream-chat: "npm:^10.0.0-rc.17" + stream-chat: "npm:^10.0.0-rc.18" typescript: "npm:6.0.3" use-sync-external-store: "npm:^1.7.0" uuid: "npm:^11.1.0" @@ -18426,9 +18426,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:^10.0.0-rc.17": - version: 10.0.0-rc.17 - resolution: "stream-chat@npm:10.0.0-rc.17" +"stream-chat@npm:^10.0.0-rc.18": + version: 10.0.0-rc.18 + resolution: "stream-chat@npm:10.0.0-rc.18" dependencies: "@stream-io/logger": "npm:^2.0.0" "@stream-io/state-store": "npm:^1.1.6" @@ -18439,7 +18439,7 @@ __metadata: built: true husky: built: true - checksum: 10c0/f4721fb4db398ff465bf58a918aa4cd4a46085cc57ce305019c65fdda5b710dfaa14af9870bf060894ab35053fe4697e6ca29276323a625bf8b445c9ca274b77 + checksum: 10c0/0b4629e8c73f6967994d93a3c2fbc1f809aecfca3d26bc4ea4634c2f78780568f055d4af7a0e3c9c7a710def5aeba04741c4a5f1bf60afa522b6a4b133facffa languageName: node linkType: hard