button onClick
This commit is contained in:
@@ -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([])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user