From 90e28d57e53cc175ea5dc6dd7cc63bbdc7926b08 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 2 Oct 2026 10:24:03 -0500 Subject: [PATCH 1/3] feat: split path params into separate request objects --- .../screenshot-attachment-actions.ts | 4 +- .../vite/docs-playwright/screenshot-misc.ts | 12 ++--- .../docs-playwright/screenshot-reactions.ts | 16 +++---- .../screenshot-system-message.ts | 4 +- .../docs-playwright/screenshot-variants.ts | 45 ++++++++++--------- .../screenshot-voice-recording.ts | 44 +++++++++--------- .../Channel/__tests__/Channel.test.tsx | 26 +++++------ .../Message/__tests__/Message.test.tsx | 14 +++--- .../hooks/__tests__/useDeleteHandler.test.tsx | 2 +- .../__tests__/useReactionHandler.test.tsx | 40 ++++++++++------- .../__tests__/useReactionsFetcher.test.tsx | 12 ++--- .../Message/hooks/useDeleteHandler.ts | 5 +-- .../Message/hooks/useReactionHandler.ts | 16 ++++--- .../Message/hooks/useReactionsFetcher.ts | 11 ++--- .../MessageActions/RemindMeSubmenu.tsx | 8 ++-- .../PollActions/SuggestPollOptionPrompt.tsx | 5 +-- .../__tests__/SuggestPollOptionForm.test.tsx | 10 ++--- 17 files changed, 140 insertions(+), 134 deletions(-) diff --git a/examples/vite/docs-playwright/screenshot-attachment-actions.ts b/examples/vite/docs-playwright/screenshot-attachment-actions.ts index 223210918e..5036854fbb 100644 --- a/examples/vite/docs-playwright/screenshot-attachment-actions.ts +++ b/examples/vite/docs-playwright/screenshot-attachment-actions.ts @@ -73,7 +73,7 @@ async function seedChannel(contextA: BrowserContext) { // Send a message with an image attachment that has custom actions await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [{ type: 'image', @@ -85,7 +85,7 @@ async function seedChannel(contextA: BrowserContext) { { name: 'vote', value: 'Loathe' }, ], }], - }); + } }); })()`); console.log(' ✅ Channel seeded'); diff --git a/examples/vite/docs-playwright/screenshot-misc.ts b/examples/vite/docs-playwright/screenshot-misc.ts index d64eb75f01..b2541dbd72 100644 --- a/examples/vite/docs-playwright/screenshot-misc.ts +++ b/examples/vite/docs-playwright/screenshot-misc.ts @@ -91,7 +91,7 @@ async function captureBaseImageFallbacks(browser: any) { // Send message with a broken image URL await page.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: 'Some text', attachments: [{ type: 'image', @@ -99,13 +99,13 @@ async function captureBaseImageFallbacks(browser: any) { thumb_url: 'https://invalid-url-that-will-fail.example/broken-thumb.jpg', title: 'broken-image.jpg', }], - }); + } }); })()`); // Send message with two images (one broken) for gallery view await page.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [ { @@ -119,7 +119,7 @@ async function captureBaseImageFallbacks(browser: any) { thumb_url: 'https://invalid-url-that-will-fail.example/broken2-thumb.jpg', }, ], - }); + } }); })()`); await page.close(); @@ -174,7 +174,7 @@ async function captureConnectionStatus(browser: any) { // Send a message for context await page.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ text: 'Hey, how are you?' }); + await ch.sendMessage({ message: { text: 'Hey, how are you?' } }); })()`); await page.close(); @@ -215,7 +215,7 @@ async function captureCustomNotification(browser: any) { const page = await createChannel(ctx, channelId, USER_A, USER_A); await page.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ text: "Hi! How's it going?!" }); + await ch.sendMessage({ message: { text: "Hi! How's it going?!" } }); })()`); await page.close(); diff --git a/examples/vite/docs-playwright/screenshot-reactions.ts b/examples/vite/docs-playwright/screenshot-reactions.ts index de3119ac29..a789f4a0b1 100644 --- a/examples/vite/docs-playwright/screenshot-reactions.ts +++ b/examples/vite/docs-playwright/screenshot-reactions.ts @@ -75,18 +75,18 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { // Send messages const r1: any = await pageA.evaluate(`(async () => { var ch = window.channel; - var m = await ch.sendMessage({ text: 'What do you think about the new design?' }); + var m = await ch.sendMessage({ message: { text: 'What do you think about the new design?' } }); return { id: m.message.id }; })()`); await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ text: 'I love it! The color scheme is much better.' }); + await ch.sendMessage({ message: { text: 'I love it! The color scheme is much better.' } }); })()`); const r3: any = await pageA.evaluate(`(async () => { var ch = window.channel; - var m = await ch.sendMessage({ text: 'Great to hear! Should we ship it this week?' }); + var m = await ch.sendMessage({ message: { text: 'Great to hear! Should we ship it this week?' } }); return { id: m.message.id }; })()`); @@ -94,27 +94,27 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { // Marco reacts to his own first message with 'like' await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(r1.id)}, { type: 'like' }); + await ch.sendReaction({ id: ${JSON.stringify(r1.id)} }, { reaction: { type: 'like' } }); })()`); // John reacts to first message with 'love' await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(r1.id)}, { type: 'love' }); + await ch.sendReaction({ id: ${JSON.stringify(r1.id)} }, { reaction: { type: 'love' } }); })()`); // John reacts to third message with 'like' await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(r3.id)}, { type: 'like' }); + await ch.sendReaction({ id: ${JSON.stringify(r3.id)} }, { reaction: { type: 'like' } }); })()`); // Add upvote/downvote reactions (for custom options variant) await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(r3.id)}, { type: 'arrow_up' }); + await ch.sendReaction({ id: ${JSON.stringify(r3.id)} }, { reaction: { type: 'arrow_up' } }); })()`); await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(r1.id)}, { type: 'arrow_up' }); + await ch.sendReaction({ id: ${JSON.stringify(r1.id)} }, { reaction: { type: 'arrow_up' } }); })()`); console.log(' ✅ Channel seeded'); diff --git a/examples/vite/docs-playwright/screenshot-system-message.ts b/examples/vite/docs-playwright/screenshot-system-message.ts index bf62dae5e0..25f28ad7ee 100644 --- a/examples/vite/docs-playwright/screenshot-system-message.ts +++ b/examples/vite/docs-playwright/screenshot-system-message.ts @@ -70,12 +70,12 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { // Send a few normal messages for context await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ text: 'Hey John, quick question about the project' }); + await ch.sendMessage({ message: { text: 'Hey John, quick question about the project' } }); })()`); await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ text: 'Sure, what is it?' }); + await ch.sendMessage({ message: { text: 'Sure, what is it?' } }); })()`); // Inject a system message into channel state via truncate with a system message diff --git a/examples/vite/docs-playwright/screenshot-variants.ts b/examples/vite/docs-playwright/screenshot-variants.ts index cfd6098fa7..4a5dac19b5 100644 --- a/examples/vite/docs-playwright/screenshot-variants.ts +++ b/examples/vite/docs-playwright/screenshot-variants.ts @@ -77,7 +77,7 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { var client = window.client; // 1. Marco: opening message - var m1 = await ch.sendMessage({ text: 'Hey, John, how are you doing?' }); + var m1 = await ch.sendMessage({ message: { text: 'Hey, John, how are you doing?' } }); return { m1Id: m1.message.id }; })()`); @@ -88,7 +88,7 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { var client = window.client; // 2. John: reply - var m2 = await ch.sendMessage({ text: 'Hey, hey! Doing well \\u2013 how about you?' }); + var m2 = await ch.sendMessage({ message: { text: 'Hey, hey! Doing well \\u2013 how about you?' } }); return { m2Id: m2.message.id }; })()`, @@ -98,9 +98,9 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { var ch = window.channel; // 3. Marco: long reply - var m3 = await ch.sendMessage({ + var m3 = await ch.sendMessage({ message: { text: "Ah, thanks for asking! I'm doing quite well, all things considered. Every day brings its own set of challenges and opportunities, but overall, I'm feeling positive and motivated. I've been keeping busy learning new things." - }); + } }); return { m3Id: m3.message.id }; })()`); @@ -111,16 +111,17 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { var ch = window.channel; var client = window.client; - var m4 = await ch.sendMessage({ + var m4 = await ch.sendMessage({ message: { text: '@Marco hey! How are you?', mentioned_users: [${JSON.stringify(USER_A)}] - }); + } }); // Edit to trigger "Edited" indicator - await client.updateMessage({ - id: m4.message.id, - text: '@Marco hey! How are you?', - mentioned_users: [${JSON.stringify(USER_A)}] + await client.updateMessage({ id: m4.message.id }, { + message: { + text: '@Marco hey! How are you?', + mentioned_users: [${JSON.stringify(USER_A)}] + } }); return { m4Id: m4.message.id }; @@ -130,52 +131,52 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { // 5. John: link message const result5: any = await pageB.evaluate(`(async () => { var ch = window.channel; - var m5 = await ch.sendMessage({ text: 'Check out this link:\\nhttps://getstream.io/' }); + var m5 = await ch.sendMessage({ message: { text: 'Check out this link:\\nhttps://getstream.io/' } }); return { m5Id: m5.message.id }; })()`); // 6. Marco: markdown message const result6: any = await pageA.evaluate(`(async () => { var ch = window.channel; - var m6 = await ch.sendMessage({ + var m6 = await ch.sendMessage({ message: { text: "Here's some ~~styled text~~ **Markdown**, *too*!" - }); + } }); return { m6Id: m6.message.id }; })()`); // 7. Marco: consecutive messages for grouping demo (V5) await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ text: 'This message right here...' }); - await ch.sendMessage({ text: '...will be grouped with this one.' }); - await ch.sendMessage({ text: 'And this one as well!' }); + await ch.sendMessage({ message: { text: 'This message right here...' } }); + await ch.sendMessage({ message: { text: '...will be grouped with this one.' } }); + await ch.sendMessage({ message: { text: 'And this one as well!' } }); })()`); // 8. Add a thread reply on the markdown message (for reply_count in V7/V8) await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: 'Nice formatting!', parent_id: ${JSON.stringify(result6.m6Id)} - }); + } }); })()`); // 9. Add reactions on the mention message (for V8) await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(result4.m4Id)}, { type: '+1' }); + await ch.sendReaction({ id: ${JSON.stringify(result4.m4Id)} }, { reaction: { type: '+1' } }); })()`); await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendReaction(${JSON.stringify(result4.m4Id)}, { type: '-1' }); + await ch.sendReaction({ id: ${JSON.stringify(result4.m4Id)} }, { reaction: { type: '-1' } }); })()`); // 10. Marco sends one more message, then soft-deletes it (for V7/V8) const result10: any = await pageA.evaluate(`(async () => { var ch = window.channel; var client = window.client; - var m = await ch.sendMessage({ text: 'Actually, never mind about that last point.' }); - await client.deleteMessage(m.message.id); + var m = await ch.sendMessage({ message: { text: 'Actually, never mind about that last point.' } }); + await client.deleteMessage({ id: m.message.id }); return { deletedId: m.message.id }; })()`); diff --git a/examples/vite/docs-playwright/screenshot-voice-recording.ts b/examples/vite/docs-playwright/screenshot-voice-recording.ts index be35fa5366..0557d31886 100644 --- a/examples/vite/docs-playwright/screenshot-voice-recording.ts +++ b/examples/vite/docs-playwright/screenshot-voice-recording.ts @@ -77,7 +77,7 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { // 1. Normal voice recording (full data) — for voice-recording-player.png await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -88,14 +88,14 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { duration: 5.2, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); })()`); // 2. Voice recording for quoted reply — for voice-recording-quoted.png // First send a message with voice recording from user B const r2: any = await pageB.evaluate(`(async () => { var ch = window.channel; - var m = await ch.sendMessage({ + var m = await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -106,23 +106,23 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { duration: 44, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); return { id: m.message.id }; })()`); // Then quote-reply it await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: 'Nice voice message!', quoted_message_id: ${JSON.stringify(r2.id)}, - }); + } }); })()`); // 3. Voice recording with full waveform + file icon (for navigation/stopped screenshots) await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -133,13 +133,13 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { duration: 7.5, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); })()`); // 4. Voice recording WITHOUT duration (file size fallback) await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -149,13 +149,13 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { file_size: 21342, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); })()`); // 5. Voice recording WITHOUT title (fallback title) await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -165,13 +165,13 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { duration: 7.2, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); })()`); // 6. Voice recording for quoted reply WITHOUT title const r6: any = await pageA.evaluate(`(async () => { var ch = window.channel; - var m = await ch.sendMessage({ + var m = await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -181,23 +181,23 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { duration: 44, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); return { id: m.message.id }; })()`); // Quote the no-title message await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: 'Replying to voice message', quoted_message_id: ${JSON.stringify(r6.id)}, - }); + } }); })()`); // 7. Voice recording for quoted reply WITHOUT duration (file size fallback) const r7: any = await pageB.evaluate(`(async () => { var ch = window.channel; - var m = await ch.sendMessage({ + var m = await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -207,22 +207,22 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { file_size: 68421, waveform_data: ${WAVEFORM_DATA}, }], - }); + } }); return { id: m.message.id }; })()`); await pageA.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: 'Got it!', quoted_message_id: ${JSON.stringify(r7.id)}, - }); + } }); })()`); // 8. Voice recording WITHOUT waveform_data (empty waveform) await pageB.evaluate(`(async () => { var ch = window.channel; - await ch.sendMessage({ + await ch.sendMessage({ message: { text: '', attachments: [{ type: 'voiceRecording', @@ -233,7 +233,7 @@ async function seedChannel(contextA: BrowserContext, contextB: BrowserContext) { duration: 7.0, waveform_data: [], }], - }); + } }); })()`); console.log(' ✅ Channel seeded'); diff --git a/src/components/Channel/__tests__/Channel.test.tsx b/src/components/Channel/__tests__/Channel.test.tsx index 7e2d99c250..ae485e627b 100644 --- a/src/components/Channel/__tests__/Channel.test.tsx +++ b/src/components/Channel/__tests__/Channel.test.tsx @@ -487,7 +487,7 @@ describe('Channel', () => { it('should call the default client.deleteMessage() function', async () => { const { channel, chatClient } = await setup(); const message = generateMessage(); - const deleteMessageOptions = { deleteForMe: true, hard: false }; + const deleteMessageOptions = { delete_for_me: true, hard: false }; const clientDeleteMessageSpy = vi .spyOn(chatClient, 'deleteMessage') .mockResolvedValue(fromPartial({ message: toMessageResponse(message) })); @@ -501,18 +501,18 @@ describe('Channel', () => { .catch(() => {}); }); await waitFor(() => - // v10: single request object - `client.deleteMessage({ id, ...options })`. - expect(clientDeleteMessageSpy).toHaveBeenCalledWith({ - id: message.id, - ...deleteMessageOptions, - }), + // The message id is the path-params argument; the options are the request. + expect(clientDeleteMessageSpy).toHaveBeenCalledWith( + { id: message.id }, + deleteMessageOptions, + ), ); }); it('calls a registered deleteMessageRequest instead of client.deleteMessage()', async () => { const { channel, chatClient } = await setup(); const message = generateMessage(); - const deleteMessageOptions = { deleteForMe: true, hard: false }; + const deleteMessageOptions = { delete_for_me: true, hard: false }; const deleteMessageRequest = vi.fn(() => Promise.resolve({ message: toMessageResponse(message) }), ); @@ -557,12 +557,12 @@ describe('Channel', () => { .catch(() => {}); }); await waitFor(() => - // v10: single request object - `client.updateMessage({ id, message })`, where `message` is - // the LocalMessage projected onto the API payload shape. - expect(clientUpdateMessageSpy).toHaveBeenCalledWith({ - id: updatedMessage.id, - message: localMessageToNewMessagePayload(fromPartial(updatedMessage)), - }), + // `client.updateMessage({ id }, { message })`, where `message` is the LocalMessage + // projected onto the API payload shape. + expect(clientUpdateMessageSpy).toHaveBeenCalledWith( + { id: updatedMessage.id }, + { message: localMessageToNewMessagePayload(fromPartial(updatedMessage)) }, + ), ); }); diff --git a/src/components/Message/__tests__/Message.test.tsx b/src/components/Message/__tests__/Message.test.tsx index 495d0f1013..d789ddbfad 100644 --- a/src/components/Message/__tests__/Message.test.tsx +++ b/src/components/Message/__tests__/Message.test.tsx @@ -261,13 +261,15 @@ describe(' component', () => { }); await context.handleReaction(reaction.type); - expect(sendReaction).toHaveBeenCalledWith({ - id: message.id, - reaction: { - emoji_code: '❤️', - type: reaction.type, + expect(sendReaction).toHaveBeenCalledWith( + { id: message.id }, + { + reaction: { + emoji_code: '❤️', + type: reaction.type, + }, }, - }); + ); }); // MERGE-RECONCILE (test migration): the reaction handler no longer gates on the diff --git a/src/components/Message/hooks/__tests__/useDeleteHandler.test.tsx b/src/components/Message/hooks/__tests__/useDeleteHandler.test.tsx index f4c15a4d85..08c6773ce7 100644 --- a/src/components/Message/hooks/__tests__/useDeleteHandler.test.tsx +++ b/src/components/Message/hooks/__tests__/useDeleteHandler.test.tsx @@ -84,7 +84,7 @@ describe('useDeleteHandler custom hook', () => { .spyOn(channel, 'deleteMessageWithLocalUpdate') .mockResolvedValue(undefined); const message = generateMessage(); - const deleteMessageOptions = { deleteForMe: true, hard: false }; + const deleteMessageOptions = { delete_for_me: true, hard: false }; const handleDelete = await renderUseDeleteHandler(message); await act(async () => { await handleDelete(deleteMessageOptions); diff --git a/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx b/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx index bd8507544a..2587a04c91 100644 --- a/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx +++ b/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx @@ -114,13 +114,15 @@ describe('useReactionHandler custom hook', () => { const message = generateMessage({ own_reactions: [] }); const handleReaction = await renderUseReactionHandlerHook({ message }); await handleReaction('love'); - expect(sendReaction).toHaveBeenCalledWith({ - id: message.id, - reaction: { - emoji_code: '❤️', - type: 'love', + expect(sendReaction).toHaveBeenCalledWith( + { id: message.id }, + { + reaction: { + emoji_code: '❤️', + type: 'love', + }, }, - }); + ); }); it('should send reaction without emoji_code when the type has no unicode', async () => { @@ -130,12 +132,14 @@ describe('useReactionHandler custom hook', () => { const message = generateMessage({ own_reactions: [] }); const handleReaction = await renderUseReactionHandlerHook({ message }); await handleReaction('unsupported-reaction-type'); - expect(sendReaction).toHaveBeenCalledWith({ - id: message.id, - reaction: { - type: 'unsupported-reaction-type', + expect(sendReaction).toHaveBeenCalledWith( + { id: message.id }, + { + reaction: { + type: 'unsupported-reaction-type', + }, }, - }); + ); }); it('should derive emoji_code from custom reaction options provided via context', async () => { @@ -158,13 +162,15 @@ describe('useReactionHandler custom hook', () => { message, }); await handleReaction('rocket'); - expect(sendReaction).toHaveBeenCalledWith({ - id: message.id, - reaction: { - emoji_code: '🚀', - type: 'rocket', + expect(sendReaction).toHaveBeenCalledWith( + { id: message.id }, + { + reaction: { + emoji_code: '🚀', + type: 'rocket', + }, }, - }); + ); }); it('should stamp emoji_code on the optimistic reaction preview ingested into the paginator', async () => { diff --git a/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx b/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx index 4e82cee80b..3aedcc1964 100644 --- a/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx +++ b/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx @@ -52,12 +52,14 @@ describe('useReactionsFetcher', () => { }); expect(queryReactions).toHaveBeenCalledTimes(2); - // v10: `client.queryReactions` takes a single request object. - expect(queryReactions.mock.calls[0]?.[0]).toEqual( - expect.objectContaining({ id: message.id, limit: 25, next: undefined }), + // `client.queryReactions` takes the message id as its own path-params argument. + expect(queryReactions.mock.calls[0]?.[0]).toEqual({ id: message.id }); + expect(queryReactions.mock.calls[0]?.[1]).toEqual( + expect.objectContaining({ limit: 25, next: undefined }), ); - expect(queryReactions.mock.calls[1]?.[0]).toEqual( - expect.objectContaining({ id: message.id, limit: 25, next: 'page-2' }), + expect(queryReactions.mock.calls[1]?.[0]).toEqual({ id: message.id }); + expect(queryReactions.mock.calls[1]?.[1]).toEqual( + expect.objectContaining({ limit: 25, next: 'page-2' }), ); expect(reactions!).toHaveLength(2); }); diff --git a/src/components/Message/hooks/useDeleteHandler.ts b/src/components/Message/hooks/useDeleteHandler.ts index 861f2b57c8..bf0479116b 100644 --- a/src/components/Message/hooks/useDeleteHandler.ts +++ b/src/components/Message/hooks/useDeleteHandler.ts @@ -38,9 +38,8 @@ export const useDeleteHandler = ( options, }); } else { - const deletedMessage = ( - await client.deleteMessage({ id: message.id, ...options }) - ).message; + const deletedMessage = (await client.deleteMessage({ id: message.id }, options)) + .message; messagePaginator.ingestItem(formatMessage(deletedMessage)); } } catch (e) { diff --git a/src/components/Message/hooks/useReactionHandler.ts b/src/components/Message/hooks/useReactionHandler.ts index 5794ca5f7b..aad97586e0 100644 --- a/src/components/Message/hooks/useReactionHandler.ts +++ b/src/components/Message/hooks/useReactionHandler.ts @@ -114,13 +114,15 @@ export const useReactionHandler = (message?: LocalMessage) => { try { updateMessage(tempMessage); const messageResponse = add - ? await channel.sendReaction({ - id, - reaction: { - type, - ...(emojiCode && { emoji_code: emojiCode }), - } as ReactionRequest, - }) + ? await channel.sendReaction( + { id }, + { + reaction: { + type, + ...(emojiCode && { emoji_code: emojiCode }), + } as ReactionRequest, + }, + ) : await channel.deleteReaction({ id, type }); // seems useless as we're expecting WS event to come in and replace this anyway diff --git a/src/components/Message/hooks/useReactionsFetcher.ts b/src/components/Message/hooks/useReactionsFetcher.ts index 03c0c064b6..f1b9c066e7 100644 --- a/src/components/Message/hooks/useReactionsFetcher.ts +++ b/src/components/Message/hooks/useReactionsFetcher.ts @@ -30,13 +30,10 @@ async function fetchMessageReactions( let hasNext = true; while (hasNext && reactions.length < MAX_MESSAGE_REACTIONS_TO_FETCH) { - const response = await client.queryReactions({ - filter: reactionType ? { type: reactionType } : {}, - id: messageId, - limit, - next, - sort, - }); + const response = await client.queryReactions( + { id: messageId }, + { filter: reactionType ? { type: reactionType } : {}, limit, next, sort }, + ); reactions.push(...response.reactions); next = response.next; diff --git a/src/components/MessageActions/RemindMeSubmenu.tsx b/src/components/MessageActions/RemindMeSubmenu.tsx index f396008d67..2660b7a2e2 100644 --- a/src/components/MessageActions/RemindMeSubmenu.tsx +++ b/src/components/MessageActions/RemindMeSubmenu.tsx @@ -54,10 +54,10 @@ export const RemindMeSubmenu = () => { key={`reminder-offset-option--${offsetMs}`} onClick={async () => { try { - await client.reminders.upsertReminder({ - message_id: message.id, - remind_at: new Date(new Date().getTime() + offsetMs), - }); + await client.reminders.upsertReminder( + { message_id: message.id }, + { remind_at: new Date(new Date().getTime() + offsetMs) }, + ); addNotification({ context: { message, diff --git a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx index 3a6cc1453c..8581512d91 100644 --- a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx +++ b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx @@ -56,10 +56,7 @@ export const SuggestPollOptionPrompt = () => { const onSubmit = useCallback( async (formValue: { optionText: string }) => { - await client.createPollOption({ - poll_id: poll.id, - text: formValue.optionText, - }); + await client.createPollOption({ poll_id: poll.id }, { text: formValue.optionText }); close(); }, [client, poll, close], diff --git a/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx b/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx index 40fbd6b335..476219b7c3 100644 --- a/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx +++ b/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx @@ -63,11 +63,11 @@ describe('SuggestPollOptionPrompt', () => { }); await waitFor(() => { - // v10 takes a single request object: `createPollOption({ poll_id, text })`. - expect(createPollOptionSpy).toHaveBeenCalledWith({ - poll_id: poll.id, - text: newlyTypedValue, - }); + // The poll id is the path-params argument: `createPollOption({ poll_id }, { text })`. + expect(createPollOptionSpy).toHaveBeenCalledWith( + { poll_id: poll.id }, + { text: newlyTypedValue }, + ); }); }); }); From 829c521f1bc5f48bbeb9dad7e0a9902ea1978609 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Fri, 2 Oct 2026 14:12:09 -0500 Subject: [PATCH 2/3] fix: update docs from review --- specs/message-pagination/decisions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/message-pagination/decisions.md b/specs/message-pagination/decisions.md index 28300961e8..492a6e59f2 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 (`client.getReplies({ parent_id, ... })`); +- when present, query thread replies (`client.getReplies({ parent_id }, { ...pagination })`); - include `parent_id` in client-side filters only for thread mode. `Thread` now constructs `MessagePaginator` with `parentMessageId: thread.id`. @@ -775,7 +775,7 @@ This keeps initial-scroll ownership in the top-level list scroll logic and avoid - `jumpToReplyInChannelMessages` must use `channel.messagePaginator`. - For channel jumps while `activeView === 'threads'`, query the target channel before paginator jump. -- `jumpToReplyInThread` must resolve thread instance from `client.threads.threadsById[parentId]`; if absent, fetch via `client.getThread(parentId, { watch: true })`, then call `thread.messagePaginator.jumpToMessage(replyId)`. +- `jumpToReplyInThread` must resolve thread instance from `client.threads.get(parentId)`; if absent, fetch via `client.getThreadAndHydrate(parentId, { watch: true })`, then call `thread.messagePaginator.jumpToMessage(replyId)`. **Notes:** Add TODO in component to replace unconditional pre-query with `ChannelListOrchestrator` loaded-instance check before querying channel. From a84adff828564989e1fe254194b103c1e8fbb606 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Mon, 5 Oct 2026 08:29:19 -0500 Subject: [PATCH 3/3] feat: update 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 9b107013e3..71a9e26593 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.15", + "stream-chat": "10.0.0-rc.18", "stream-chat-react": "workspace:^" }, "devDependencies": { diff --git a/examples/vite/package.json b/examples/vite/package.json index 75cdaf4f95..0a62aea740 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -18,7 +18,7 @@ "modern-normalize": "^3.0.1", "react": "^19.2.6", "react-dom": "^19.2.6", - "stream-chat": "10.0.0-rc.15", + "stream-chat": "10.0.0-rc.18", "stream-chat-react": "workspace:^" }, "devDependencies": { diff --git a/package.json b/package.json index ffd1a693fa..c7335cdbd1 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,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.15" + "stream-chat": "^10.0.0-rc.18" }, "peerDependenciesMeta": { "@breezystack/lamejs": { @@ -195,7 +195,7 @@ "react-dom": "^19.2.6", "sass": "^1.100.0", "semantic-release": "^25.0.3", - "stream-chat": "10.0.0-rc.15", + "stream-chat": "10.0.0-rc.18", "typescript": "^6.0.3", "typescript-eslint": "^8.59.4", "vite": "^8.1.3", diff --git a/yarn.lock b/yarn.lock index 8c85afe9a7..1ba443ce49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1851,7 +1851,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.15" + stream-chat: "npm:10.0.0-rc.18" stream-chat-react: "workspace:^" typescript: "npm:^6.0.3" vite: "npm:^8.1.3" @@ -1879,7 +1879,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.15" + stream-chat: "npm:10.0.0-rc.18" stream-chat-react: "workspace:^" typescript: "npm:^6.0.3" vite: "npm:^8.1.3" @@ -9540,7 +9540,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.15" + stream-chat: "npm:10.0.0-rc.18" typescript: "npm:^6.0.3" typescript-eslint: "npm:^8.59.4" unified: "npm:^11.0.5" @@ -9557,7 +9557,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.15 + stream-chat: ^10.0.0-rc.18 dependenciesMeta: "@parcel/watcher": built: true @@ -9581,9 +9581,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:10.0.0-rc.15": - version: 10.0.0-rc.15 - resolution: "stream-chat@npm:10.0.0-rc.15" +"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" @@ -9594,7 +9594,7 @@ __metadata: built: true husky: built: true - checksum: 10c0/7a180896390c1f71dfa4296a6c1346170af2476e26a5d371d3bfc3af337095db7ec9e4396fe2f28bc9b1ad7e025bd77ba8d295e169c3420619d55a26c6bac764 + checksum: 10c0/0b4629e8c73f6967994d93a3c2fbc1f809aecfca3d26bc4ea4634c2f78780568f055d4af7a0e3c9c7a710def5aeba04741c4a5f1bf60afa522b6a4b133facffa languageName: node linkType: hard