From b155cfd526472514ebaec843f73cbcaba78b6437 Mon Sep 17 00:00:00 2001 From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Tue, 28 Dec 2021 21:17:50 -0600 Subject: [PATCH] component events --- library/core/component-event.ts | 8 + library/core/components/button.tsx | 5 +- library/core/components/select.tsx | 33 +- library/core/instance.ts | 7 + library/core/reacord-discord-js.ts | 298 ++++++++++-------- library/core/reacord-tester.ts | 66 +++- library/core/reacord.ts | 7 +- library/internal/interaction.ts | 41 +-- library/internal/limited-collection.ts | 24 ++ ...derer.ts => interaction-reply-renderer.ts} | 6 +- library/internal/renderers/renderer.ts | 18 +- library/main.ts | 2 + playground/fruit-select.tsx | 2 +- playground/main.tsx | 16 +- test/event-callbacks.test.tsx | 3 + test/select.test.tsx | 26 +- todo.md | 12 +- 17 files changed, 370 insertions(+), 204 deletions(-) create mode 100644 library/core/component-event.ts create mode 100644 library/core/instance.ts create mode 100644 library/internal/limited-collection.ts rename library/internal/renderers/{command-reply-renderer.ts => interaction-reply-renderer.ts} (76%) create mode 100644 test/event-callbacks.test.tsx diff --git a/library/core/component-event.ts b/library/core/component-event.ts new file mode 100644 index 0000000..a248715 --- /dev/null +++ b/library/core/component-event.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from "react" +import type { ReacordInstance } from "./instance" + +export type ComponentEvent = { + // todo: add more info, like user, channel, member, guild, etc. + reply(content?: ReactNode): ReacordInstance + ephemeralReply(content?: ReactNode): ReacordInstance +} diff --git a/library/core/components/button.tsx b/library/core/components/button.tsx index 4e87e3a..1fa8eeb 100644 --- a/library/core/components/button.tsx +++ b/library/core/components/button.tsx @@ -5,6 +5,7 @@ import type { ComponentInteraction } from "../../internal/interaction" import type { MessageOptions } from "../../internal/message" import { getNextActionRow } from "../../internal/message" import { Node } from "../../internal/node.js" +import type { ComponentEvent } from "../component-event" export type ButtonProps = { label?: string @@ -14,7 +15,7 @@ export type ButtonProps = { onClick: (event: ButtonClickEvent) => void } -export type ButtonClickEvent = {} +export type ButtonClickEvent = ComponentEvent export function Button(props: ButtonProps) { return ( @@ -41,7 +42,7 @@ class ButtonNode extends Node { interaction.type === "button" && interaction.customId === this.customId ) { - this.props.onClick(interaction) + this.props.onClick(interaction.event) return true } return false diff --git a/library/core/components/select.tsx b/library/core/components/select.tsx index 746d4a7..4224a58 100644 --- a/library/core/components/select.tsx +++ b/library/core/components/select.tsx @@ -6,6 +6,7 @@ import { ReacordElement } from "../../internal/element.js" import type { ComponentInteraction } from "../../internal/interaction" import type { ActionRow, MessageOptions } from "../../internal/message" import { Node } from "../../internal/node.js" +import type { ComponentEvent } from "../component-event" import { OptionNode } from "./option-node" export type SelectProps = { @@ -17,12 +18,12 @@ export type SelectProps = { minValues?: number maxValues?: number disabled?: boolean - onSelect?: (event: SelectEvent) => void - onSelectValue?: (value: string) => void - onSelectMultiple?: (values: string[]) => void + onChange?: (event: SelectChangeEvent) => void + onChangeValue?: (value: string, event: SelectChangeEvent) => void + onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void } -export type SelectEvent = { +export type SelectChangeEvent = ComponentEvent & { values: string[] } @@ -52,9 +53,9 @@ class SelectNode extends Node { minValues = 0, maxValues = 25, children, - onSelect, - onSelectValue, - onSelectMultiple, + onChange, + onChangeValue, + onChangeMultiple, ...props } = this.props @@ -72,18 +73,18 @@ class SelectNode extends Node { override handleComponentInteraction( interaction: ComponentInteraction, ): boolean { - if ( + const isSelectInteraction = interaction.type === "select" && interaction.customId === this.customId && !this.props.disabled - ) { - this.props.onSelect?.({ values: interaction.values }) - this.props.onSelectMultiple?.(interaction.values) - if (interaction.values[0]) { - this.props.onSelectValue?.(interaction.values[0]) - } - return true + + if (!isSelectInteraction) return false + + this.props.onChange?.(interaction.event) + this.props.onChangeMultiple?.(interaction.event.values, interaction.event) + if (interaction.event.values[0]) { + this.props.onChangeValue?.(interaction.event.values[0], interaction.event) } - return false + return true } } diff --git a/library/core/instance.ts b/library/core/instance.ts new file mode 100644 index 0000000..11baef5 --- /dev/null +++ b/library/core/instance.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from "react" + +export type ReacordInstance = { + render: (content: ReactNode) => void + deactivate: () => void + destroy: () => void +} diff --git a/library/core/reacord-discord-js.ts b/library/core/reacord-discord-js.ts index dd82e52..ff57b94 100644 --- a/library/core/reacord-discord-js.ts +++ b/library/core/reacord-discord-js.ts @@ -1,11 +1,15 @@ +/* eslint-disable class-methods-use-this */ import type * as Discord from "discord.js" +import type { ReactNode } from "react" +import type { Except } from "type-fest" import { raise } from "../../helpers/raise" import { toUpper } from "../../helpers/to-upper" import type { ComponentInteraction } from "../internal/interaction" import type { Message, MessageOptions } from "../internal/message" import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer" -import { CommandReplyRenderer } from "../internal/renderers/command-reply-renderer" -import type { ReacordConfig, ReacordInstance } from "./reacord" +import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer" +import type { ReacordInstance } from "./instance" +import type { ReacordConfig } from "./reacord" import { Reacord } from "./reacord" export class ReacordDiscordJs extends Reacord { @@ -15,7 +19,7 @@ export class ReacordDiscordJs extends Reacord { client.on("interactionCreate", (interaction) => { if (interaction.isMessageComponent()) { this.handleComponentInteraction( - createReacordComponentInteraction(interaction), + this.createReacordComponentInteraction(interaction), ) } }) @@ -26,21 +30,7 @@ export class ReacordDiscordJs extends Reacord { initialContent?: React.ReactNode, ): ReacordInstance { return this.createInstance( - new ChannelMessageRenderer({ - send: async (options) => { - const channel = - this.client.channels.cache.get(channelId) ?? - (await this.client.channels.fetch(channelId)) ?? - raise(`Channel ${channelId} not found`) - - if (!channel.isText()) { - raise(`Channel ${channelId} is not a text channel`) - } - - const message = await channel.send(getDiscordMessageOptions(options)) - return createReacordMessage(message) - }, - }), + this.createChannelRenderer(channelId), initialContent, ) } @@ -50,25 +40,7 @@ export class ReacordDiscordJs extends Reacord { initialContent?: React.ReactNode, ): ReacordInstance { return this.createInstance( - new CommandReplyRenderer({ - type: "command", - id: interaction.id, - channelId: interaction.channelId, - reply: async (options) => { - const message = await interaction.reply({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - followUp: async (options) => { - const message = await interaction.followUp({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - }), + this.createInteractionReplyRenderer(interaction), initialContent, ) } @@ -78,61 +50,180 @@ export class ReacordDiscordJs extends Reacord { initialContent?: React.ReactNode, ): ReacordInstance { return this.createInstance( - new CommandReplyRenderer({ - type: "command", - id: interaction.id, - channelId: interaction.channelId, - reply: async (options) => { - await interaction.reply({ - ...getDiscordMessageOptions(options), - ephemeral: true, - }) - return createEphemeralReacordMessage() - }, - followUp: async (options) => { - await interaction.followUp({ - ...getDiscordMessageOptions(options), - ephemeral: true, - }) - return createEphemeralReacordMessage() - }, - }), + this.createEphemeralInteractionReplyRenderer(interaction), initialContent, ) } + + private createChannelRenderer(channelId: string) { + return new ChannelMessageRenderer({ + send: async (options) => { + const channel = + this.client.channels.cache.get(channelId) ?? + (await this.client.channels.fetch(channelId)) ?? + raise(`Channel ${channelId} not found`) + + if (!channel.isText()) { + raise(`Channel ${channelId} is not a text channel`) + } + + const message = await channel.send(getDiscordMessageOptions(options)) + return createReacordMessage(message) + }, + }) + } + + private createInteractionReplyRenderer( + interaction: + | Discord.CommandInteraction + | Discord.MessageComponentInteraction, + ) { + return new InteractionReplyRenderer({ + type: "command", + id: interaction.id, + reply: async (options) => { + const message = await interaction.reply({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message as Discord.Message) + }, + followUp: async (options) => { + const message = await interaction.followUp({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message as Discord.Message) + }, + }) + } + + private createEphemeralInteractionReplyRenderer( + interaction: + | Discord.CommandInteraction + | Discord.MessageComponentInteraction, + ) { + return new InteractionReplyRenderer({ + type: "command", + id: interaction.id, + reply: async (options) => { + await interaction.reply({ + ...getDiscordMessageOptions(options), + ephemeral: true, + }) + return createEphemeralReacordMessage() + }, + followUp: async (options) => { + await interaction.followUp({ + ...getDiscordMessageOptions(options), + ephemeral: true, + }) + return createEphemeralReacordMessage() + }, + }) + } + + private createReacordComponentInteraction( + interaction: Discord.MessageComponentInteraction, + ): ComponentInteraction { + const baseProps: Except = { + id: interaction.id, + customId: interaction.customId, + update: async (options: MessageOptions) => { + await interaction.update(getDiscordMessageOptions(options)) + }, + deferUpdate: async () => { + if (interaction.replied || interaction.deferred) return + await interaction.deferUpdate() + }, + reply: async (options) => { + const message = await interaction.reply({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message as Discord.Message) + }, + followUp: async (options) => { + const message = await interaction.followUp({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message as Discord.Message) + }, + event: { + reply: (content?: ReactNode) => + this.createInstance( + this.createInteractionReplyRenderer(interaction), + content, + ), + + ephemeralReply: (content: ReactNode) => + this.createInstance( + this.createEphemeralInteractionReplyRenderer(interaction), + content, + ), + }, + } + + if (interaction.isButton()) { + return { + ...baseProps, + type: "button", + } + } + + if (interaction.isSelectMenu()) { + return { + ...baseProps, + type: "select", + event: { + ...baseProps.event, + values: interaction.values, + }, + } + } + + raise(`Unsupported component interaction type: ${interaction.type}`) + } } -function createReacordComponentInteraction( - interaction: Discord.MessageComponentInteraction, -): ComponentInteraction { - if (interaction.isButton()) { - return { - type: "button", - id: interaction.id, - channelId: interaction.channelId, - customId: interaction.customId, - update: async (options) => { - await interaction.update(getDiscordMessageOptions(options)) - }, - deferUpdate: () => interaction.deferUpdate(), - } - } +function createReacordMessage(message: Discord.Message): Message { + return { + edit: async (options) => { + await message.edit(getDiscordMessageOptions(options)) + }, + disableComponents: async () => { + for (const actionRow of message.components) { + for (const component of actionRow.components) { + component.setDisabled(true) + } + } - if (interaction.isSelectMenu()) { - return { - type: "select", - id: interaction.id, - channelId: interaction.channelId, - customId: interaction.customId, - values: interaction.values, - update: async (options) => { - await interaction.update(getDiscordMessageOptions(options)) - }, - deferUpdate: () => interaction.deferUpdate(), - } + await message.edit({ + components: message.components, + }) + }, + delete: async () => { + await message.delete() + }, } +} - raise(`Unsupported component interaction type: ${interaction.type}`) +function createEphemeralReacordMessage(): Message { + return { + edit: () => { + console.warn("Ephemeral messages can't be edited") + return Promise.resolve() + }, + disableComponents: () => { + console.warn("Ephemeral messages can't be edited") + return Promise.resolve() + }, + delete: () => { + console.warn("Ephemeral messages can't be deleted") + return Promise.resolve() + }, + } } // TODO: this could be a part of the core library, @@ -182,42 +273,3 @@ function getDiscordMessageOptions( return options } - -function createReacordMessage(message: Discord.Message): Message { - return { - edit: async (options) => { - await message.edit(getDiscordMessageOptions(options)) - }, - disableComponents: async () => { - for (const actionRow of message.components) { - for (const component of actionRow.components) { - component.setDisabled(true) - } - } - - await message.edit({ - components: message.components, - }) - }, - delete: async () => { - await message.delete() - }, - } -} - -function createEphemeralReacordMessage(): Message { - return { - edit: () => { - console.warn("Ephemeral messages can't be edited") - return Promise.resolve() - }, - disableComponents: () => { - console.warn("Ephemeral messages can't be edited") - return Promise.resolve() - }, - delete: () => { - console.warn("Ephemeral messages can't be deleted") - return Promise.resolve() - }, - } -} diff --git a/library/core/reacord-tester.ts b/library/core/reacord-tester.ts index 1e3ea23..ac56fa7 100644 --- a/library/core/reacord-tester.ts +++ b/library/core/reacord-tester.ts @@ -21,8 +21,10 @@ import type { MessageSelectOptions, } from "../internal/message" import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer" -import { CommandReplyRenderer } from "../internal/renderers/command-reply-renderer" -import type { ReacordInstance } from "./reacord" +import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer" +import type { ButtonClickEvent } from "./components/button" +import type { SelectChangeEvent } from "./components/select" +import type { ReacordInstance } from "./instance" import { Reacord } from "./reacord" const nextTickPromise = promisify(nextTick) @@ -46,7 +48,7 @@ export class ReacordTester extends Reacord { override reply(): ReacordInstance { return this.createInstance( - new CommandReplyRenderer( + new InteractionReplyRenderer( new TestCommandInteraction(this.messageContainer), ), ) @@ -111,6 +113,10 @@ export class ReacordTester extends Reacord { raise(`Couldn't find select with placeholder "${placeholder}"`) } + createMessage(options: MessageOptions) { + return new TestMessage(options, this.messageContainer) + } + private createButtonActions( button: MessageButtonOptions, message: TestMessage, @@ -118,7 +124,7 @@ export class ReacordTester extends Reacord { return { click: () => { this.handleComponentInteraction( - new TestButtonInteraction(button.customId, message), + new TestButtonInteraction(button.customId, message, this), ) }, } @@ -131,7 +137,7 @@ export class ReacordTester extends Reacord { return { select: (...values: string[]) => { this.handleComponentInteraction( - new TestSelectInteraction(component.customId, message, values), + new TestSelectInteraction(component.customId, message, values, this), ) }, } @@ -189,13 +195,25 @@ class TestInteraction { readonly id = nanoid() readonly channelId = "test-channel-id" - constructor(readonly customId: string, readonly message: TestMessage) {} + constructor( + readonly customId: string, + readonly message: TestMessage, + private tester: ReacordTester, + ) {} async update(options: MessageOptions): Promise { this.message.options = options } async deferUpdate(): Promise {} + + async reply(messageOptions: MessageOptions): Promise { + return this.tester.createMessage(messageOptions) + } + + async followUp(messageOptions: MessageOptions): Promise { + return this.tester.createMessage(messageOptions) + } } class TestButtonInteraction @@ -203,6 +221,12 @@ class TestButtonInteraction implements ButtonInteraction { readonly type = "button" + readonly event: ButtonClickEvent + + constructor(customId: string, message: TestMessage, tester: ReacordTester) { + super(customId, message, tester) + this.event = new TestButtonClickEvent(tester) + } } class TestSelectInteraction @@ -210,13 +234,41 @@ class TestSelectInteraction implements SelectInteraction { readonly type = "select" + readonly event: SelectChangeEvent constructor( customId: string, message: TestMessage, readonly values: string[], + tester: ReacordTester, ) { - super(customId, message) + super(customId, message, tester) + this.event = new TestSelectChangeEvent(values, tester) + } +} + +class TestComponentEvent { + constructor(private tester: ReacordTester) {} + + reply(content?: ReactNode): ReacordInstance { + return this.tester.reply() + } + + ephemeralReply(content?: ReactNode): ReacordInstance { + return this.tester.ephemeralReply() + } +} + +class TestButtonClickEvent + extends TestComponentEvent + implements ButtonClickEvent {} + +class TestSelectChangeEvent + extends TestComponentEvent + implements SelectChangeEvent +{ + constructor(readonly values: string[], tester: ReacordTester) { + super(tester) } } diff --git a/library/core/reacord.ts b/library/core/reacord.ts index 1708869..aeabe4f 100644 --- a/library/core/reacord.ts +++ b/library/core/reacord.ts @@ -2,6 +2,7 @@ import type { ReactNode } from "react" import type { ComponentInteraction } from "../internal/interaction" import { reconciler } from "../internal/reconciler.js" import type { Renderer } from "../internal/renderers/renderer" +import type { ReacordInstance } from "./instance" export type ReacordConfig = { /** @@ -11,12 +12,6 @@ export type ReacordConfig = { maxInstances?: number } -export type ReacordInstance = { - render: (content: ReactNode) => void - deactivate: () => void - destroy: () => void -} - export type ComponentInteractionListener = ( interaction: ComponentInteraction, ) => void diff --git a/library/internal/interaction.ts b/library/internal/interaction.ts index e686e99..06c2daf 100644 --- a/library/internal/interaction.ts +++ b/library/internal/interaction.ts @@ -1,32 +1,35 @@ +import type { ComponentEvent } from "../core/component-event" +import type { ButtonClickEvent, SelectChangeEvent } from "../main" import type { Message, MessageOptions } from "./message" export type Interaction = CommandInteraction | ComponentInteraction +export type ComponentInteraction = ButtonInteraction | SelectInteraction -export type CommandInteraction = { - type: "command" +export type CommandInteraction = BaseInteraction<"command"> + +export type ButtonInteraction = BaseComponentInteraction< + "button", + ButtonClickEvent +> + +export type SelectInteraction = BaseComponentInteraction< + "select", + SelectChangeEvent +> + +export type BaseInteraction = { + type: Type id: string - channelId: string reply(messageOptions: MessageOptions): Promise followUp(messageOptions: MessageOptions): Promise } -export type ComponentInteraction = ButtonInteraction | SelectInteraction - -export type ButtonInteraction = { - type: "button" - id: string - channelId: string +export type BaseComponentInteraction< + Type extends string, + Event extends ComponentEvent, +> = BaseInteraction & { + event: Event customId: string update(options: MessageOptions): Promise deferUpdate(): Promise } - -export type SelectInteraction = { - type: "select" - id: string - channelId: string - customId: string - values: string[] - update(options: MessageOptions): Promise - deferUpdate(): Promise -} diff --git a/library/internal/limited-collection.ts b/library/internal/limited-collection.ts new file mode 100644 index 0000000..2150d3f --- /dev/null +++ b/library/internal/limited-collection.ts @@ -0,0 +1,24 @@ +export class LimitedCollection { + private items: T[] = [] + + constructor(private readonly size: number) {} + + add(item: T) { + if (this.items.length >= this.size) { + this.items.shift() + } + this.items.push(item) + } + + has(item: T) { + return this.items.includes(item) + } + + values(): readonly T[] { + return this.items + } + + [Symbol.iterator]() { + return this.items[Symbol.iterator]() + } +} diff --git a/library/internal/renderers/command-reply-renderer.ts b/library/internal/renderers/interaction-reply-renderer.ts similarity index 76% rename from library/internal/renderers/command-reply-renderer.ts rename to library/internal/renderers/interaction-reply-renderer.ts index b17209c..163c78a 100644 --- a/library/internal/renderers/command-reply-renderer.ts +++ b/library/internal/renderers/interaction-reply-renderer.ts @@ -1,4 +1,4 @@ -import type { CommandInteraction } from "../interaction" +import type { Interaction } from "../interaction" import type { Message, MessageOptions } from "../message" import { Renderer } from "./renderer" @@ -6,8 +6,8 @@ import { Renderer } from "./renderer" // so we know whether to call reply() or followUp() const repliedInteractionIds = new Set() -export class CommandReplyRenderer extends Renderer { - constructor(private interaction: CommandInteraction) { +export class InteractionReplyRenderer extends Renderer { + constructor(private interaction: Interaction) { super() } diff --git a/library/internal/renderers/renderer.ts b/library/internal/renderers/renderer.ts index 470c170..31dadb4 100644 --- a/library/internal/renderers/renderer.ts +++ b/library/internal/renderers/renderer.ts @@ -4,10 +4,10 @@ import { Container } from "../container.js" import type { ComponentInteraction } from "../interaction" import type { Message, MessageOptions } from "../message" import type { Node } from "../node.js" -import { Timeout } from "../timeout" type UpdatePayload = | { action: "update" | "deactivate"; options: MessageOptions } + | { action: "deferUpdate"; interaction: ComponentInteraction } | { action: "destroy" } export abstract class Renderer { @@ -21,10 +21,6 @@ export abstract class Renderer { .pipe(concatMap((payload) => this.updateMessage(payload))) .subscribe({ error: console.error }) - private deferUpdateTimeout = new Timeout(500, () => { - this.componentInteraction?.deferUpdate().catch(console.error) - }) - render() { if (!this.active) { console.warn("Attempted to update a deactivated message") @@ -52,7 +48,11 @@ export abstract class Renderer { handleComponentInteraction(interaction: ComponentInteraction) { this.componentInteraction = interaction - this.deferUpdateTimeout.run() + + setTimeout(() => { + this.updates.next({ action: "deferUpdate", interaction }) + }, 500) + for (const node of this.nodes) { if (node.handleComponentInteraction(interaction)) { return true @@ -87,10 +87,14 @@ export abstract class Renderer { return } + if (payload.action === "deferUpdate") { + await payload.interaction.deferUpdate() + return + } + if (this.componentInteraction) { const promise = this.componentInteraction.update(payload.options) this.componentInteraction = undefined - this.deferUpdateTimeout.cancel() await promise return } diff --git a/library/main.ts b/library/main.ts index a71f695..6b48c94 100644 --- a/library/main.ts +++ b/library/main.ts @@ -1,3 +1,4 @@ +export * from "./core/component-event" export * from "./core/components/action-row" export * from "./core/components/button" export * from "./core/components/embed" @@ -10,6 +11,7 @@ export * from "./core/components/embed-title" export * from "./core/components/link" export * from "./core/components/option" export * from "./core/components/select" +export * from "./core/instance" export * from "./core/reacord" export * from "./core/reacord-discord-js" export * from "./core/reacord-tester" diff --git a/playground/fruit-select.tsx b/playground/fruit-select.tsx index 46607ab..2527de0 100644 --- a/playground/fruit-select.tsx +++ b/playground/fruit-select.tsx @@ -15,7 +15,7 @@ export function FruitSelect() { @@ -124,19 +126,19 @@ test("multiple select", async () => { tester.findSelectByPlaceholder("select").select("1", "3") await assertSelect(expect.arrayContaining(["1", "3"])) - expect(onSelect).toHaveBeenCalledWith({ - values: expect.arrayContaining(["1", "3"]), - }) + expect(onSelect).toHaveBeenCalledWith( + expect.objectContaining({ values: expect.arrayContaining(["1", "3"]) }), + ) tester.findSelectByPlaceholder("select").select("2") await assertSelect(expect.arrayContaining(["2"])) - expect(onSelect).toHaveBeenCalledWith({ - values: expect.arrayContaining(["2"]), - }) + expect(onSelect).toHaveBeenCalledWith( + expect.objectContaining({ values: expect.arrayContaining(["2"]) }), + ) tester.findSelectByPlaceholder("select").select() await assertSelect([]) - expect(onSelect).toHaveBeenCalledWith({ values: [] }) + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ values: [] })) }) test("optional onSelect + unknown value", async () => { diff --git a/todo.md b/todo.md index c3de50b..7f3b759 100644 --- a/todo.md +++ b/todo.md @@ -21,8 +21,14 @@ - [x] select onChange - [x] action row - [x] button onClick - - [ ] button click event - - [ ] select change event + - component events + - [x] reply / send functions + - [x] select values + - [ ] message.\* + - [ ] channel.\* + - [ ] guild.\* + - [ ] guild.member.\* + - [ ] user.\* - [x] deactivate - [x] destroy - [ ] docs @@ -48,3 +54,5 @@ - [x] single class/helper function for testing `ReacordTester` - [ ] handle deletion outside of reacord - [ ] for more easily writing adapters, address discord API nuances at the reacord level instead of the adapter level. the goal being that adapters can just take the objects and send them to discord. probably make use of discord api types for this +- [ ] allow users to specify their own customId for components + - this could be an easy and intuitive way to make component interactions work over bot restarts... among other interesting things