component events
This commit is contained in:
8
library/core/component-event.ts
Normal file
8
library/core/component-event.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -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<ButtonProps> {
|
||||
interaction.type === "button" &&
|
||||
interaction.customId === this.customId
|
||||
) {
|
||||
this.props.onClick(interaction)
|
||||
this.props.onClick(interaction.event)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -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<SelectProps> {
|
||||
minValues = 0,
|
||||
maxValues = 25,
|
||||
children,
|
||||
onSelect,
|
||||
onSelectValue,
|
||||
onSelectMultiple,
|
||||
onChange,
|
||||
onChangeValue,
|
||||
onChangeMultiple,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
@@ -72,18 +73,18 @@ class SelectNode extends Node<SelectProps> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
7
library/core/instance.ts
Normal file
7
library/core/instance.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export type ReacordInstance = {
|
||||
render: (content: ReactNode) => void
|
||||
deactivate: () => void
|
||||
destroy: () => void
|
||||
}
|
||||
@@ -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<ComponentInteraction, "type"> = {
|
||||
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()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
this.message.options = options
|
||||
}
|
||||
|
||||
async deferUpdate(): Promise<void> {}
|
||||
|
||||
async reply(messageOptions: MessageOptions): Promise<Message> {
|
||||
return this.tester.createMessage(messageOptions)
|
||||
}
|
||||
|
||||
async followUp(messageOptions: MessageOptions): Promise<Message> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 extends string> = {
|
||||
type: Type
|
||||
id: string
|
||||
channelId: string
|
||||
reply(messageOptions: MessageOptions): Promise<Message>
|
||||
followUp(messageOptions: MessageOptions): Promise<Message>
|
||||
}
|
||||
|
||||
export type ComponentInteraction = ButtonInteraction | SelectInteraction
|
||||
|
||||
export type ButtonInteraction = {
|
||||
type: "button"
|
||||
id: string
|
||||
channelId: string
|
||||
export type BaseComponentInteraction<
|
||||
Type extends string,
|
||||
Event extends ComponentEvent,
|
||||
> = BaseInteraction<Type> & {
|
||||
event: Event
|
||||
customId: string
|
||||
update(options: MessageOptions): Promise<void>
|
||||
deferUpdate(): Promise<void>
|
||||
}
|
||||
|
||||
export type SelectInteraction = {
|
||||
type: "select"
|
||||
id: string
|
||||
channelId: string
|
||||
customId: string
|
||||
values: string[]
|
||||
update(options: MessageOptions): Promise<void>
|
||||
deferUpdate(): Promise<void>
|
||||
}
|
||||
|
||||
24
library/internal/limited-collection.ts
Normal file
24
library/internal/limited-collection.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export class LimitedCollection<T> {
|
||||
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]()
|
||||
}
|
||||
}
|
||||
@@ -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<string>()
|
||||
|
||||
export class CommandReplyRenderer extends Renderer {
|
||||
constructor(private interaction: CommandInteraction) {
|
||||
export class InteractionReplyRenderer extends Renderer {
|
||||
constructor(private interaction: Interaction) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,7 +15,7 @@ export function FruitSelect() {
|
||||
<Select
|
||||
placeholder="choose a fruit"
|
||||
value={value}
|
||||
onSelectValue={setValue}
|
||||
onChangeValue={setValue}
|
||||
>
|
||||
<Option value="🍎" />
|
||||
<Option value="🍌" />
|
||||
|
||||
@@ -67,12 +67,16 @@ createCommandHandler(client, [
|
||||
run: (interaction) => {
|
||||
reacord.reply(
|
||||
interaction,
|
||||
<Button
|
||||
label="clic"
|
||||
onClick={() => {
|
||||
reacord.ephemeralReply(interaction, "you clic")
|
||||
}}
|
||||
/>,
|
||||
<>
|
||||
<Button
|
||||
label="public clic"
|
||||
onClick={() => reacord.reply(interaction, "you clic")}
|
||||
/>
|
||||
<Button
|
||||
label="clic"
|
||||
onClick={(event) => event.ephemeralReply("you clic")}
|
||||
/>
|
||||
</>,
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
3
test/event-callbacks.test.tsx
Normal file
3
test/event-callbacks.test.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
test.todo("button onClick")
|
||||
test.todo("select onChange")
|
||||
export {}
|
||||
@@ -14,8 +14,8 @@ test("single select", async () => {
|
||||
<Select
|
||||
placeholder="choose one"
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
onSelectValue={setValue}
|
||||
onChange={onSelect}
|
||||
onChangeValue={setValue}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Option value="1" />
|
||||
@@ -60,7 +60,9 @@ test("single select", async () => {
|
||||
|
||||
tester.findSelectByPlaceholder("choose one").select("2")
|
||||
await assertSelect(["2"])
|
||||
expect(onSelect).toHaveBeenCalledWith({ values: ["2"] })
|
||||
expect(onSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ values: ["2"] }),
|
||||
)
|
||||
|
||||
tester.findButtonByLabel("disable").click()
|
||||
await assertSelect(["2"], true)
|
||||
@@ -81,8 +83,8 @@ test("multiple select", async () => {
|
||||
placeholder="select"
|
||||
multiple
|
||||
values={values}
|
||||
onSelect={onSelect}
|
||||
onSelectMultiple={setValues}
|
||||
onChange={onSelect}
|
||||
onChangeMultiple={setValues}
|
||||
>
|
||||
<Option value="1">one</Option>
|
||||
<Option value="2">two</Option>
|
||||
@@ -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 () => {
|
||||
|
||||
12
todo.md
12
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
|
||||
|
||||
Reference in New Issue
Block a user