diff --git a/packages/reacord/library/core/component-event.ts b/packages/reacord/library/core/component-event.ts index 713f659..c877708 100644 --- a/packages/reacord/library/core/component-event.ts +++ b/packages/reacord/library/core/component-event.ts @@ -5,32 +5,6 @@ import type { ReacordInstance } from "./instance" * @category Component Event */ export type ComponentEvent = { - /** - * The message associated with this event. - * For example: with a button click, - * this is the message that the button is on. - * @see https://discord.com/developers/docs/resources/channel#message-object - */ - message: MessageInfo - - /** - * The channel that this event occurred in. - * @see https://discord.com/developers/docs/resources/channel#channel-object - */ - channel: ChannelInfo - - /** - * The user that triggered this event. - * @see https://discord.com/developers/docs/resources/user#user-object - */ - user: UserInfo - - /** - * The guild that this event occurred in. - * @see https://discord.com/developers/docs/resources/guild#guild-object - */ - guild?: GuildInfo - /** * Create a new reply to this event. */ @@ -42,72 +16,3 @@ export type ComponentEvent = { */ ephemeralReply(content?: ReactNode): ReacordInstance } - -/** - * @category Component Event - */ -export type ChannelInfo = { - id: string - name?: string - topic?: string - nsfw?: boolean - lastMessageId?: string - ownerId?: string - parentId?: string - rateLimitPerUser?: number -} - -/** - * @category Component Event - */ -export type MessageInfo = { - id: string - channelId: string - authorId: UserInfo - member?: GuildMemberInfo - content: string - timestamp: string - editedTimestamp?: string - tts: boolean - mentionEveryone: boolean - /** The IDs of mentioned users */ - mentions: string[] -} - -/** - * @category Component Event - */ -export type GuildInfo = { - id: string - name: string - member: GuildMemberInfo -} - -/** - * @category Component Event - */ -export type GuildMemberInfo = { - id: string - nick?: string - displayName: string - avatarUrl?: string - displayAvatarUrl: string - roles: string[] - color: number - joinedAt?: string - premiumSince?: string - pending?: boolean - communicationDisabledUntil?: string -} - -/** - * @category Component Event - */ -export type UserInfo = { - id: string - username: string - discriminator: string - tag: string - avatarUrl: string - accentColor?: number -} diff --git a/packages/reacord/library/core/components/button.tsx b/packages/reacord/library/core/components/button.tsx index aa3e67a..5405da2 100644 --- a/packages/reacord/library/core/components/button.tsx +++ b/packages/reacord/library/core/components/button.tsx @@ -1,3 +1,4 @@ +import type { APIMessageComponentButtonInteraction } from "discord.js" import { randomUUID } from "node:crypto" import React from "react" import { ReacordElement } from "../../internal/element.js" @@ -27,7 +28,13 @@ export type ButtonProps = ButtonSharedProps & { /** * @category Button */ -export type ButtonClickEvent = ComponentEvent +export type ButtonClickEvent = ComponentEvent & { + /** + * Event details, e.g. the user who clicked, guild member, guild id, etc. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object + */ + interaction: APIMessageComponentButtonInteraction +} /** * @category Button @@ -42,8 +49,8 @@ export function Button(props: ButtonProps) { ) } -class ButtonNode extends Node { - private customId = randomUUID() +export class ButtonNode extends Node { + readonly customId = randomUUID() // this has text children, but buttons themselves shouldn't yield text // eslint-disable-next-line class-methods-use-this diff --git a/packages/reacord/library/core/components/select.tsx b/packages/reacord/library/core/components/select.tsx index bbc94d3..0338ce0 100644 --- a/packages/reacord/library/core/components/select.tsx +++ b/packages/reacord/library/core/components/select.tsx @@ -1,4 +1,5 @@ import { isInstanceOf } from "@reacord/helpers/is-instance-of.js" +import type { APIMessageComponentSelectMenuInteraction } from "discord.js" import { randomUUID } from "node:crypto" import type { ReactNode } from "react" import React from "react" @@ -73,7 +74,16 @@ export type SelectProps = { * @category Select */ export type SelectChangeEvent = ComponentEvent & { + /** The set of values that were selected by the user. + * If `multiple`, this can have more than one value. + */ values: string[] + + /** + * Event details, e.g. the user who clicked, guild member, guild id, etc. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object + */ + interaction: APIMessageComponentSelectMenuInteraction } /** @@ -88,7 +98,7 @@ export function Select(props: SelectProps) { ) } -class SelectNode extends Node { +export class SelectNode extends Node { readonly customId = randomUUID() override modifyMessageOptions(message: MessageOptions): void { diff --git a/packages/reacord/library/djs/reacord-discord-js.ts b/packages/reacord/library/djs/reacord-discord-js.ts index 6e08994..a509966 100644 --- a/packages/reacord/library/djs/reacord-discord-js.ts +++ b/packages/reacord/library/djs/reacord-discord-js.ts @@ -1,17 +1,12 @@ /* eslint-disable class-methods-use-this */ -import { pick } from "@reacord/helpers/pick" -import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values" import { raise } from "@reacord/helpers/raise" +import type { + APIMessageComponentButtonInteraction, + APIMessageComponentSelectMenuInteraction, +} from "discord.js" import * as Discord from "discord.js" import type { ReactNode } from "react" -import type { Except } from "type-fest" -import type { - ChannelInfo, - GuildInfo, - GuildMemberInfo, - MessageInfo, - UserInfo, -} from "../core/component-event" + import type { ReacordInstance } from "../core/instance" import type { ReacordConfig } from "../core/reacord" import { Reacord } from "../core/reacord" @@ -39,6 +34,18 @@ export class ReacordDiscordJs extends Reacord { ) } }) + + client.ws.on( + Discord.GatewayDispatchEvents.InteractionCreate, + (data: Discord.APIInteraction) => { + if (data.type === Discord.InteractionType.MessageComponent) { + data + // this.handleComponentInteraction( + // this.createReacordComponentInteraction(data), + // ) + } + }, + ) } /** @@ -154,83 +161,7 @@ export class ReacordDiscordJs extends Reacord { private createReacordComponentInteraction( interaction: Discord.MessageComponentInteraction, ): ComponentInteraction { - // todo please dear god clean this up - const channel: ChannelInfo = interaction.channel - ? { - ...pruneNullishValues( - pick(interaction.channel, [ - "topic", - "nsfw", - "lastMessageId", - "ownerId", - "parentId", - "rateLimitPerUser", - ]), - ), - id: interaction.channelId, - } - : raise("Non-channel interactions are not supported") - - const message: MessageInfo = - interaction.message instanceof Discord.Message - ? { - ...pick(interaction.message, [ - "id", - "channelId", - "authorId", - "content", - "tts", - "mentionEveryone", - ]), - timestamp: new Date( - interaction.message.createdTimestamp, - ).toISOString(), - editedTimestamp: interaction.message.editedTimestamp - ? new Date(interaction.message.editedTimestamp).toISOString() - : undefined, - mentions: interaction.message.mentions.users.map((u) => u.id), - } - : raise("Message not found") - - const member: GuildMemberInfo | undefined = - interaction.member instanceof Discord.GuildMember - ? { - ...pruneNullishValues( - pick(interaction.member, [ - "id", - "nick", - "displayName", - "avatarUrl", - "displayAvatarUrl", - "color", - "pending", - ]), - ), - displayName: interaction.member.displayName, - roles: [...interaction.member.roles.cache.map((role) => role.id)], - joinedAt: interaction.member.joinedAt?.toISOString(), - premiumSince: interaction.member.premiumSince?.toISOString(), - communicationDisabledUntil: - interaction.member.communicationDisabledUntil?.toISOString(), - } - : undefined - - const guild: GuildInfo | undefined = interaction.guild - ? { - ...pruneNullishValues(pick(interaction.guild, ["id", "name"])), - member: member ?? raise("unexpected: member is undefined"), - } - : undefined - - const user: UserInfo = { - ...pruneNullishValues( - pick(interaction.user, ["id", "username", "discriminator", "tag"]), - ), - avatarUrl: interaction.user.avatarURL()!, - accentColor: interaction.user.accentColor ?? undefined, - } - - const baseProps: Except = { + const baseProps = { id: interaction.id, customId: interaction.customId, update: async (options: MessageOptions) => { @@ -240,14 +171,14 @@ export class ReacordDiscordJs extends Reacord { if (interaction.replied || interaction.deferred) return await interaction.deferUpdate() }, - reply: async (options) => { + reply: async (options: MessageOptions) => { const message = await interaction.reply({ ...getDiscordMessageOptions(options), fetchReply: true, }) return createReacordMessage(message as Discord.Message) }, - followUp: async (options) => { + followUp: async (options: MessageOptions) => { const message = await interaction.followUp({ ...getDiscordMessageOptions(options), fetchReply: true, @@ -255,11 +186,6 @@ export class ReacordDiscordJs extends Reacord { return createReacordMessage(message as Discord.Message) }, event: { - channel, - message, - user, - guild, - reply: (content?: ReactNode) => this.createInstance( this.createInteractionReplyRenderer(interaction), @@ -278,6 +204,11 @@ export class ReacordDiscordJs extends Reacord { return { ...baseProps, type: "button", + event: { + ...baseProps.event, + interaction: + interaction.toJSON() as APIMessageComponentButtonInteraction, + }, } } @@ -288,6 +219,8 @@ export class ReacordDiscordJs extends Reacord { event: { ...baseProps.event, values: interaction.values, + interaction: + interaction.toJSON() as APIMessageComponentSelectMenuInteraction, }, } } diff --git a/packages/reacord/library/internal/node.ts b/packages/reacord/library/internal/node.ts index 8efe584..4a49d22 100644 --- a/packages/reacord/library/internal/node.ts +++ b/packages/reacord/library/internal/node.ts @@ -1,20 +1,45 @@ -/* eslint-disable class-methods-use-this */ -import { Container } from "./container.js" -import type { ComponentInteraction } from "./interaction" -import type { MessageOptions } from "./message" - -export abstract class Node { - readonly children = new Container>() +export class Node { + private readonly _children: Node[] = [] constructor(public props: Props) {} - modifyMessageOptions(options: MessageOptions) {} - - handleComponentInteraction(interaction: ComponentInteraction): boolean { - return false + get children(): readonly Node[] { + return this._children } - get text(): string { - return [...this.children].map((child) => child.text).join("") + clear() { + this._children.splice(0) + } + + add(...nodes: Node[]) { + this._children.push(...nodes) + } + + remove(node: Node) { + const index = this._children.indexOf(node) + if (index !== -1) this._children.splice(index, 1) + } + + insertBefore(node: Node, beforeNode: Node) { + const index = this._children.indexOf(beforeNode) + if (index !== -1) this._children.splice(index, 0, node) + } + + replace(oldNode: Node, newNode: Node) { + const index = this._children.indexOf(oldNode) + if (index !== -1) this._children[index] = newNode + } + + clone(): this { + const cloned: this = new (this.constructor as any)() + cloned.add(...this.children.map((child) => child.clone())) + return cloned + } + + *walk(): Generator { + yield this + for (const child of this.children) { + yield* child.walk() + } } } diff --git a/packages/reacord/library/internal/reconciler.ts b/packages/reacord/library/internal/reconciler.ts index c5e82ca..7b5fe5b 100644 --- a/packages/reacord/library/internal/reconciler.ts +++ b/packages/reacord/library/internal/reconciler.ts @@ -1,16 +1,16 @@ +/* eslint-disable unicorn/prefer-modern-dom-apis */ import { raise } from "@reacord/helpers/raise.js" -import type { HostConfig } from "react-reconciler" import ReactReconciler from "react-reconciler" import { DefaultEventPriority } from "react-reconciler/constants" +import type { ReacordInstancePrivate } from "../reacord-instance.js" import { Node } from "./node.js" -import type { Renderer } from "./renderers/renderer" import { TextNode } from "./text-node.js" -const config: HostConfig< +export const reconciler = ReactReconciler< string, // Type, Record, // Props, - Renderer, // Container, - Node, // Instance, + ReacordInstancePrivate, // Container, + Node, // Instance, TextNode, // TextInstance, never, // SuspenseInstance, never, // HydratableInstance, @@ -20,7 +20,7 @@ const config: HostConfig< never, // ChildSet, number, // TimeoutHandle, number // NoTimeout, -> = { +>({ supportsMutation: true, supportsPersistence: false, supportsHydration: false, @@ -49,7 +49,7 @@ const config: HostConfig< return node }, - createTextInstance: (text) => new TextNode(text), + createTextInstance: (text) => new TextNode({ text }), shouldSetTextContent: () => false, detachDeletedInstance: (instance) => {}, beforeActiveInstanceBlur: () => {}, @@ -59,30 +59,30 @@ const config: HostConfig< // eslint-disable-next-line unicorn/no-null getInstanceFromScope: (scopeInstance: any) => null, - clearContainer: (renderer) => { - renderer.nodes.clear() + clearContainer: (instance) => { + instance.tree.clear() }, - appendChildToContainer: (renderer, child) => { - renderer.nodes.add(child) + appendChildToContainer: (instance, child) => { + instance.tree.add(child) }, - removeChildFromContainer: (renderer, child) => { - renderer.nodes.remove(child) + removeChildFromContainer: (instance, child) => { + instance.tree.remove(child) }, - insertInContainerBefore: (renderer, child, before) => { - renderer.nodes.addBefore(child, before) + insertInContainerBefore: (instance, child, before) => { + instance.tree.insertBefore(child, before) }, appendInitialChild: (parent, child) => { - parent.children.add(child) + parent.add(child) }, appendChild: (parent, child) => { - parent.children.add(child) + parent.add(child) }, removeChild: (parent, child) => { - parent.children.remove(child) + parent.remove(child) }, insertBefore: (parent, child, before) => { - parent.children.addBefore(child, before) + parent.insertBefore(child, before) }, prepareUpdate: () => true, @@ -90,13 +90,13 @@ const config: HostConfig< node.props = newProps.props }, commitTextUpdate: (node, oldText, newText) => { - node.props = newText + node.props.text = newText }, // eslint-disable-next-line unicorn/no-null prepareForCommit: () => null, resetAfterCommit: (renderer) => { - renderer.render() + void renderer.update(renderer.tree) }, prepareScopeUpdate: (scopeInstance: any, instance: any) => {}, @@ -106,6 +106,4 @@ const config: HostConfig< finalizeInitialChildren: () => false, getCurrentEventPriority: () => DefaultEventPriority, -} - -export const reconciler = ReactReconciler(config) +}) diff --git a/packages/reacord/library/internal/text-node.ts b/packages/reacord/library/internal/text-node.ts index ead02ad..1958b76 100644 --- a/packages/reacord/library/internal/text-node.ts +++ b/packages/reacord/library/internal/text-node.ts @@ -1,12 +1,3 @@ -import type { MessageOptions } from "./message" import { Node } from "./node.js" -export class TextNode extends Node { - override modifyMessageOptions(options: MessageOptions) { - options.content = options.content + this.props - } - - override get text() { - return this.props - } -} +export class TextNode extends Node<{ text: string }> {} diff --git a/packages/reacord/library/main.ts b/packages/reacord/library/main.ts index 6180c63..36432d7 100644 --- a/packages/reacord/library/main.ts +++ b/packages/reacord/library/main.ts @@ -1,18 +1,19 @@ -export * from "./core/component-event" -export * from "./core/components/action-row" -export * from "./core/components/button" -export * from "./core/components/button-shared-props" -export * from "./core/components/embed" -export * from "./core/components/embed-author" -export * from "./core/components/embed-field" -export * from "./core/components/embed-footer" -export * from "./core/components/embed-image" -export * from "./core/components/embed-thumbnail" -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 { useInstance } from "./core/instance-context" -export * from "./core/reacord" -export * from "./djs/reacord-discord-js" +// export * from "./core/component-event" +// export * from "./core/components/action-row" +// export * from "./core/components/button" +// export * from "./core/components/button-shared-props" +// export * from "./core/components/embed" +// export * from "./core/components/embed-author" +// export * from "./core/components/embed-field" +// export * from "./core/components/embed-footer" +// export * from "./core/components/embed-image" +// export * from "./core/components/embed-thumbnail" +// 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 { useInstance } from "./core/instance-context" +// export * from "./core/reacord" +// export * from "./djs/reacord-discord-js" +export {} diff --git a/packages/reacord/library/reacord-client.tsx b/packages/reacord/library/reacord-client.tsx new file mode 100644 index 0000000..3d70705 --- /dev/null +++ b/packages/reacord/library/reacord-client.tsx @@ -0,0 +1,153 @@ +import type { APIInteraction } from "discord.js" +import { + Client, + GatewayDispatchEvents, + GatewayIntentBits, + InteractionType, +} from "discord.js" +import * as React from "react" +import { InstanceProvider } from "./core/instance-context.js" +import type { ReacordInstance } from "./reacord-instance.js" +import { ReacordInstancePrivate } from "./reacord-instance.js" +import type { Renderer } from "./renderer.js" +import { + ChannelMessageRenderer, + EphemeralInteractionReplyRenderer, + InteractionReplyRenderer, +} from "./renderer.js" + +/** + * @category Core + */ +export type ReacordConfig = { + /** Discord bot token */ + token: string + + /** + * The max number of active instances. + * When this limit is exceeded, the oldest instances will be cleaned up + * to prevent memory leaks. + */ + maxInstances?: number +} + +/** + * Info for replying to an interaction. For Discord.js + * (and probably other libraries) you should be able to pass the + * interaction object directly: + * ```js + * client.on("interactionCreate", (interaction) => { + * if (interaction.isChatInputCommand() && interaction.commandName === "hi") { + * interaction.reply("hi lol") + * } + * }) + * ``` + */ +export type InteractionInfo = { + id: string + token: string +} + +/** + * @category Core + */ +export class ReacordClient { + private readonly config: Required + private readonly client: Client + private instances: ReacordInstancePrivate[] = [] + private destroyed = false + + constructor(config: ReacordConfig) { + this.config = { + ...config, + maxInstances: config.maxInstances ?? 50, + } + + this.client = new Client({ intents: [GatewayIntentBits.Guilds] }) + this.client.login(this.config.token).catch(console.error) + + this.client.ws.on( + GatewayDispatchEvents.InteractionCreate, + (interaction: APIInteraction) => { + if (interaction.type !== InteractionType.MessageComponent) return + for (const instance of this.instances) { + instance.handleInteraction(interaction, this) + } + }, + ) + } + + send(channelId: string, initialContent?: React.ReactNode): ReacordInstance { + return this.createInstance( + new ChannelMessageRenderer(channelId), + initialContent, + ) + } + + reply( + interaction: InteractionInfo, + initialContent?: React.ReactNode, + ): ReacordInstance { + return this.createInstance( + new InteractionReplyRenderer(interaction), + initialContent, + ) + } + + ephemeralReply( + interaction: InteractionInfo, + initialContent?: React.ReactNode, + ): ReacordInstance { + return this.createInstance( + new EphemeralInteractionReplyRenderer(interaction), + initialContent, + ) + } + + destroy() { + this.client.destroy() + this.destroyed = true + } + + private createInstance( + renderer: Renderer, + initialContent?: React.ReactNode, + ): ReacordInstance { + if (this.destroyed) throw new Error("ReacordClient is destroyed") + + const instance = new ReacordInstancePrivate(renderer) + + this.instances.push(instance) + + if (this.instances.length > this.config.maxInstances) { + void this.instances[0]?.deactivate() + this.removeInstance(this.instances[0]!) + } + + const publicInstance: ReacordInstance = { + render: (content: React.ReactNode) => { + instance.render( + {content}, + ) + }, + deactivate: () => { + this.removeInstance(instance) + renderer.deactivate().catch(console.error) + }, + destroy: () => { + this.removeInstance(instance) + renderer.destroy().catch(console.error) + }, + } + + if (initialContent !== undefined) { + publicInstance.render(initialContent) + } + + return publicInstance + } + + private removeInstance(instance: ReacordInstancePrivate) { + this.instances = this.instances.filter((the) => the !== instance) + } +} diff --git a/packages/reacord/library/reacord-instance.ts b/packages/reacord/library/reacord-instance.ts new file mode 100644 index 0000000..a473f2a --- /dev/null +++ b/packages/reacord/library/reacord-instance.ts @@ -0,0 +1,130 @@ +import type { + APIMessageComponentButtonInteraction, + APIMessageComponentInteraction, + APIMessageComponentSelectMenuInteraction, +} from "discord.js" +import { ComponentType } from "discord.js" +import type { ComponentEvent } from "./core/component-event.js" +import { ButtonNode } from "./core/components/button.js" +import type { SelectChangeEvent } from "./core/components/select.js" +import { SelectNode } from "./core/components/select.js" +import { Node } from "./internal/node.js" +import { reconciler } from "./internal/reconciler.js" +import type { ReacordClient } from "./reacord-client.js" +import type { Renderer } from "./renderer.js" + +/** + * Represents an interactive message, which can later be replaced or deleted. + * @category Core + */ +export type ReacordInstance = { + /** Render some JSX to this instance (edits the message) */ + render(content: React.ReactNode): void + + /** Remove this message */ + deactivate(): void + + /** + * Same as destroy, but keeps the message and disables the components on it. + * This prevents it from listening to user interactions. + */ + destroy(): void +} + +export class ReacordInstancePrivate { + private readonly container = reconciler.createContainer( + this, + 0, + // eslint-disable-next-line unicorn/no-null + null, + false, + // eslint-disable-next-line unicorn/no-null + null, + "reacord", + () => {}, + // eslint-disable-next-line unicorn/no-null + null, + ) + + readonly tree = new Node({}) + private latestTree?: Node + + constructor(private readonly renderer: Renderer) {} + + render(content: React.ReactNode) { + reconciler.updateContainer(content, this.container) + } + + async update(tree: Node) { + try { + await this.renderer.update(tree) + this.latestTree = tree + } catch (error) { + console.error(error) + } + } + + async deactivate() { + try { + await this.renderer.deactivate() + } catch (error) { + console.error(error) + } + } + + async destroy() { + try { + await this.renderer.destroy() + } catch (error) { + console.error(error) + } + } + + handleInteraction( + interaction: APIMessageComponentInteraction, + client: ReacordClient, + ) { + if (!this.latestTree) return + + const baseEvent: ComponentEvent = { + reply: (content) => client.reply(interaction, content), + ephemeralReply: (content) => client.ephemeralReply(interaction, content), + } + + if (interaction.data.component_type === ComponentType.Button) { + for (const node of this.latestTree.walk()) { + if ( + node instanceof ButtonNode && + node.customId === interaction.data.custom_id + ) { + node.props.onClick({ + ...baseEvent, + interaction: interaction as APIMessageComponentButtonInteraction, + }) + break + } + } + } + + if (interaction.data.component_type === ComponentType.SelectMenu) { + const event: SelectChangeEvent = { + ...baseEvent, + interaction: interaction as APIMessageComponentSelectMenuInteraction, + values: interaction.data.values, + } + + for (const node of this.latestTree.walk()) { + if ( + node instanceof SelectNode && + node.customId === interaction.data.custom_id + ) { + node.props.onChange?.(event) + node.props.onChangeMultiple?.(interaction.data.values, event) + if (interaction.data.values[0]) { + node.props.onChangeValue?.(interaction.data.values[0], event) + } + } + } + } + } +} diff --git a/packages/reacord/library/renderer.ts b/packages/reacord/library/renderer.ts new file mode 100644 index 0000000..1ea7deb --- /dev/null +++ b/packages/reacord/library/renderer.ts @@ -0,0 +1,59 @@ +import { AsyncQueue } from "@reacord/helpers/async-queue" +import type { Node } from "./internal/node.js" +import type { InteractionInfo } from "./reacord-client.js" + +export type Renderer = { + update(tree: Node): Promise + deactivate(): Promise + destroy(): Promise +} + +export class ChannelMessageRenderer implements Renderer { + private readonly queue = new AsyncQueue() + + constructor(private readonly channelId: string) {} + + update(tree: Node): Promise { + throw new Error("Method not implemented.") + } + + deactivate(): Promise { + throw new Error("Method not implemented.") + } + + destroy(): Promise { + throw new Error("Method not implemented.") + } +} + +export class InteractionReplyRenderer implements Renderer { + constructor(private readonly interaction: InteractionInfo) {} + + update(tree: Node): Promise { + throw new Error("Method not implemented.") + } + + deactivate(): Promise { + throw new Error("Method not implemented.") + } + + destroy(): Promise { + throw new Error("Method not implemented.") + } +} + +export class EphemeralInteractionReplyRenderer implements Renderer { + constructor(private readonly interaction: InteractionInfo) {} + + update(tree: Node): Promise { + throw new Error("Method not implemented.") + } + + deactivate(): Promise { + throw new Error("Method not implemented.") + } + + destroy(): Promise { + throw new Error("Method not implemented.") + } +} diff --git a/packages/reacord/scripts/discordjs-manual-test.tsx b/packages/reacord/scripts/discordjs-manual-test.tsx index 2d1e8bf..23e2a4f 100644 --- a/packages/reacord/scripts/discordjs-manual-test.tsx +++ b/packages/reacord/scripts/discordjs-manual-test.tsx @@ -59,9 +59,10 @@ const tests: TestCase[] = [ <>