diff --git a/src/channel.ts b/src/channel.ts index 1494ddbe3..1652f3b57 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -578,6 +578,14 @@ export class Channel extends ChannelApi { ); } + async sendReaction(...args: Parameters) { + return await this.getClient().sendReaction(...args); + } + + async deleteReaction(...args: Parameters) { + return await this.getClient().deleteReaction(...args); + } + /** * Upload a file to this channel's file endpoint (multipart). * diff --git a/test/unit/channel.test.js b/test/unit/channel.test.js index 5b9cc9d91..c716c3c08 100644 --- a/test/unit/channel.test.js +++ b/test/unit/channel.test.js @@ -3837,6 +3837,38 @@ describe('send reaction flow', () => { expect(result.message).toMatchObject({ id: messageId }); }); }); + + describe('channel.sendReaction (a pass-through to client.sendReaction)', () => { + it('forwards every argument to client.sendReaction and returns its response', async () => { + const response = { message: { id: messageId } }; + const clientSendReaction = vi + .spyOn(client, 'sendReaction') + .mockResolvedValue(response); + const requestOptions = { signal: new AbortController().signal }; + + const result = await channel.sendReaction(pathParams, request, requestOptions); + + expect(clientSendReaction).toHaveBeenCalledTimes(1); + expect(clientSendReaction).toHaveBeenCalledWith( + pathParams, + request, + requestOptions, + ); + expect(result).toBe(response); + expect(queueTaskSpy).not.toHaveBeenCalled(); + }); + + it('does not need the channel to have an id, since a reaction is addressed by message', async () => { + const clientSendReaction = vi.spyOn(client, 'sendReaction').mockResolvedValue({}); + const distinct = client.channel('messaging', undefined, { + members: ['user-abc', 'other-user'], + }); + + await distinct.sendReaction(pathParams, request); + + expect(clientSendReaction).toHaveBeenCalledWith(pathParams, request); + }); + }); }); describe('delete reaction flow', () => { @@ -3954,6 +3986,28 @@ describe('delete reaction flow', () => { expect(result.message).toMatchObject({ id: messageId }); }); }); + + describe('channel.deleteReaction (a pass-through to client.deleteReaction)', () => { + it('forwards every argument to client.deleteReaction and returns its response', async () => { + const response = { message: { id: messageId } }; + const clientDeleteReaction = vi + .spyOn(client, 'deleteReaction') + .mockResolvedValue(response); + const requestOptions = { signal: new AbortController().signal }; + + const result = await channel.deleteReaction(request, undefined, requestOptions); + + expect(clientDeleteReaction).toHaveBeenCalledTimes(1); + expect(clientDeleteReaction).toHaveBeenCalledWith( + request, + undefined, + requestOptions, + ); + expect(result).toBe(response); + expect(queueTaskSpy).not.toHaveBeenCalled(); + expect(deleteReactionSpy).not.toHaveBeenCalled(); + }); + }); }); describe('message sending flow', () => { diff --git a/v9-to-v10-migration-guide-methods.md b/v9-to-v10-migration-guide-methods.md index 23b5bcded..bc21e5757 100644 --- a/v9-to-v10-migration-guide-methods.md +++ b/v9-to-v10-migration-guide-methods.md @@ -42,11 +42,11 @@ await client.deleteReaction({ id: messageId, type: 'love' }, undefined, { await channel.pin(undefined, { signal: controller.signal }); ``` -The slot keeps `requestOptions` in the same position when the endpoint later gains a query parameter. Without it, `getApp(requestOptions)` would become `getApp(request, requestOptions)`, and options passed by existing callers would quietly go out as query parameters. The generated methods affected today are `getApp`, `unreadCounts`, `listDevices`, `getBlockedUsers`, `getUserLiveLocations`, `getMessage`, `deleteReaction`, `deletePoll`, `getPoll`, `deletePollOption`, `getPollOption`, `deletePollVote` and `deleteReminder` on `client`, and `listQueues`, `getQueue`, `getAppeal` and `deleteActionConfig` on `client.moderation`. The wrappers around them take the same shape (`client.getAppSettings`, `channel.removeVote`). So do the hand-written `Channel` conveniences that send a fixed request: `archive`, `unarchive`, `pin`, `unpin`, `disableSlowMode`, `clearAIIndicator` and `stopAIResponse`. A call that omits `requestOptions` is unaffected (`client.getAppSettings()`, `channel.pin()`). A call that passes it in the first free position is a compile error, because no `StreamRequestOptions` value is assignable to `Record`. +The slot keeps `requestOptions` in the same position when the endpoint later gains a query parameter. Without it, `getApp(requestOptions)` would become `getApp(request, requestOptions)`, and options passed by existing callers would quietly go out as query parameters. The generated methods affected today are `getApp`, `unreadCounts`, `listDevices`, `getBlockedUsers`, `getUserLiveLocations`, `getMessage`, `deleteReaction`, `deletePoll`, `getPoll`, `deletePollOption`, `getPollOption`, `deletePollVote` and `deleteReminder` on `client`, and `listQueues`, `getQueue`, `getAppeal` and `deleteActionConfig` on `client.moderation`. The wrappers around them take the same shape (`client.getAppSettings`, `channel.deleteReaction`, `channel.removeVote`). So do the hand-written `Channel` conveniences that send a fixed request: `archive`, `unarchive`, `pin`, `unpin`, `disableSlowMode`, `clearAIIndicator` and `stopAIResponse`. A call that omits `requestOptions` is unaffected (`client.getAppSettings()`, `channel.pin()`). A call that passes it in the first free position is a compile error, because no `StreamRequestOptions` value is assignable to `Record`. This replaces v9's `client.createAbortControllerForNextRequest()` — see [below](#clientcreateabortcontrollerfornextrequest). The upload methods are **not** an exception: they take the same `requestOptions`, which carries `onUploadProgress` alongside `signal`. v9's `axiosRequestConfig` parameter is gone everywhere. -**Path parameters are their own argument.** A generated method whose endpoint has URL path parameters takes them as a separate first argument: `method(pathParams, request?, requestOptions?)`. `request` carries the query and body fields; a method whose arguments are all path parameters keeps an unused `_request` slot so `requestOptions` stays third ([see above](#global-renames-applied-everywhere)). The hand-written methods that wrap one of these take the same shape (`channel.vote`, `client.queryReactionsAndHydrate`, `client.queryPollAnswers`, `client.reminders.upsertReminder` / `createReminder` / `updateReminder`). +**Path parameters are their own argument.** A generated method whose endpoint has URL path parameters takes them as a separate first argument: `method(pathParams, request?, requestOptions?)`. `request` carries the query and body fields; a method whose arguments are all path parameters keeps an unused `_request` slot so `requestOptions` stays third ([see above](#global-renames-applied-everywhere)). The hand-written methods that wrap one of these take the same shape (`channel.sendReaction`, `channel.vote`, `client.queryReactionsAndHydrate`, `client.queryPollAnswers`, `client.reminders.upsertReminder` / `createReminder` / `updateReminder`). ```ts await client.sendReaction({ id: messageId }, { reaction: { type: 'love' } }); @@ -848,14 +848,16 @@ channel.queryMembers(request?: { payload?: Partial }); For rewriting the `sort` value, see `v9-to-v10-migration-guide-sort.md`. -#### `channel.sendReaction` / `channel.deleteReaction` — removed +#### `channel.sendReaction` / `channel.deleteReaction` ```ts // v9 channel.sendReaction(messageID, reaction: Reaction, options?); channel.deleteReaction(messageID, reactionType, user_id?); -// v10 — the HTTP request +// v10 — the HTTP request (the channel methods pass straight through to the client ones) +channel.sendReaction(pathParams: { id: messageId }, request: { reaction, enforce_unique?, skip_push? }); +channel.deleteReaction(pathParams: { id: messageId, type: reactionType }, _request?: Record); client.sendReaction(pathParams: { id: messageId }, request: { reaction, enforce_unique?, skip_push? }); client.deleteReaction(pathParams: { id: messageId, type: reactionType }, _request?: Record); @@ -1593,11 +1595,14 @@ Each message action now has exactly two entry points: the HTTP request alone, an | `updateMessageWithLocalUpdate(p)` | `messageOperations.update(p)` | | `deleteMessageWithLocalUpdate(p)` | `messageOperations.delete(p)` | | `addReactionWithLocalUpdate(p)` / `deleteReactionWithLocalUpdate(p)` | `messageOperations.addReaction(p)` / `deleteReaction(p)` | -| `channel.sendReaction(r)` / `channel.deleteReaction(r)` | `client.sendReaction(r)` / `client.deleteReaction(r)` | | `channel._sendMessage`, `client._updateMessage`, `client._deleteMessage`, `channel._sendReaction`, `channel._deleteReaction` | the HTTP method above | Parameters are unchanged. +`channel.sendReaction` / `channel.deleteReaction` were removed in this release too, and came back +after `10.0.0-rc.20` as pass-throughs to `client.sendReaction` / `client.deleteReaction`: the HTTP +request only, with no local state and no offline queueing. + **Behaviour change for direct callers:** `channel.sendMessage`, `client.updateMessage` and `client.deleteMessage` no longer queue for offline replay; they used to. Call the full operation where that matters. Drafts are unchanged — `channel.createDraft` / `deleteDraft` still queue.