From 9a96da1d3472a80b9bcc8e299fc968e234c97070 Mon Sep 17 00:00:00 2001 From: itsMapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Mon, 25 Jul 2022 10:47:12 -0500 Subject: [PATCH] simplify node structure + convert to message payload in core --- packages/reacord/library.new/button.tsx | 30 +++----- .../library.new/make-message-payload.ts | 76 +++++++++++++++++++ packages/reacord/library.new/node.ts | 51 +++++++------ .../reacord/library.new/reacord-discord-js.ts | 53 ++----------- .../reacord/library.new/reacord-element.ts | 12 ++- .../library.new/reacord-instance-pool.ts | 10 ++- packages/reacord/library.new/reconciler.ts | 17 +++-- packages/reacord/package.json | 1 + .../reacord/scripts/discordjs-manual-test.tsx | 30 +++++--- pnpm-lock.yaml | 3 +- 10 files changed, 164 insertions(+), 119 deletions(-) create mode 100644 packages/reacord/library.new/make-message-payload.ts diff --git a/packages/reacord/library.new/button.tsx b/packages/reacord/library.new/button.tsx index f56cfe0..445735a 100644 --- a/packages/reacord/library.new/button.tsx +++ b/packages/reacord/library.new/button.tsx @@ -1,10 +1,10 @@ import { randomUUID } from "node:crypto" -import React from "react" +import React, { useState } from "react" +import type { Except } from "type-fest" import type { ButtonSharedProps } from "./button-shared-props" import type { ComponentEvent } from "./component-event" -import { Container } from "./container" -import type { Node, NodeContainer } from "./node" -import { TextNode } from "./node" +import type { NodeBase } from "./node" +import { makeNode } from "./node" import { ReacordElement } from "./reacord-element" /** @@ -28,24 +28,16 @@ export type ButtonProps = ButtonSharedProps & { */ export type ButtonClickEvent = ComponentEvent +export type ButtonNode = NodeBase< + "button", + Except & { customId: string } +> + export function Button({ label, ...props }: ButtonProps) { + const [customId] = useState(() => randomUUID()) return ( - new ButtonNode(props)} nodeProps={props}> + {label} ) } - -export class ButtonNode implements Node { - readonly children: NodeContainer = new Container() - readonly customId = randomUUID() - - constructor(readonly props: ButtonProps) {} - - get label(): string { - return this.children - .getItems() - .map((child) => (child instanceof TextNode ? child.props.text : "")) - .join("") - } -} diff --git a/packages/reacord/library.new/make-message-payload.ts b/packages/reacord/library.new/make-message-payload.ts new file mode 100644 index 0000000..5b7a7e8 --- /dev/null +++ b/packages/reacord/library.new/make-message-payload.ts @@ -0,0 +1,76 @@ +import type { + APIActionRowComponent, + APIButtonComponent, + RESTPostAPIChannelMessageJSONBody, +} from "discord-api-types/v10" +import { ButtonStyle, ComponentType } from "discord-api-types/v10" +import type { ButtonProps } from "./button" +import type { Node } from "./node" + +export type MessagePayload = RESTPostAPIChannelMessageJSONBody + +export function makeMessagePayload(tree: readonly Node[]) { + const payload: MessagePayload = {} + + const content = tree + .map((item) => (item.type === "text" ? item.props.text : "")) + .join("") + if (content) { + payload.content = content + } + + const actionRows = makeActionRows(tree) + if (actionRows.length > 0) { + payload.components = actionRows + } + + return payload +} + +function makeActionRows(tree: readonly Node[]) { + const actionRows: Array> = [] + + for (const node of tree) { + if (node.type === "button") { + let currentRow = actionRows[actionRows.length - 1] + if (!currentRow || currentRow.components.length === 5) { + currentRow = { + type: ComponentType.ActionRow, + components: [], + } + actionRows.push(currentRow) + } + + currentRow.components.push({ + type: ComponentType.Button, + custom_id: node.props.customId, + label: extractText(node.children.getItems()), + emoji: { name: node.props.emoji }, + style: translateButtonStyle(node.props.style ?? "secondary"), + disabled: node.props.disabled, + }) + } + } + + return actionRows +} + +function extractText(tree: readonly Node[]): string { + return tree + .map((item) => { + return item.type === "text" + ? item.props.text + : extractText(item.children.getItems()) + }) + .join("") +} + +function translateButtonStyle(style: NonNullable) { + const styleMap = { + primary: ButtonStyle.Primary, + secondary: ButtonStyle.Secondary, + danger: ButtonStyle.Danger, + success: ButtonStyle.Success, + } as const + return styleMap[style] +} diff --git a/packages/reacord/library.new/node.ts b/packages/reacord/library.new/node.ts index 138ade0..47c5b1e 100644 --- a/packages/reacord/library.new/node.ts +++ b/packages/reacord/library.new/node.ts @@ -1,32 +1,37 @@ -import type { Container } from "./container" +import type { ButtonNode } from "./button" +import { Container } from "./container" -export type NodeContainer = Container> - -export type Node = { - props?: Props - children?: NodeContainer +export type NodeBase = { + type: Type + props: Props + children: Container } -export class TextNode implements Node<{ text: string }> { - props: { text: string } - constructor(text: string) { - this.props = { text } - } -} +export type Node = TextNode | ButtonNode | ActionRowNode -export class NodeDefinition { - static parse(value: unknown): NodeDefinition { - if (value instanceof NodeDefinition) { - return value +export type TextNode = NodeBase<"text", { text: string }> + +export type ActionRowNode = NodeBase<"actionRow", {}> + +export const makeNode = ( + type: Type, + props: Extract["props"], +) => + ({ type, props, children: new Container() } as Extract) + +/** A wrapper for ensuring we're actually working with a real node + * inside the React reconciler + */ +export class NodeRef { + constructor(public readonly node: Node) {} + + static unwrap(maybeNodeRef: unknown): Node { + if (maybeNodeRef instanceof NodeRef) { + return maybeNodeRef.node } - const received = value as Object | null | undefined + const received = maybeNodeRef as Object | null | undefined throw new TypeError( - `Expected ${NodeDefinition.name}, received instance of ${received?.constructor.name}`, + `Expected ${NodeRef.name}, received instance of "${received?.constructor.name}"`, ) } - - constructor( - public readonly create: () => Node, - public readonly props: Props, - ) {} } diff --git a/packages/reacord/library.new/reacord-discord-js.ts b/packages/reacord/library.new/reacord-discord-js.ts index 6ae48d4..ea62364 100644 --- a/packages/reacord/library.new/reacord-discord-js.ts +++ b/packages/reacord/library.new/reacord-discord-js.ts @@ -1,18 +1,9 @@ -import type { - Client, - Interaction, - Message, - MessageEditOptions, - MessageOptions, - TextBasedChannel, -} from "discord.js" -import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js" +import type { Client, Interaction, Message, TextBasedChannel } from "discord.js" +import { ButtonStyle } from "discord.js" import type { ReactNode } from "react" import { AsyncQueue } from "./async-queue" import type { ButtonProps } from "./button" -import { ButtonNode } from "./button" -import type { Node } from "./node" -import { TextNode } from "./node" +import type { MessagePayload as MessagePayloadType } from "./make-message-payload" import type { ReacordMessageRenderer, ReacordOptions, @@ -47,51 +38,19 @@ class ChannelMessageRenderer implements ReacordMessageRenderer { private readonly channelId: string, ) {} - update(nodes: ReadonlyArray>) { - const rows: Array> = [] - - for (const node of nodes) { - if (node instanceof ButtonNode) { - let currentRow = rows[rows.length - 1] - if (!currentRow || currentRow.components.length === 5) { - currentRow = new ActionRowBuilder() - rows.push(currentRow) - } - - currentRow.addComponents( - new ButtonBuilder({ - label: node.label, - customId: node.customId, - emoji: node.props.emoji, - disabled: node.props.disabled, - style: node.props.style - ? getButtonStyle(node.props.style) - : ButtonStyle.Secondary, - }), - ) - } - } - - const options: MessageOptions & MessageEditOptions = { - content: nodes - .map((node) => (node instanceof TextNode ? node.props.text : "")) - .join(""), - - components: rows.length > 0 ? rows : undefined, - } - + update({ content, embeds, components }: MessagePayloadType) { return this.queue.add(async () => { if (!this.active) { return } if (this.message) { - await this.message.edit(options) + await this.message.edit({ content, embeds, components }) return } const channel = await this.getChannel() - this.message = await channel.send(options) + this.message = await channel.send({ content, embeds, components }) }) } diff --git a/packages/reacord/library.new/reacord-element.ts b/packages/reacord/library.new/reacord-element.ts index 83bcb5d..00b1473 100644 --- a/packages/reacord/library.new/reacord-element.ts +++ b/packages/reacord/library.new/reacord-element.ts @@ -1,20 +1,18 @@ import type { ReactNode } from "react" import React from "react" import type { Node } from "./node" -import { NodeDefinition } from "./node" +import { NodeRef } from "./node" -export function ReacordElement({ +export function ReacordElement({ + node, children, - createNode, - nodeProps, }: { - createNode: () => Node - nodeProps: Props + node: Node children?: ReactNode }) { return React.createElement( "reacord-element", - { definition: new NodeDefinition(createNode, nodeProps) }, + { node: new NodeRef(node) }, children, ) } diff --git a/packages/reacord/library.new/reacord-instance-pool.ts b/packages/reacord/library.new/reacord-instance-pool.ts index 1d5df7f..4cffe77 100644 --- a/packages/reacord/library.new/reacord-instance-pool.ts +++ b/packages/reacord/library.new/reacord-instance-pool.ts @@ -1,6 +1,8 @@ import type { ReactNode } from "react" import { Container } from "./container" -import type { Node, NodeContainer } from "./node" +import type { MessagePayload } from "./make-message-payload" +import { makeMessagePayload } from "./make-message-payload" +import type { Node } from "./node" import { reconciler } from "./reconciler" export type ReacordOptions = { @@ -31,7 +33,7 @@ export type ReacordInstanceOptions = { } export type ReacordMessageRenderer = { - update: (nodes: ReadonlyArray>) => Promise + update: (payload: MessagePayload) => Promise deactivate: () => Promise destroy: () => Promise } @@ -45,11 +47,11 @@ export class ReacordInstancePool { } create({ initialContent, renderer }: ReacordInstanceOptions) { - const nodes: NodeContainer = new Container() + const nodes = new Container() const render = async () => { try { - await renderer.update(nodes.getItems()) + await renderer.update(makeMessagePayload(nodes.getItems())) } catch (error) { console.error("Failed to update message.", error) } diff --git a/packages/reacord/library.new/reconciler.ts b/packages/reacord/library.new/reconciler.ts index 212db41..597fa81 100644 --- a/packages/reacord/library.new/reconciler.ts +++ b/packages/reacord/library.new/reconciler.ts @@ -1,13 +1,14 @@ import ReactReconciler from "react-reconciler" import { DefaultEventPriority } from "react-reconciler/constants" -import type { Node, NodeContainer } from "./node" -import { NodeDefinition, TextNode } from "./node" +import type { Container } from "./container" +import type { Node, TextNode } from "./node" +import { makeNode, NodeRef } from "./node" export const reconciler = ReactReconciler< string, // Type - { definition?: unknown }, // Props - { nodes: NodeContainer; render: () => void }, // Container - Node, // Instance + { node?: unknown }, // Props + { nodes: Container; render: () => void }, // Container + Node, // Instance TextNode, // TextInstance never, // SuspenseInstance never, // HydratableInstance @@ -27,11 +28,11 @@ export const reconciler = ReactReconciler< noTimeout: -1, createInstance(type, props) { - return NodeDefinition.parse(props.definition).create() + return NodeRef.unwrap(props.node) }, createTextInstance(text) { - return new TextNode(text) + return makeNode("text", { text }) }, appendInitialChild(parent, child) { @@ -71,7 +72,7 @@ export const reconciler = ReactReconciler< }, commitUpdate(node, updatePayload, type, prevProps, nextProps) { - node.props = NodeDefinition.parse(nextProps.definition).props + node.props = NodeRef.unwrap(nextProps.node).props }, prepareForCommit() { diff --git a/packages/reacord/package.json b/packages/reacord/package.json index c8c14fa..bb722c5 100644 --- a/packages/reacord/package.json +++ b/packages/reacord/package.json @@ -48,6 +48,7 @@ "@types/node": "*", "@types/react": "*", "@types/react-reconciler": "^0.28.0", + "discord-api-types": "^0.36.3", "react-reconciler": "^0.29.0", "rxjs": "^7.5.6" }, diff --git a/packages/reacord/scripts/discordjs-manual-test.tsx b/packages/reacord/scripts/discordjs-manual-test.tsx index 133cbb5..4e922b2 100644 --- a/packages/reacord/scripts/discordjs-manual-test.tsx +++ b/packages/reacord/scripts/discordjs-manual-test.tsx @@ -41,16 +41,26 @@ const createTest = async ( await block(channel) } -await createTest("components", "test 'dem buttons", async (channel) => { - reacord.send( - channel.id, - <> - {Array.from({ length: 6 }, (_, i) => ( -