Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,14 @@ export class Channel extends ChannelApi {
);
}

async sendReaction(...args: Parameters<ChatApi['sendReaction']>) {
return await this.getClient().sendReaction(...args);
}

async deleteReaction(...args: Parameters<ChatApi['deleteReaction']>) {
return await this.getClient().deleteReaction(...args);
}

/**
* Upload a file to this channel's file endpoint (multipart).
*
Expand Down
54 changes: 54 additions & 0 deletions test/unit/channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
15 changes: 10 additions & 5 deletions v9-to-v10-migration-guide-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, never>`.
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<string, never>`.

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' } });
Expand Down Expand Up @@ -848,14 +848,16 @@ channel.queryMembers(request?: { payload?: Partial<QueryMembersPayload> });

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<string, never>);
client.sendReaction(pathParams: { id: messageId }, request: { reaction, enforce_unique?, skip_push? });
client.deleteReaction(pathParams: { id: messageId, type: reactionType }, _request?: Record<string, never>);

Expand Down Expand Up @@ -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.
Expand Down
Loading