From 765c6fadbb30ab3c3166c7b71d1d9c72bd5a02fc Mon Sep 17 00:00:00 2001 From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Wed, 22 Dec 2021 10:35:55 -0600 Subject: [PATCH] refactor and simplify things --- integration/rendering.test.tsx | 13 +- jest.config.js | 1 + package.json | 2 +- src/action-row.tsx | 45 ------ src/base-instance.ts | 25 --- src/button.tsx | 77 ---------- src/components/action-row.tsx | 13 ++ src/components/button.tsx | 21 +++ src/components/embed-field.tsx | 17 +++ src/components/embed.tsx | 32 ++++ src/components/text.tsx | 14 ++ src/container-instance.ts | 36 ----- src/embed-field.tsx | 34 ----- src/embed.tsx | 85 ----------- src/helpers/pick.ts | 1 + src/helpers/types.ts | 1 + src/helpers/wait-for-with-timeout.ts | 1 + src/helpers/with-logged-method-calls.ts | 1 + src/jsx.d.ts | 17 ++- src/main.ts | 10 +- src/node-tree.ts | 192 ++++++++++++++++++++++++ src/reconciler.ts | 65 ++++---- src/{container.ts => renderer.ts} | 26 +--- src/root.ts | 4 +- src/text-instance.ts | 23 --- src/text.tsx | 36 ----- 26 files changed, 351 insertions(+), 441 deletions(-) delete mode 100644 src/action-row.tsx delete mode 100644 src/base-instance.ts delete mode 100644 src/button.tsx create mode 100644 src/components/action-row.tsx create mode 100644 src/components/button.tsx create mode 100644 src/components/embed-field.tsx create mode 100644 src/components/embed.tsx create mode 100644 src/components/text.tsx delete mode 100644 src/container-instance.ts delete mode 100644 src/embed-field.tsx delete mode 100644 src/embed.tsx create mode 100644 src/node-tree.ts rename src/{container.ts => renderer.ts} (78%) delete mode 100644 src/text-instance.ts delete mode 100644 src/text.tsx diff --git a/integration/rendering.test.tsx b/integration/rendering.test.tsx index 521c3c5..652227e 100644 --- a/integration/rendering.test.tsx +++ b/integration/rendering.test.tsx @@ -74,14 +74,7 @@ test("empty embed fallback", async () => { test("embed with only author", async () => { await root.render() - await assertMessages([ - { embeds: [{ description: "_ _", author: { name: "only author" } }] }, - ]) -}) - -test("empty embed author", async () => { - await root.render() - await assertMessages([{ embeds: [{ description: "_ _" }] }]) + await assertMessages([{ embeds: [{ author: { name: "only author" } }] }]) }) test("kitchen sink", async () => { @@ -252,7 +245,7 @@ async function assertMessages(expected: MessageOptions[]) { } function extractMessageData(message: Message): MessageOptions { - return { + return pruneUndefinedValues({ content: nonEmptyOrUndefined(message.content), embeds: nonEmptyOrUndefined( pruneUndefinedValues( @@ -305,7 +298,7 @@ function extractMessageData(message: Message): MessageOptions { }), })), ), - } + }) } function pruneUndefinedValues(input: T) { diff --git a/jest.config.js b/jest.config.js index f6800c2..f84ab63 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,4 +16,5 @@ const config = { verbose: true, } +// eslint-disable-next-line import/no-unused-modules export default config diff --git a/package.json b/package.json index b481c59..20ffe5a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "lint": "eslint --ext js,ts,tsx .", "lint-fix": "pnpm lint -- --fix", "format": "prettier --write .", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js", "test-watch": "pnpm test -- --watch", "coverage": "pnpm test -- --coverage", "typecheck": "tsc --noEmit" diff --git a/src/action-row.tsx b/src/action-row.tsx deleted file mode 100644 index b82be05..0000000 --- a/src/action-row.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { - MessageActionRowComponentOptions, - MessageOptions, -} from "discord.js" -import React from "react" -import { ContainerInstance } from "./container-instance.js" - -export type ActionRowProps = { - children: React.ReactNode -} - -export function ActionRow(props: ActionRowProps) { - return ( - new ActionRowInstance()}> - {props.children} - - ) -} - -class ActionRowInstance extends ContainerInstance { - readonly name = "ActionRow" - - constructor() { - super({ warnOnNonTextChildren: false }) - } - - // eslint-disable-next-line class-methods-use-this - override renderToMessage(options: MessageOptions) { - const row = { - type: "ACTION_ROW" as const, - components: [] as MessageActionRowComponentOptions[], - } - - for (const child of this.children) { - if (!child.renderToActionRow) { - console.warn(`${child.name} is not an action row component`) - continue - } - child.renderToActionRow(row) - } - - options.components ??= [] - options.components.push(row) - } -} diff --git a/src/base-instance.ts b/src/base-instance.ts deleted file mode 100644 index 10187da..0000000 --- a/src/base-instance.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { - MessageActionRowOptions, - MessageEmbedOptions, - MessageOptions, -} from "discord.js" - -export abstract class BaseInstance { - /** The name of the JSX element represented by this instance */ - abstract readonly name: string - - /** If the element represents text, the text for this element */ - getText?(): string - - /** If this element can be a child of a message, - * the function to modify the message options */ - renderToMessage?(options: MessageOptions): void - - /** If this element can be a child of an embed, - * the function to modify the embed options */ - renderToEmbed?(options: MessageEmbedOptions): void - - /** If this element can be a child of an action row, - * the function to modify the action row options */ - renderToActionRow?(options: MessageActionRowOptions): void -} diff --git a/src/button.tsx b/src/button.tsx deleted file mode 100644 index 04ac77b..0000000 --- a/src/button.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { - BaseMessageComponentOptions, - EmojiResolvable, - MessageActionRowOptions, - MessageButtonOptions, - MessageButtonStyle, - MessageOptions, -} from "discord.js" -import { nanoid } from "nanoid" -import React from "react" -import { ContainerInstance } from "./container-instance.js" -import { last } from "./helpers/last.js" -import { pick } from "./helpers/pick.js" -import { toUpper } from "./helpers/to-upper.js" - -export type ButtonStyle = Exclude, "link"> - -export type ButtonProps = { - style?: ButtonStyle - emoji?: EmojiResolvable - disabled?: boolean - children?: React.ReactNode -} - -export function Button(props: ButtonProps) { - return ( - new ButtonInstance(props)}> - {props.children} - - ) -} - -class ButtonInstance extends ContainerInstance { - readonly name = "Button" - - constructor(private readonly props: ButtonProps) { - super({ warnOnNonTextChildren: true }) - } - - private getButtonOptions(): Required & - MessageButtonOptions { - return { - ...pick(this.props, "emoji", "disabled"), - type: "BUTTON", - style: this.props.style ? toUpper(this.props.style) : "SECONDARY", - label: this.getChildrenText(), - customId: nanoid(), - } - } - - override renderToMessage(options: MessageOptions) { - options.components ??= [] - - // i hate this - let actionRow: - | (Required & MessageActionRowOptions) - | undefined = last(options.components) - - if ( - !actionRow || - actionRow.components[0]?.type === "SELECT_MENU" || - actionRow.components.length >= 5 - ) { - actionRow = { - type: "ACTION_ROW", - components: [], - } - options.components.push(actionRow) - } - - actionRow.components.push(this.getButtonOptions()) - } - - override renderToActionRow(row: MessageActionRowOptions) { - row.components.push(this.getButtonOptions()) - } -} diff --git a/src/components/action-row.tsx b/src/components/action-row.tsx new file mode 100644 index 0000000..c68c5ff --- /dev/null +++ b/src/components/action-row.tsx @@ -0,0 +1,13 @@ +import React from "react" + +export type ActionRowProps = { + children: React.ReactNode +} + +export function ActionRow(props: ActionRowProps) { + return ( + ({ type: "actionRow", children: [] })}> + {props.children} + + ) +} diff --git a/src/components/button.tsx b/src/components/button.tsx new file mode 100644 index 0000000..7f5dac4 --- /dev/null +++ b/src/components/button.tsx @@ -0,0 +1,21 @@ +import type { EmojiResolvable, MessageButtonStyle } from "discord.js" +import React from "react" + +export type ButtonStyle = Exclude, "link"> + +export type ButtonProps = { + style?: ButtonStyle + emoji?: EmojiResolvable + disabled?: boolean + children?: React.ReactNode +} + +export function Button(props: ButtonProps) { + return ( + ({ ...props, type: "button", children: [] })} + > + {props.children} + + ) +} diff --git a/src/components/embed-field.tsx b/src/components/embed-field.tsx new file mode 100644 index 0000000..881bf8b --- /dev/null +++ b/src/components/embed-field.tsx @@ -0,0 +1,17 @@ +import React from "react" + +export type EmbedFieldProps = { + name: string + children: React.ReactNode + inline?: boolean +} + +export function EmbedField(props: EmbedFieldProps) { + return ( + ({ ...props, type: "embedField", children: [] })} + > + {props.children} + + ) +} diff --git a/src/components/embed.tsx b/src/components/embed.tsx new file mode 100644 index 0000000..51afa6e --- /dev/null +++ b/src/components/embed.tsx @@ -0,0 +1,32 @@ +import type { ColorResolvable } from "discord.js" +import type { ReactNode } from "react" +import React from "react" + +export type EmbedProps = { + title?: string + color?: ColorResolvable + url?: string + timestamp?: Date | number | string + imageUrl?: string + thumbnailUrl?: string + author?: { + name: string + url?: string + iconUrl?: string + } + footer?: { + text: string + iconUrl?: string + } + children?: ReactNode +} + +export function Embed(props: EmbedProps) { + return ( + ({ ...props, type: "embed", children: [] })} + > + {props.children} + + ) +} diff --git a/src/components/text.tsx b/src/components/text.tsx new file mode 100644 index 0000000..c46a50c --- /dev/null +++ b/src/components/text.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from "react" +import React from "react" + +export type TextProps = { + children?: ReactNode +} + +export function Text(props: TextProps) { + return ( + ({ type: "textElement", children: [] })}> + {props.children} + + ) +} diff --git a/src/container-instance.ts b/src/container-instance.ts deleted file mode 100644 index bc64ad6..0000000 --- a/src/container-instance.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BaseInstance } from "./base-instance.js" - -// eslint-disable-next-line import/no-unused-modules -export type ContainerInstanceOptions = { - /** - * Whether or not to log a warning when calling getChildrenText() with non-text children - * - * Regardless of what this is set to, non-text children will always be skipped */ - warnOnNonTextChildren: boolean -} - -export abstract class ContainerInstance extends BaseInstance { - readonly children: BaseInstance[] = [] - - constructor(private readonly options: ContainerInstanceOptions) { - super() - } - - add(child: BaseInstance) { - this.children.push(child) - } - - protected getChildrenText(): string { - let text = "" - for (const child of this.children) { - if (!child.getText) { - if (this.options.warnOnNonTextChildren) { - console.warn(`${child.name} is not a valid child of ${this.name}`) - } - continue - } - text += child.getText() - } - return text - } -} diff --git a/src/embed-field.tsx b/src/embed-field.tsx deleted file mode 100644 index df87e03..0000000 --- a/src/embed-field.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { MessageEmbedOptions } from "discord.js" -import React from "react" -import { ContainerInstance } from "./container-instance.js" -import { pick } from "./helpers/pick.js" - -export type EmbedFieldProps = { - name: string - children: React.ReactNode - inline?: boolean -} - -export function EmbedField(props: EmbedFieldProps) { - return ( - new EmbedFieldInstance(props)}> - {props.children} - - ) -} - -class EmbedFieldInstance extends ContainerInstance { - readonly name = "EmbedField" - - constructor(private readonly props: EmbedFieldProps) { - super({ warnOnNonTextChildren: true }) - } - - override renderToEmbed(options: MessageEmbedOptions) { - options.fields ??= [] - options.fields.push({ - ...pick(this.props, "name", "inline"), - value: this.getChildrenText(), - }) - } -} diff --git a/src/embed.tsx b/src/embed.tsx deleted file mode 100644 index 66e935b..0000000 --- a/src/embed.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import type { - ColorResolvable, - MessageEmbedOptions, - MessageOptions, -} from "discord.js" -import type { ReactNode } from "react" -import React from "react" -import { ContainerInstance } from "./container-instance.js" - -export type EmbedProps = { - title?: string - color?: ColorResolvable - url?: string - timestamp?: Date | number | string - imageUrl?: string - thumbnailUrl?: string - author?: { - name?: string - url?: string - iconUrl?: string - } - footer?: { - text?: string - iconUrl?: string - } - children?: ReactNode -} - -export function Embed(props: EmbedProps) { - return ( - new EmbedInstance(props)}> - {props.children} - - ) -} - -class EmbedInstance extends ContainerInstance { - readonly name = "Embed" - - constructor(readonly props: EmbedProps) { - super({ warnOnNonTextChildren: false }) - } - - override renderToMessage(message: MessageOptions) { - message.embeds ??= [] - message.embeds.push(this.getEmbedOptions()) - } - - getEmbedOptions(): MessageEmbedOptions { - const options: MessageEmbedOptions = { - ...this.props, - image: this.props.imageUrl ? { url: this.props.imageUrl } : undefined, - thumbnail: this.props.thumbnailUrl - ? { url: this.props.thumbnailUrl } - : undefined, - author: { - ...this.props.author, - iconURL: this.props.author?.iconUrl, - }, - footer: { - text: "", - ...this.props.footer, - iconURL: this.props.footer?.iconUrl, - }, - timestamp: this.props.timestamp - ? new Date(this.props.timestamp) // this _may_ need date-fns to parse this - : undefined, - } - - for (const child of this.children) { - if (!child.renderToEmbed) { - console.warn(`${child.name} is not a valid child of ${this.name}`) - continue - } - child.renderToEmbed(options) - } - - // can't render an empty embed - if (!options.description) { - options.description = "_ _" - } - - return options - } -} diff --git a/src/helpers/pick.ts b/src/helpers/pick.ts index 403bc98..e921acb 100644 --- a/src/helpers/pick.ts +++ b/src/helpers/pick.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-unused-modules export function pick( object: T, ...keys: K[] diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 5060b24..d89c1c7 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-unused-modules */ export type MaybePromise = T | Promise export type ValueOf = Type extends ReadonlyArray diff --git a/src/helpers/wait-for-with-timeout.ts b/src/helpers/wait-for-with-timeout.ts index 6a93e85..90aaeea 100644 --- a/src/helpers/wait-for-with-timeout.ts +++ b/src/helpers/wait-for-with-timeout.ts @@ -2,6 +2,7 @@ import { rejectAfter } from "./reject-after.js" import type { MaybePromise } from "./types.js" import { waitFor } from "./wait-for.js" +// eslint-disable-next-line import/no-unused-modules export function waitForWithTimeout( condition: () => MaybePromise, timeout = 1000, diff --git a/src/helpers/with-logged-method-calls.ts b/src/helpers/with-logged-method-calls.ts index e99eb72..d7e086a 100644 --- a/src/helpers/with-logged-method-calls.ts +++ b/src/helpers/with-logged-method-calls.ts @@ -1,5 +1,6 @@ import { inspect } from "node:util" +// eslint-disable-next-line import/no-unused-modules export function withLoggedMethodCalls(value: T) { return new Proxy(value as Record, { get(target, property) { diff --git a/src/jsx.d.ts b/src/jsx.d.ts index eb41a3b..2311575 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -1,11 +1,14 @@ -declare namespace JSX { - import type { ReactNode } from "react" +import type { ReactNode } from "react" +import type { Node } from "./node-tree" - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions - interface IntrinsicElements { - "reacord-element": { - createInstance: () => unknown - children?: ReactNode +declare global { + namespace JSX { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface IntrinsicElements { + "reacord-element": { + createNode: () => Node + children?: ReactNode + } } } } diff --git a/src/main.ts b/src/main.ts index e9e235c..0e277d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-unused-modules */ -export * from "./action-row.js" -export * from "./button.js" -export * from "./embed-field.js" -export * from "./embed.js" +export * from "./components/action-row.js" +export * from "./components/button.js" +export * from "./components/embed-field.js" +export * from "./components/embed.js" +export * from "./components/text.js" export * from "./root.js" -export * from "./text.js" diff --git a/src/node-tree.ts b/src/node-tree.ts new file mode 100644 index 0000000..a7802cf --- /dev/null +++ b/src/node-tree.ts @@ -0,0 +1,192 @@ +import type { + BaseMessageComponentOptions, + ColorResolvable, + EmojiResolvable, + MessageActionRowOptions, + MessageEmbedOptions, + MessageOptions, +} from "discord.js" +import { nanoid } from "nanoid" +import type { ButtonStyle } from "./components/button.js" +import { last } from "./helpers/last.js" +import { toUpper } from "./helpers/to-upper.js" + +export type MessageNode = { + type: "message" + children: Node[] +} + +export type TextNode = { + type: "text" + text: string +} + +type TextElementNode = { + type: "textElement" + children: Node[] +} + +type EmbedNode = { + type: "embed" + title?: string + color?: ColorResolvable + url?: string + timestamp?: Date | number | string + imageUrl?: string + thumbnailUrl?: string + author?: { + name: string + url?: string + iconUrl?: string + } + footer?: { + text: string + iconUrl?: string + } + children: Node[] +} + +type EmbedFieldNode = { + type: "embedField" + name: string + inline?: boolean + children: Node[] +} + +type ActionRowNode = { + type: "actionRow" + children: Node[] +} + +type ButtonNode = { + type: "button" + style?: ButtonStyle + emoji?: EmojiResolvable + disabled?: boolean + children: Node[] +} + +export type Node = + | MessageNode + | TextNode + | TextElementNode + | EmbedNode + | EmbedFieldNode + | ActionRowNode + | ButtonNode + +export function getMessageOptions(node: MessageNode): MessageOptions { + if (node.children.length === 0) { + // can't send an empty message + return { content: "_ _" } + } + + const options: MessageOptions = {} + + for (const child of node.children) { + if (child.type === "text" || child.type === "textElement") { + options.content = `${options.content ?? ""}${getNodeText(child)}` + } + + if (child.type === "embed") { + options.embeds ??= [] + options.embeds.push(getEmbedOptions(child)) + } + + if (child.type === "actionRow") { + options.components ??= [] + options.components.push({ + type: "ACTION_ROW", + components: [], + }) + addActionRowItems(options.components, child.children) + } + + if (child.type === "button") { + options.components ??= [] + addActionRowItems(options.components, [child]) + } + } + + if (!options.content && !options.embeds?.length) { + options.content = "_ _" + } + + return options +} + +function getNodeText(node: Node): string | undefined { + if (node.type === "text") { + return node.text + } + if (node.type === "textElement") { + return node.children.map(getNodeText).join("") + } +} + +function getEmbedOptions(node: EmbedNode) { + const options: MessageEmbedOptions = { + title: node.title, + color: node.color, + url: node.url, + timestamp: node.timestamp ? new Date(node.timestamp) : undefined, + image: { url: node.imageUrl }, + thumbnail: { url: node.thumbnailUrl }, + description: node.children.map(getNodeText).join(""), + author: node.author + ? { ...node.author, iconURL: node.author.iconUrl } + : undefined, + footer: node.footer + ? { text: node.footer.text, iconURL: node.footer.iconUrl } + : undefined, + } + + for (const child of node.children) { + if (child.type === "embedField") { + options.fields ??= [] + options.fields.push({ + name: child.name, + value: child.children.map(getNodeText).join("") || "_ _", + inline: child.inline, + }) + } + } + + if (!options.description && !options.author) { + options.description = "_ _" + } + + return options +} + +type ActionRowOptions = Required & + MessageActionRowOptions + +function addActionRowItems(components: ActionRowOptions[], nodes: Node[]) { + let actionRow = last(components) + + if ( + actionRow == undefined || + actionRow.components[0]?.type === "SELECT_MENU" || + actionRow.components.length >= 5 + ) { + actionRow = { + type: "ACTION_ROW", + components: [], + } + components.push(actionRow) + } + + for (const node of nodes) { + if (node.type === "button") { + actionRow.components.push({ + type: "BUTTON", + label: node.children.map(getNodeText).join(""), + style: node.style ? toUpper(node.style) : "SECONDARY", + emoji: node.emoji, + disabled: node.disabled, + customId: nanoid(), + }) + } + } +} diff --git a/src/reconciler.ts b/src/reconciler.ts index fc68e56..cb592ed 100644 --- a/src/reconciler.ts +++ b/src/reconciler.ts @@ -1,46 +1,41 @@ /* eslint-disable unicorn/no-null */ import { inspect } from "node:util" import ReactReconciler from "react-reconciler" -import { BaseInstance } from "./base-instance.js" -import { ContainerInstance } from "./container-instance.js" -import type { ReacordContainer } from "./container.js" import { raise } from "./helpers/raise.js" -import { TextInstance } from "./text-instance.js" +import type { MessageNode, Node, TextNode } from "./node-tree.js" +import type { MessageRenderer } from "./renderer.js" type ElementTag = string type Props = Record -const createInstance = (type: string, props: Props): BaseInstance => { +const createInstance = (type: string, props: Props): Node => { if (type !== "reacord-element") { raise(`createInstance: unknown type: ${type}`) } - if (typeof props.createInstance !== "function") { - const actual = inspect(props.createInstance) - raise(`invalid createInstance function, received: ${actual}`) + if (typeof props.createNode !== "function") { + const actual = inspect(props.createNode) + raise(`invalid createNode function, received: ${actual}`) } - const instance = props.createInstance() - if (!(instance instanceof BaseInstance)) { - raise(`invalid instance: ${inspect(instance)}`) - } - - return instance + return props.createNode() } +type ChildSet = MessageNode + export const reconciler = ReactReconciler< string, // Type (jsx tag), Props, // Props, - ReacordContainer, // Container, - BaseInstance, // Instance, - TextInstance, // TextInstance, + MessageRenderer, // Container, + Node, // Instance, + TextNode, // TextInstance, never, // SuspenseInstance, never, // HydratableInstance, never, // PublicInstance, null, // HostContext, [], // UpdatePayload, - BaseInstance[], // ChildSet, + ChildSet, // ChildSet, unknown, // TimeoutHandle, unknown // NoTimeout >({ @@ -59,41 +54,37 @@ export const reconciler = ReactReconciler< createInstance, - createTextInstance: (text) => new TextInstance(text), + createTextInstance: (text) => ({ type: "text", text }), - createContainerChildSet: () => [], + createContainerChildSet: (): ChildSet => ({ + type: "message", + children: [], + }), - appendChildToContainerChildSet: ( - childSet: BaseInstance[], - child: BaseInstance, - ) => { - childSet.push(child) + appendChildToContainerChildSet: (childSet: ChildSet, child: Node) => { + childSet.children.push(child) }, - finalizeContainerChildren: ( - container: ReacordContainer, - children: BaseInstance[], - ) => false, + finalizeContainerChildren: (container: MessageRenderer, children: ChildSet) => + false, replaceContainerChildren: ( - container: ReacordContainer, - children: BaseInstance[], + container: MessageRenderer, + children: ChildSet, ) => { container.render(children) }, appendInitialChild: (parent, child) => { - if (parent instanceof ContainerInstance) { - parent.add(child) + if ("children" in parent) { + parent.children.push(child) } else { - raise( - `Cannot append child of type ${child.constructor.name} to ${parent.constructor.name}`, - ) + raise(`${parent.type} cannot have children`) } }, cloneInstance: ( - instance: BaseInstance, + instance: Node, _: unknown, type: ElementTag, oldProps: Props, diff --git a/src/container.ts b/src/renderer.ts similarity index 78% rename from src/container.ts rename to src/renderer.ts index 8302e78..3e50cb9 100644 --- a/src/container.ts +++ b/src/renderer.ts @@ -1,11 +1,12 @@ import type { Message, MessageOptions, TextBasedChannels } from "discord.js" -import type { BaseInstance } from "./base-instance.js" +import type { MessageNode } from "./node-tree.js" +import { getMessageOptions } from "./node-tree.js" type Action = | { type: "updateMessage"; options: MessageOptions } | { type: "deleteMessage" } -export class ReacordContainer { +export class MessageRenderer { private channel: TextBasedChannels private message?: Message private actions: Action[] = [] @@ -15,22 +16,11 @@ export class ReacordContainer { this.channel = channel } - render(children: BaseInstance[]) { - const options: MessageOptions = {} - for (const child of children) { - if (!child.renderToMessage) { - console.warn(`${child.name} is not a valid message child`) - continue - } - child.renderToMessage(options) - } - - // can't render an empty message - if (!options?.content && !options.embeds?.length) { - options.content = "_ _" - } - - this.addAction({ type: "updateMessage", options }) + render(node: MessageNode) { + this.addAction({ + type: "updateMessage", + options: getMessageOptions(node), + }) } destroy() { diff --git a/src/root.ts b/src/root.ts index 9043295..98d5cba 100644 --- a/src/root.ts +++ b/src/root.ts @@ -1,15 +1,15 @@ /* eslint-disable unicorn/no-null */ import type { TextBasedChannels } from "discord.js" import type { ReactNode } from "react" -import { ReacordContainer } from "./container" import { reconciler } from "./reconciler" +import { MessageRenderer } from "./renderer" export type ReacordRenderTarget = TextBasedChannels export type ReacordRoot = ReturnType export function createRoot(target: ReacordRenderTarget) { - const container = new ReacordContainer(target) + const container = new MessageRenderer(target) const containerId = reconciler.createContainer(container, 0, false, null) return { render: (content: ReactNode) => { diff --git a/src/text-instance.ts b/src/text-instance.ts deleted file mode 100644 index 1f8be1c..0000000 --- a/src/text-instance.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { MessageEmbedOptions, MessageOptions } from "discord.js" -import { BaseInstance } from "./base-instance.js" - -/** Represents raw strings in JSX */ -export class TextInstance extends BaseInstance { - readonly name = "Text" - - constructor(private readonly text: string) { - super() - } - - override getText() { - return this.text - } - - override renderToMessage(options: MessageOptions) { - options.content = (options.content ?? "") + this.getText() - } - - override renderToEmbed(options: MessageEmbedOptions) { - options.description = (options.description ?? "") + this.getText() - } -} diff --git a/src/text.tsx b/src/text.tsx deleted file mode 100644 index 89bad7d..0000000 --- a/src/text.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { MessageEmbedOptions, MessageOptions } from "discord.js" -import type { ReactNode } from "react" -import React from "react" -import { ContainerInstance } from "./container-instance.js" - -export type TextProps = { - children?: ReactNode -} - -export function Text(props: TextProps) { - return ( - new TextElementInstance()}> - {props.children} - - ) -} - -class TextElementInstance extends ContainerInstance { - readonly name = "Text" - - constructor() { - super({ warnOnNonTextChildren: true }) - } - - override getText() { - return this.getChildrenText() - } - - override renderToMessage(options: MessageOptions) { - options.content = (options.content ?? "") + this.getText() - } - - override renderToEmbed(options: MessageEmbedOptions) { - options.description = (options.description ?? "") + this.getText() - } -}