button onClick

This commit is contained in:
MapleLeaf
2021-12-22 12:57:15 -06:00
parent 067b9b43a1
commit b3b0fdc279
4 changed files with 133 additions and 26 deletions

View File

@@ -1,9 +1,11 @@
/* eslint-disable unicorn/no-null */
import type { Message, MessageOptions } from "discord.js"
import type { ButtonInteraction, Message, MessageOptions } from "discord.js"
import { Client, TextChannel } from "discord.js"
import { nanoid } from "nanoid"
import React from "react"
import { omit } from "../src/helpers/omit.js"
import { raise } from "../src/helpers/raise.js"
import { waitForWithTimeout } from "../src/helpers/wait-for-with-timeout.js"
import type { ReacordRoot } from "../src/main.js"
import {
ActionRow,
@@ -112,17 +114,27 @@ test("kitchen sink", async () => {
field content but inline
</EmbedField>
</Embed>
<Button style="primary">primary button</Button>
<Button style="danger">danger button</Button>
<Button style="success">success button</Button>
<Button style="secondary">secondary button</Button>
<Button>secondary by default</Button>
<Button>
<Button onClick={() => {}} style="primary">
primary button
</Button>
<Button onClick={() => {}} style="danger">
danger button
</Button>
<Button onClick={() => {}} style="success">
success button
</Button>
<Button onClick={() => {}} style="secondary">
secondary button
</Button>
<Button onClick={() => {}}>secondary by default</Button>
<Button onClick={() => {}}>
complex <Text>button</Text> text
</Button>
<Button disabled>disabled button</Button>
<Button onClick={() => {}} disabled>
disabled button
</Button>
<ActionRow>
<Button>new action row</Button>
<Button onClick={() => {}}>new action row</Button>
</ActionRow>
</>,
)
@@ -231,6 +243,31 @@ test("kitchen sink", async () => {
])
})
test("button onClick", async () => {
let clicked = false
await root.render(<Button onClick={() => (clicked = true)} />)
const messages = await channel.messages.fetch()
const customId =
messages.first()?.components[0]?.components[0]?.customId ??
raise("Message not created")
client.emit("interactionCreate", {
id: nanoid(),
type: "MESSAGE_COMPONENT",
componentType: "BUTTON",
channelId: channel.id,
guildId: channel.guildId,
isButton: () => true,
customId,
user: { id: "123" },
} as ButtonInteraction)
await waitForWithTimeout(() => clicked, 1000)
})
test("destroy", async () => {
await root.destroy()
await assertMessages([])

View File

@@ -1,4 +1,9 @@
import type { EmojiResolvable, MessageButtonStyle } from "discord.js"
import type {
ButtonInteraction,
EmojiResolvable,
MessageButtonStyle,
} from "discord.js"
import { nanoid } from "nanoid"
import React from "react"
export type ButtonStyle = Exclude<Lowercase<MessageButtonStyle>, "link">
@@ -7,13 +12,19 @@ export type ButtonProps = {
style?: ButtonStyle
emoji?: EmojiResolvable
disabled?: boolean
onClick: (interaction: ButtonInteraction) => void
children?: React.ReactNode
}
export function Button(props: ButtonProps) {
return (
<reacord-element
createNode={() => ({ ...props, type: "button", children: [] })}
createNode={() => ({
...props,
type: "button",
children: [],
customId: nanoid(),
})}
>
{props.children}
</reacord-element>

View File

@@ -1,12 +1,12 @@
import type {
BaseMessageComponentOptions,
ButtonInteraction,
ColorResolvable,
EmojiResolvable,
MessageActionRowOptions,
MessageEmbedOptions,
MessageOptions,
} from "discord.js"
import { nanoid } from "nanoid"
import type { ButtonStyle } from "./components/button.js"
import { last } from "./helpers/last.js"
import { toUpper } from "./helpers/to-upper.js"
@@ -63,6 +63,8 @@ type ButtonNode = {
style?: ButtonStyle
emoji?: EmojiResolvable
disabled?: boolean
customId: string
onClick: (interaction: ButtonInteraction) => void
children: Node[]
}
@@ -181,12 +183,30 @@ function addActionRowItems(components: ActionRowOptions[], nodes: Node[]) {
if (node.type === "button") {
actionRow.components.push({
type: "BUTTON",
label: node.children.map(getNodeText).join(""),
label: node.children.map(getNodeText).join("") || "_ _",
style: node.style ? toUpper(node.style) : "SECONDARY",
emoji: node.emoji,
disabled: node.disabled,
customId: nanoid(),
customId: node.customId,
})
}
}
}
type InteractionHandler = {
type: "button"
customId: string
onClick: (interaction: ButtonInteraction) => void
}
export function collectInteractionHandlers(node: Node): InteractionHandler[] {
if (node.type === "button") {
return [{ type: "button", customId: node.customId, onClick: node.onClick }]
}
if ("children" in node) {
return node.children.flatMap(collectInteractionHandlers)
}
return []
}

View File

@@ -1,31 +1,65 @@
import type { Message, MessageOptions, TextBasedChannels } from "discord.js"
import type {
InteractionCollector,
Message,
MessageComponentInteraction,
MessageComponentType,
TextBasedChannels,
} from "discord.js"
import type { MessageNode } from "./node-tree.js"
import { getMessageOptions } from "./node-tree.js"
import { collectInteractionHandlers, getMessageOptions } from "./node-tree.js"
type Action =
| { type: "updateMessage"; options: MessageOptions }
| { type: "updateMessage"; tree: MessageNode }
| { type: "deleteMessage" }
export class MessageRenderer {
private channel: TextBasedChannels
private interactionCollector: InteractionCollector<MessageComponentInteraction>
private message?: Message
private tree?: MessageNode
private actions: Action[] = []
private runningPromise?: Promise<void>
constructor(channel: TextBasedChannels) {
this.channel = channel
this.interactionCollector =
this.createInteractionCollector() as InteractionCollector<MessageComponentInteraction>
}
private getInteractionHandler(customId: string) {
if (!this.tree) return undefined
const handlers = collectInteractionHandlers(this.tree)
return handlers.find((handler) => handler.customId === customId)
}
private createInteractionCollector() {
const collector =
this.channel.createMessageComponentCollector<MessageComponentType>({
filter: (interaction) =>
!!this.getInteractionHandler(interaction.customId),
})
collector.on("collect", (interaction) => {
const handler = this.getInteractionHandler(interaction.customId)
if (handler?.type === "button" && interaction.isButton()) {
handler.onClick(interaction)
}
})
return collector
}
render(node: MessageNode) {
this.addAction({
type: "updateMessage",
options: getMessageOptions(node),
tree: node,
})
}
destroy() {
this.actions = []
this.addAction({ type: "deleteMessage" })
this.interactionCollector.stop()
}
completion() {
@@ -65,15 +99,20 @@ export class MessageRenderer {
private async runAction(action: Action) {
if (action.type === "updateMessage") {
this.message = await (this.message
? this.message.edit({
...action.options,
const options = getMessageOptions(action.tree)
// eslint-disable-next-line unicorn/prefer-ternary
if (this.message) {
this.message = await this.message.edit({
...options,
// need to ensure that, if there's no text, it's erased
// eslint-disable-next-line unicorn/no-null
content: action.options.content ?? null,
})
: this.channel.send(action.options))
// need to ensure that, if there's no text, it's erased
// eslint-disable-next-line unicorn/no-null
content: options.content ?? null,
})
} else {
this.message = await this.channel.send(options)
}
this.tree = action.tree
}
if (action.type === "deleteMessage") {