From 18bcf4828c854c9cc61046f95c3f5fdcf1ad9dcc Mon Sep 17 00:00:00 2001 From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Sat, 25 Dec 2021 01:52:55 -0600 Subject: [PATCH] refactor: interactive button --- package.json | 2 +- playground/main.tsx | 2 +- src.new/components/button.tsx | 3 ++ src.new/main.ts | 33 ++++++++++-- src.new/node.ts | 1 + src.new/reconciler.ts | 28 ++++++----- src.new/renderer.ts | 94 +++++++++++++++++++++++++++++++++++ src.new/root-node.ts | 67 ------------------------- src.new/text-node.ts | 8 +-- 9 files changed, 145 insertions(+), 93 deletions(-) create mode 100644 src.new/renderer.ts delete mode 100644 src.new/root-node.ts diff --git a/package.json b/package.json index 27f2afa..43248d2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test-watch": "vitest --watch", "coverage": "vitest --coverage", "typecheck": "tsc --noEmit", - "playground": "nodemon -x esmo ./playground/main.tsx" + "playground": "nodemon --exec esmo --ext ts,tsx ./playground/main.tsx" }, "dependencies": { "@types/node": "*", diff --git a/playground/main.tsx b/playground/main.tsx index a9f78b0..c423e88 100644 --- a/playground/main.tsx +++ b/playground/main.tsx @@ -9,7 +9,7 @@ const client = new Client({ intents: ["GUILDS"], }) -const manager = new InstanceManager() +const manager = InstanceManager.create(client) createCommandHandler(client, [ { diff --git a/src.new/components/button.tsx b/src.new/components/button.tsx index b79a2e4..3cb8e7e 100644 --- a/src.new/components/button.tsx +++ b/src.new/components/button.tsx @@ -3,6 +3,7 @@ import type { MessageButtonStyle, MessageComponentInteraction, } from "discord.js" +import { nanoid } from "nanoid" import React from "react" import { Node } from "../node.js" @@ -22,6 +23,8 @@ export function Button(props: ButtonProps) { export class ButtonNode extends Node { readonly name = "button" + readonly customId = nanoid() + constructor(public props: ButtonProps) { super() } diff --git a/src.new/main.ts b/src.new/main.ts index 0a52d7e..d35a699 100644 --- a/src.new/main.ts +++ b/src.new/main.ts @@ -1,12 +1,31 @@ -import type { CommandInteraction } from "discord.js" +import type { + Client, + CommandInteraction, + MessageComponentInteraction, +} from "discord.js" import type { ReactNode } from "react" import type { OpaqueRoot } from "react-reconciler" import { reconciler } from "./reconciler.js" -import { RootNode } from "./root-node.js" +import { Renderer } from "./renderer.js" export class InstanceManager { private instances = new Set() + private constructor() {} + + static create(client: Client) { + const manager = new InstanceManager() + + client.on("interactionCreate", (interaction) => { + if (!interaction.isMessageComponent()) return + for (const instance of manager.instances) { + if (instance.handleInteraction(interaction)) return + } + }) + + return manager + } + create(interaction: CommandInteraction) { const instance = new Instance(interaction) this.instances.add(instance) @@ -19,15 +38,19 @@ export class InstanceManager { } class Instance { - private rootNode: RootNode + private renderer: Renderer private container: OpaqueRoot constructor(interaction: CommandInteraction) { - this.rootNode = new RootNode(interaction) - this.container = reconciler.createContainer(this.rootNode, 0, false, {}) + this.renderer = new Renderer(interaction) + this.container = reconciler.createContainer(this.renderer, 0, false, {}) } render(content: ReactNode) { reconciler.updateContainer(content, this.container) } + + handleInteraction(interaction: MessageComponentInteraction) { + return this.renderer.handleInteraction(interaction) + } } diff --git a/src.new/node.ts b/src.new/node.ts index b38b461..8f356a9 100644 --- a/src.new/node.ts +++ b/src.new/node.ts @@ -1,3 +1,4 @@ export abstract class Node { abstract get name(): string + abstract props: Record } diff --git a/src.new/reconciler.ts b/src.new/reconciler.ts index 7e40f3a..6bd8ae9 100644 --- a/src.new/reconciler.ts +++ b/src.new/reconciler.ts @@ -3,20 +3,20 @@ import ReactReconciler from "react-reconciler" import { raise } from "../src/helpers/raise.js" import { ButtonNode } from "./components/button.js" import type { Node } from "./node.js" -import type { RootNode } from "./root-node.js" +import type { Renderer } from "./renderer.js" import { TextNode } from "./text-node.js" const config: HostConfig< string, // Type, Record, // Props, - RootNode, // Container, + Renderer, // Container, Node, // Instance, TextNode, // TextInstance, never, // SuspenseInstance, never, // HydratableInstance, never, // PublicInstance, {}, // HostContext, - never, // UpdatePayload, + true, // UpdatePayload, never, // ChildSet, number, // TimeoutHandle, number // NoTimeout, @@ -41,27 +41,29 @@ const config: HostConfig< createTextInstance: (text) => new TextNode(text), shouldSetTextContent: () => false, - clearContainer: (root) => { - root.clear() + clearContainer: (renderer) => { + renderer.clear() }, - appendChildToContainer: (root, child) => { - root.add(child) + appendChildToContainer: (renderer, child) => { + renderer.add(child) }, - removeChildFromContainer: (root, child) => { - root.remove(child) + removeChildFromContainer: (renderer, child) => { + renderer.remove(child) }, // eslint-disable-next-line unicorn/no-null - prepareUpdate: () => null, - commitUpdate: () => {}, + prepareUpdate: () => true, + commitUpdate: (node, payload, type, oldProps, newProps) => { + node.props = newProps + }, commitTextUpdate: (node, oldText, newText) => { node.text = newText }, // eslint-disable-next-line unicorn/no-null prepareForCommit: () => null, - resetAfterCommit: (root) => { - root.render() + resetAfterCommit: (renderer) => { + renderer.render() }, preparePortalMount: () => raise("Portals are not supported"), diff --git a/src.new/renderer.ts b/src.new/renderer.ts new file mode 100644 index 0000000..4bfd937 --- /dev/null +++ b/src.new/renderer.ts @@ -0,0 +1,94 @@ +import type { + CommandInteraction, + MessageComponentInteraction, + MessageOptions, +} from "discord.js" +import { MessageActionRow } from "discord.js" +import { last } from "../src/helpers/last.js" +import { toUpper } from "../src/helpers/to-upper.js" +import { ButtonNode } from "./components/button.js" +import type { Node } from "./node.js" +import { TextNode } from "./text-node.js" + +export class Renderer { + private nodes = new Set() + private componentInteraction?: MessageComponentInteraction + + constructor(private interaction: CommandInteraction) {} + + add(child: Node | TextNode) { + this.nodes.add(child) + } + + remove(child: Node | TextNode) { + this.nodes.delete(child) + } + + clear() { + this.nodes.clear() + } + + render() { + const options = this.getMessageOptions() + if (this.componentInteraction) { + this.componentInteraction.update(options).catch(console.error) + this.componentInteraction = undefined + } else if (this.interaction.replied) { + this.interaction.editReply(options).catch(console.error) + } else { + this.interaction.reply(options).catch(console.error) + } + } + + handleInteraction(interaction: MessageComponentInteraction) { + if (interaction.isButton()) { + this.componentInteraction = interaction + this.getButtonCallback(interaction.customId)?.(interaction) + return true + } + + return false + } + + private getMessageOptions(): MessageOptions { + let content = "" + let components: MessageActionRow[] = [] + + for (const child of this.nodes) { + if (child instanceof TextNode) { + content += child.text + } + + if (child instanceof ButtonNode) { + let actionRow = last(components) + if ( + !actionRow || + actionRow.components.length >= 5 || + actionRow.components[0]?.type === "SELECT_MENU" + ) { + actionRow = new MessageActionRow() + components.push(actionRow) + } + + actionRow.addComponents({ + type: "BUTTON", + customId: child.customId, + style: toUpper(child.props.style ?? "secondary"), + disabled: child.props.disabled, + emoji: child.props.emoji, + label: child.props.label, + }) + } + } + + return { content, components } + } + + private getButtonCallback(customId: string) { + for (const child of this.nodes) { + if (child instanceof ButtonNode && child.customId === customId) { + return child.props.onClick + } + } + } +} diff --git a/src.new/root-node.ts b/src.new/root-node.ts deleted file mode 100644 index 84e9560..0000000 --- a/src.new/root-node.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { CommandInteraction, MessageOptions } from "discord.js" -import { MessageActionRow } from "discord.js" -import { nanoid } from "nanoid" -import { last } from "../src/helpers/last.js" -import { toUpper } from "../src/helpers/to-upper.js" -import { ButtonNode } from "./components/button.js" -import { Node } from "./node.js" -import { TextNode } from "./text-node.js" - -export class RootNode extends Node { - readonly name = "root" - private children = new Set() - - constructor(private interaction: CommandInteraction) { - super() - } - - add(child: Node) { - this.children.add(child) - } - - clear() { - this.children.clear() - } - - remove(child: Node) { - this.children.delete(child) - } - - render() { - this.interaction.reply(this.getMessageOptions()).catch(console.error) - } - - private getMessageOptions(): MessageOptions { - let content = "" - let components: MessageActionRow[] = [] - - for (const child of this.children) { - if (child instanceof TextNode) { - content += child.text - } - - if (child instanceof ButtonNode) { - let actionRow = last(components) - if ( - !actionRow || - actionRow.components.length >= 5 || - actionRow.components[0]?.type === "SELECT_MENU" - ) { - actionRow = new MessageActionRow() - components.push(actionRow) - } - - actionRow.addComponents({ - type: "BUTTON", - customId: nanoid(), - style: toUpper(child.props.style ?? "secondary"), - disabled: child.props.disabled, - emoji: child.props.emoji, - label: child.props.label, - }) - } - } - - return { content, components } - } -} diff --git a/src.new/text-node.ts b/src.new/text-node.ts index 06bb000..9548d40 100644 --- a/src.new/text-node.ts +++ b/src.new/text-node.ts @@ -1,8 +1,4 @@ -import { Node } from "./node.js" - -export class TextNode extends Node { +export class TextNode { readonly name = "text" - constructor(public text: string) { - super() - } + constructor(public text: string) {} }