button onClick
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
/* eslint-disable unicorn/no-null */
|
/* 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 { Client, TextChannel } from "discord.js"
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { omit } from "../src/helpers/omit.js"
|
import { omit } from "../src/helpers/omit.js"
|
||||||
import { raise } from "../src/helpers/raise.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 type { ReacordRoot } from "../src/main.js"
|
||||||
import {
|
import {
|
||||||
ActionRow,
|
ActionRow,
|
||||||
@@ -112,17 +114,27 @@ test("kitchen sink", async () => {
|
|||||||
field content but inline
|
field content but inline
|
||||||
</EmbedField>
|
</EmbedField>
|
||||||
</Embed>
|
</Embed>
|
||||||
<Button style="primary">primary button</Button>
|
<Button onClick={() => {}} style="primary">
|
||||||
<Button style="danger">danger button</Button>
|
primary button
|
||||||
<Button style="success">success button</Button>
|
</Button>
|
||||||
<Button style="secondary">secondary button</Button>
|
<Button onClick={() => {}} style="danger">
|
||||||
<Button>secondary by default</Button>
|
danger button
|
||||||
<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
|
complex <Text>button</Text> text
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled>disabled button</Button>
|
<Button onClick={() => {}} disabled>
|
||||||
|
disabled button
|
||||||
|
</Button>
|
||||||
<ActionRow>
|
<ActionRow>
|
||||||
<Button>new action row</Button>
|
<Button onClick={() => {}}>new action row</Button>
|
||||||
</ActionRow>
|
</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 () => {
|
test("destroy", async () => {
|
||||||
await root.destroy()
|
await root.destroy()
|
||||||
await assertMessages([])
|
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"
|
import React from "react"
|
||||||
|
|
||||||
export type ButtonStyle = Exclude<Lowercase<MessageButtonStyle>, "link">
|
export type ButtonStyle = Exclude<Lowercase<MessageButtonStyle>, "link">
|
||||||
@@ -7,13 +12,19 @@ export type ButtonProps = {
|
|||||||
style?: ButtonStyle
|
style?: ButtonStyle
|
||||||
emoji?: EmojiResolvable
|
emoji?: EmojiResolvable
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
onClick: (interaction: ButtonInteraction) => void
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: ButtonProps) {
|
export function Button(props: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<reacord-element
|
<reacord-element
|
||||||
createNode={() => ({ ...props, type: "button", children: [] })}
|
createNode={() => ({
|
||||||
|
...props,
|
||||||
|
type: "button",
|
||||||
|
children: [],
|
||||||
|
customId: nanoid(),
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</reacord-element>
|
</reacord-element>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type {
|
import type {
|
||||||
BaseMessageComponentOptions,
|
BaseMessageComponentOptions,
|
||||||
|
ButtonInteraction,
|
||||||
ColorResolvable,
|
ColorResolvable,
|
||||||
EmojiResolvable,
|
EmojiResolvable,
|
||||||
MessageActionRowOptions,
|
MessageActionRowOptions,
|
||||||
MessageEmbedOptions,
|
MessageEmbedOptions,
|
||||||
MessageOptions,
|
MessageOptions,
|
||||||
} from "discord.js"
|
} from "discord.js"
|
||||||
import { nanoid } from "nanoid"
|
|
||||||
import type { ButtonStyle } from "./components/button.js"
|
import type { ButtonStyle } from "./components/button.js"
|
||||||
import { last } from "./helpers/last.js"
|
import { last } from "./helpers/last.js"
|
||||||
import { toUpper } from "./helpers/to-upper.js"
|
import { toUpper } from "./helpers/to-upper.js"
|
||||||
@@ -63,6 +63,8 @@ type ButtonNode = {
|
|||||||
style?: ButtonStyle
|
style?: ButtonStyle
|
||||||
emoji?: EmojiResolvable
|
emoji?: EmojiResolvable
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
customId: string
|
||||||
|
onClick: (interaction: ButtonInteraction) => void
|
||||||
children: Node[]
|
children: Node[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,12 +183,30 @@ function addActionRowItems(components: ActionRowOptions[], nodes: Node[]) {
|
|||||||
if (node.type === "button") {
|
if (node.type === "button") {
|
||||||
actionRow.components.push({
|
actionRow.components.push({
|
||||||
type: "BUTTON",
|
type: "BUTTON",
|
||||||
label: node.children.map(getNodeText).join(""),
|
label: node.children.map(getNodeText).join("") || "_ _",
|
||||||
style: node.style ? toUpper(node.style) : "SECONDARY",
|
style: node.style ? toUpper(node.style) : "SECONDARY",
|
||||||
emoji: node.emoji,
|
emoji: node.emoji,
|
||||||
disabled: node.disabled,
|
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 type { MessageNode } from "./node-tree.js"
|
||||||
import { getMessageOptions } from "./node-tree.js"
|
import { collectInteractionHandlers, getMessageOptions } from "./node-tree.js"
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "updateMessage"; options: MessageOptions }
|
| { type: "updateMessage"; tree: MessageNode }
|
||||||
| { type: "deleteMessage" }
|
| { type: "deleteMessage" }
|
||||||
|
|
||||||
export class MessageRenderer {
|
export class MessageRenderer {
|
||||||
private channel: TextBasedChannels
|
private channel: TextBasedChannels
|
||||||
|
private interactionCollector: InteractionCollector<MessageComponentInteraction>
|
||||||
private message?: Message
|
private message?: Message
|
||||||
|
private tree?: MessageNode
|
||||||
private actions: Action[] = []
|
private actions: Action[] = []
|
||||||
private runningPromise?: Promise<void>
|
private runningPromise?: Promise<void>
|
||||||
|
|
||||||
constructor(channel: TextBasedChannels) {
|
constructor(channel: TextBasedChannels) {
|
||||||
this.channel = channel
|
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) {
|
render(node: MessageNode) {
|
||||||
this.addAction({
|
this.addAction({
|
||||||
type: "updateMessage",
|
type: "updateMessage",
|
||||||
options: getMessageOptions(node),
|
tree: node,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.actions = []
|
this.actions = []
|
||||||
this.addAction({ type: "deleteMessage" })
|
this.addAction({ type: "deleteMessage" })
|
||||||
|
this.interactionCollector.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
completion() {
|
completion() {
|
||||||
@@ -65,15 +99,20 @@ export class MessageRenderer {
|
|||||||
|
|
||||||
private async runAction(action: Action) {
|
private async runAction(action: Action) {
|
||||||
if (action.type === "updateMessage") {
|
if (action.type === "updateMessage") {
|
||||||
this.message = await (this.message
|
const options = getMessageOptions(action.tree)
|
||||||
? this.message.edit({
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
...action.options,
|
if (this.message) {
|
||||||
|
this.message = await this.message.edit({
|
||||||
|
...options,
|
||||||
|
|
||||||
// need to ensure that, if there's no text, it's erased
|
// need to ensure that, if there's no text, it's erased
|
||||||
// eslint-disable-next-line unicorn/no-null
|
// eslint-disable-next-line unicorn/no-null
|
||||||
content: action.options.content ?? null,
|
content: options.content ?? null,
|
||||||
})
|
})
|
||||||
: this.channel.send(action.options))
|
} else {
|
||||||
|
this.message = await this.channel.send(options)
|
||||||
|
}
|
||||||
|
this.tree = action.tree
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === "deleteMessage") {
|
if (action.type === "deleteMessage") {
|
||||||
|
|||||||
Reference in New Issue
Block a user