diff --git a/packages/reacord/library.new.new/core/button.tsx b/packages/reacord/library.new.new/core/button.tsx deleted file mode 100644 index 5d236e3..0000000 --- a/packages/reacord/library.new.new/core/button.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { APIMessageComponentButtonInteraction } from "discord.js" -import { randomUUID } from "node:crypto" -import type { ReactNode } from "react" -import React from "react" -import type { ComponentEvent } from "./component-event.js" -import { Node } from "./node.js" -import { ReacordElement } from "./reacord-element.js" - -export type ButtonProps = { - /** The text on the button. Rich formatting (markdown) is not supported here. */ - label?: ReactNode - - /** The text on the button. Rich formatting (markdown) is not supported here. - * If both `label` and `children` are passed, `children` will be ignored. - */ - children?: ReactNode - - /** When true, the button will be slightly faded, and cannot be clicked. */ - disabled?: boolean - - /** - * Renders an emoji to the left of the text. - * Has to be a literal emoji character (e.g. 🍍), - * or an emoji code, like `<:plus_one:778531744860602388>`. - * - * To get an emoji code, type your emoji in Discord chat - * with a backslash `\` in front. - * The bot has to be in the emoji's guild to use it. - */ - emoji?: string - - /** - * The style determines the color of the button and signals intent. - * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles - */ - style?: "primary" | "secondary" | "success" | "danger" - - /** - * Happens when a user clicks the button. - */ - onClick: (event: ButtonClickEvent) => void -} - -/** - * @category Button - */ -export type ButtonClickEvent = ComponentEvent & { - interaction: APIMessageComponentButtonInteraction -} - -export function Button({ label, children, ...props }: ButtonProps) { - return ( - new ButtonNode(props)}> - {label ?? children} - - ) -} - -export class ButtonNode extends Node { - readonly customId = randomUUID() -} diff --git a/packages/reacord/library.new.new/core/embed.tsx b/packages/reacord/library.new.new/core/embed.tsx deleted file mode 100644 index c725f26..0000000 --- a/packages/reacord/library.new.new/core/embed.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { ReactNode } from "react" -import React from "react" -import type { Except } from "type-fest" - -export type EmbedProps = { - title?: string - description?: string - url?: string - color?: number - fields?: Array<{ name: string; value: string; inline?: boolean }> - author?: { name: string; url?: string; iconUrl?: string } - thumbnail?: { url: string } - image?: { url: string } - video?: { url: string } - footer?: { text: string; iconUrl?: string } - timestamp?: string | number | Date - children?: ReactNode -} - -export function Embed({ children, ...props }: EmbedProps) { - return {children} -} - -declare global { - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions - interface ReacordHostElementMap { - "reacord-embed": Except - } -} diff --git a/packages/reacord/library.new.new/core/make-message-update-payload.ts b/packages/reacord/library.new.new/core/make-message-update-payload.ts deleted file mode 100644 index 2bc99c4..0000000 --- a/packages/reacord/library.new.new/core/make-message-update-payload.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { omit } from "@reacord/helpers/omit" -import type { - APIActionRowComponent, - APIEmbed, - APIMessageActionRowComponent, -} from "discord.js" -import type { HostElement } from "./host-element.js" - -export type MessageUpdatePayload = { - content: string - embeds: APIEmbed[] - components: Array> -} - -export function makeMessageUpdatePayload( - tree: HostElement, -): MessageUpdatePayload { - return { - content: tree.children - .map((child) => (child.type === "reacord-text" ? child.props.text : "")) - .join(""), - - embeds: tree.children.flatMap((child) => { - if (child.type !== "reacord-embed") return [] - - const embed: APIEmbed = omit(child.props, ["timestamp"]) - - if (child.props.timestamp != undefined) { - embed.timestamp = new Date(child.props.timestamp).toISOString() - } - - return embed - }), - - components: [], - } -} diff --git a/packages/reacord/library.new.new/core/node.ts b/packages/reacord/library.new.new/core/node.ts deleted file mode 100644 index 4a49d22..0000000 --- a/packages/reacord/library.new.new/core/node.ts +++ /dev/null @@ -1,45 +0,0 @@ -export class Node { - private readonly _children: Node[] = [] - - constructor(public props: Props) {} - - get children(): readonly Node[] { - return this._children - } - - 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.new.new/core/reacord-element.ts b/packages/reacord/library.new.new/core/reacord-element.ts deleted file mode 100644 index bfcd960..0000000 --- a/packages/reacord/library.new.new/core/reacord-element.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createElement } from "react" -import type { Node } from "./node.js" - -export type ReacordElementHostProps = { - factory: ReacordElementFactory -} - -export function ReacordElement(props: { - createNode: () => Node - children?: React.ReactNode -}) { - return createElement( - "reacord-element", - { factory: new ReacordElementFactory(props.createNode) }, - props.children, - ) -} - -export class ReacordElementFactory { - constructor(public readonly createNode: () => Node) {} - - static unwrap(maybeFactory: unknown): Node { - if (maybeFactory instanceof ReacordElementFactory) { - return maybeFactory.createNode() - } - const received = (maybeFactory as any)?.constructor.name - throw new TypeError( - `Expected a ${ReacordElementFactory.name}, got ${received}`, - ) - } -} diff --git a/packages/reacord/library.new.new/core/reacord-instance.ts b/packages/reacord/library.new.new/core/reacord-instance.ts deleted file mode 100644 index 9343a75..0000000 --- a/packages/reacord/library.new.new/core/reacord-instance.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ReactNode } from "react" -import type { ButtonClickEvent } from "./button.js" -import { ButtonNode } from "./button.js" -import type { HostElement } from "./host-element.js" -import { Node } from "./node.js" -import { reconciler } from "./reconciler.js" - -export type ReacordRenderer = { - updateMessage(tree: HostElement): Promise -} - -export class ReacordInstance { - readonly currentTree = new Node() - private latestTree?: Node - private readonly reconcilerContainer = reconciler.createContainer() - - constructor(private readonly renderer: ReacordRenderer) {} - - render(content?: ReactNode) { - reconciler.updateContainer(content, this.reconcilerContainer) - } - - async updateMessage(tree: Node) { - await this.renderer.updateMessage(tree) - this.latestTree = tree - } - - handleButtonInteraction(customId: string, event: ButtonClickEvent) { - if (!this.latestTree) return - for (const node of this.latestTree.walk()) { - if (node instanceof ButtonNode && node.customId === customId) { - node.props.onClick(event) - } - } - } -} diff --git a/packages/reacord/library.new.new/core/reconciler.ts b/packages/reacord/library.new.new/core/reconciler.ts deleted file mode 100644 index a4531e8..0000000 --- a/packages/reacord/library.new.new/core/reconciler.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* eslint-disable unicorn/prefer-modern-dom-apis */ -import ReactReconciler from "react-reconciler" -import { DefaultEventPriority } from "react-reconciler/constants" -import type { Node } from "./node.js" -import type { ReacordElementHostProps } from "./reacord-element.js" -import { ReacordElementFactory } from "./reacord-element.js" -import type { ReacordInstance } from "./reacord-instance.js" -import { TextNode } from "./text-node.js" - -// technically elements of any shape can go through the reconciler, -// so I'm typing this as unknown to ensure we validate the props -// before using them -type ReconcilerProps = { - [_ in keyof ReacordElementHostProps]?: unknown -} - -export const reconciler = ReactReconciler< - string, // Type - ReconcilerProps, // Props - ReacordInstance, // Container - Node, // Instance - TextNode, // TextInstance - never, // SuspenseInstance - never, // HydratableInstance - never, // PublicInstance - {}, // HostContext - true, // UpdatePayload - never, // ChildSet - NodeJS.Timeout, // TimeoutHandle - -1 // NoTimeout ->({ - isPrimaryRenderer: true, - supportsMutation: true, - supportsHydration: false, - supportsPersistence: false, - scheduleTimeout: setTimeout, - cancelTimeout: clearTimeout, - noTimeout: -1, - - createInstance(type, props) { - return ReacordElementFactory.unwrap(props.factory) - }, - - createTextInstance(text) { - return new TextNode(text) - }, - - appendInitialChild(parent, child) { - parent.add(child) - }, - - appendChild(parent, child) { - parent.add(child) - }, - - appendChildToContainer(container, child) { - container.currentTree.add(child) - }, - - insertBefore(parent, child, beforeChild) { - parent.insertBefore(child, beforeChild) - }, - - insertInContainerBefore(container, child, beforeChild) { - container.currentTree.insertBefore(child, beforeChild) - }, - - removeChild(parent, child) { - parent.remove(child) - }, - - removeChildFromContainer(container, child) { - container.currentTree.remove(child) - }, - - clearContainer(container) { - container.currentTree.clear() - }, - - commitTextUpdate(node, oldText, newText) { - node.text = newText - }, - - prepareUpdate() { - return true - }, - - commitUpdate(node, updatePayload, type, prevProps, nextProps) { - node.props = ReacordElementFactory.unwrap(nextProps.factory).props - }, - - prepareForCommit() { - // eslint-disable-next-line unicorn/no-null - return null - }, - - resetAfterCommit(container) { - container.updateMessage(container.currentTree.clone()).catch(console.error) - }, - - finalizeInitialChildren() { - return false - }, - - shouldSetTextContent() { - return false - }, - - getRootHostContext() { - return {} - }, - - getChildHostContext() { - return {} - }, - - getPublicInstance() { - throw new Error("Refs are not supported") - }, - - preparePortalMount() {}, - - getCurrentEventPriority() { - return DefaultEventPriority - }, - - getInstanceFromNode() { - return undefined - }, - - beforeActiveInstanceBlur() {}, - afterActiveInstanceBlur() {}, - prepareScopeUpdate() {}, - getInstanceFromScope() { - // eslint-disable-next-line unicorn/no-null - return null - }, - detachDeletedInstance() {}, -}) diff --git a/packages/reacord/library.new.new/core/text-node.ts b/packages/reacord/library.new.new/core/text-node.ts deleted file mode 100644 index 73002dd..0000000 --- a/packages/reacord/library.new.new/core/text-node.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Node } from "./node.js" - -export class TextNode extends Node { - constructor(public text: string) { - super() - } -} diff --git a/packages/reacord/library.new.new/djs/reacord-discord-js.ts b/packages/reacord/library.new.new/djs/reacord-discord-js.ts deleted file mode 100644 index af8ebe8..0000000 --- a/packages/reacord/library.new.new/djs/reacord-discord-js.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { - APIMessageComponentButtonInteraction, - Client, - Message, - TextChannel, -} from "discord.js" -import type { ReactNode } from "react" -import type { HostElement } from "../core/host-element.js" -import { makeMessageUpdatePayload } from "../core/make-message-update-payload.js" -import type { ReacordRenderer } from "../core/reacord-instance.js" -import { ReacordInstance } from "../core/reacord-instance.js" - -export class ReacordDiscordJs { - instances: ReacordInstance[] = [] - - constructor(private readonly client: Client) { - client.on("interactionCreate", (interaction) => { - if (!interaction.inGuild()) return - - if (interaction.isButton()) { - const json = - interaction.toJSON() as APIMessageComponentButtonInteraction - - for (const instance of this.instances) { - instance.handleButtonInteraction(interaction.customId, { - interaction: json, - reply: () => {}, - ephemeralReply: () => {}, - }) - } - } - - if (interaction.isSelectMenu()) { - // etc. - } - }) - } - - send(channelId: string, initialContent?: ReactNode) { - const instance = new ReacordInstance( - new ChannelMessageRenderer(this.client, channelId), - ) - if (initialContent !== undefined) { - instance.render(initialContent) - } - } -} - -class ChannelMessageRenderer implements ReacordRenderer { - private message?: Message - - constructor( - private readonly client: Client, - private readonly channelId: string, - ) {} - - async updateMessage(tree: HostElement) { - const payload = makeMessageUpdatePayload(tree) - - if (!this.message) { - const channel = (await this.client.channels.fetch( - this.channelId, - )) as TextChannel - this.message = await channel.send(payload) - } else { - await this.message.edit(payload) - } - } -} diff --git a/packages/reacord/library.new/core/button-shared-props.ts b/packages/reacord/library.new/core/button-shared-props.ts deleted file mode 100644 index 4ba558f..0000000 --- a/packages/reacord/library.new/core/button-shared-props.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ReactNode } from "react" - -/** - * Common props between button-like components - * @category Button - */ -export type ButtonSharedProps = { - /** The text on the button. Rich formatting (markdown) is not supported here. */ - label?: ReactNode - - /** The text on the button. Rich formatting (markdown) is not supported here. - * If both `label` and `children` are passed, `children` will be ignored. - */ - children?: ReactNode - - /** When true, the button will be slightly faded, and cannot be clicked. */ - disabled?: boolean - - /** - * Renders an emoji to the left of the text. - * Has to be a literal emoji character (e.g. 🍍), - * or an emoji code, like `<:plus_one:778531744860602388>`. - * - * To get an emoji code, type your emoji in Discord chat - * with a backslash `\` in front. - * The bot has to be in the emoji's guild to use it. - */ - emoji?: string -} diff --git a/packages/reacord/library.new/core/button.tsx b/packages/reacord/library.new/core/button.tsx deleted file mode 100644 index bc3957b..0000000 --- a/packages/reacord/library.new/core/button.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { randomUUID } from "node:crypto" -import React from "react" -import type { ButtonSharedProps } from "./button-shared-props" -import type { ComponentEvent } from "./component-event" -import { Node } from "./node" -import { ReacordElement } from "./reacord-element" - -/** - * @category Button - */ -export type ButtonProps = ButtonSharedProps & { - /** - * The style determines the color of the button and signals intent. - * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles - */ - style?: "primary" | "secondary" | "success" | "danger" - - /** - * Happens when a user clicks the button. - */ - onClick: (event: ButtonClickEvent) => void -} - -/** - * @category Button - */ -export type ButtonClickEvent = ComponentEvent - -export function Button({ label, children, ...props }: ButtonProps) { - return ( - new ButtonNode(props)} - nodeProps={props} - > - {label ?? children} - - ) -} - -export class ButtonNode extends Node { - readonly customId = randomUUID() -} diff --git a/packages/reacord/library.new/core/component-event.ts b/packages/reacord/library.new/core/component-event.ts deleted file mode 100644 index 7b10665..0000000 --- a/packages/reacord/library.new/core/component-event.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { ReactNode } from "react" -import type { ReacordInstance } from "./reacord-instance-pool" - -/** - * @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. - */ - reply(content?: ReactNode): ReacordInstance - - /** - * Create an ephemeral reply to this event, - * shown only to the user who triggered it. - */ - 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.new/core/make-message-payload.ts b/packages/reacord/library.new/core/make-message-payload.ts deleted file mode 100644 index ec57ea5..0000000 --- a/packages/reacord/library.new/core/make-message-payload.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { - APIActionRowComponent, - APIButtonComponent, - RESTPostAPIChannelMessageJSONBody, -} from "discord-api-types/v10" -import { ButtonStyle, ComponentType } from "discord-api-types/v10" -import type { ButtonProps } from "./button" -import { ButtonNode } from "./button" -import type { Node } from "./node" -import { TextNode } from "./text-node" - -export type MessageUpdatePayload = RESTPostAPIChannelMessageJSONBody - -export function makeMessageUpdatePayload(root: Node) { - const payload: MessageUpdatePayload = {} - - const content = extractText(root, 1) - if (content) { - payload.content = content - } - - const actionRows = makeActionRows(root) - if (actionRows.length > 0) { - payload.components = actionRows - } - - return payload -} - -function makeActionRows(root: Node) { - const actionRows: Array> = [] - - for (const node of root.children) { - if (node instanceof ButtonNode) { - 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.customId, - label: extractText(node, Number.POSITIVE_INFINITY), - emoji: { name: node.props.emoji }, - style: translateButtonStyle(node.props.style ?? "secondary"), - disabled: node.props.disabled, - }) - } - } - - return actionRows -} - -function extractText(node: Node, depth: number): string { - if (node instanceof TextNode) return node.props.text - if (depth <= 0) return "" - return node.children.map((child) => extractText(child, depth - 1)).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/core/node.ts b/packages/reacord/library.new/core/node.ts deleted file mode 100644 index 985abc1..0000000 --- a/packages/reacord/library.new/core/node.ts +++ /dev/null @@ -1,33 +0,0 @@ -export class Node { - private readonly _children: Node[] = [] - - constructor(public props: Props) {} - - get children(): readonly Node[] { - return this._children - } - - 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) - } - - clone(): this { - const cloned: this = new (this.constructor as any)(this.props) - cloned.add(...this.children.map((child) => child.clone())) - return cloned - } -} diff --git a/packages/reacord/library.new/core/reacord-element.ts b/packages/reacord/library.new/core/reacord-element.ts deleted file mode 100644 index dd1ac2a..0000000 --- a/packages/reacord/library.new/core/reacord-element.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ReactNode } from "react" -import { createElement } from "react" -import { inspect } from "node:util" -import type { Node } from "./node" - -export function ReacordElement({ - name, - createNode, - nodeProps, - children, -}: { - // A name representing what type of element this is, - // so that react will know if/when it needs to recreate the node, - // or just assign the props if the element name is the same on re-render - name: string - createNode: () => Node - nodeProps: NodeProps - children?: ReactNode -}) { - return createElement( - `reacord-${name}`, - { config: new ReacordElementConfig(createNode, nodeProps) }, - children, - ) -} - -export type ReacordHostElementProps = { - config: ReacordElementConfig -} - -// Any kind of element can go through the React reconciler. -// This class serves as a typesafe wrapper for creating a node -// and assigning props to an existing node. -// We can use `instanceof` to know for sure that the element is a Reacord element -export class ReacordElementConfig { - constructor(readonly create: () => Node, readonly props: Props) {} - - static parse(value: unknown): ReacordElementConfig { - if (value instanceof ReacordElementConfig) return value - const debugValue = inspect(value, { depth: 1 }) - throw new Error(`Expected ${ReacordElementConfig.name}, got ${debugValue}`) - } -} diff --git a/packages/reacord/library.new/core/reacord-instance-pool.ts b/packages/reacord/library.new/core/reacord-instance-pool.ts deleted file mode 100644 index f4d3d78..0000000 --- a/packages/reacord/library.new/core/reacord-instance-pool.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ReactNode } from "react" -import { Node } from "./node" -import { reconciler } from "./reconciler" - -export type ReacordOptions = { - /** - * The max number of active instances. - * When this limit is exceeded, the oldest instances will be disabled. - */ - maxInstances?: number -} - -export type ReacordInstance = { - /** Render some JSX to this instance (edits the message) */ - render: (content: ReactNode) => void - - /** Remove this message */ - destroy: () => void - - /** - * Same as destroy, but keeps the message and disables the components on it. - * This prevents it from listening to user interactions. - */ - deactivate: () => void -} - -export type ReacordInstanceOptions = { - initialContent: ReactNode - renderer: ReacordMessageRenderer -} - -export type ReacordMessageRenderer = { - update: (tree: Node) => Promise - deactivate: () => Promise - destroy: () => Promise -} - -export class ReacordInstancePool { - private readonly options: Required - private readonly instances = new Set() - - constructor({ maxInstances = 50 }: ReacordOptions) { - this.options = { maxInstances } - } - - create({ initialContent, renderer }: ReacordInstanceOptions) { - const root = new Node({}) - - const render = async (tree: Node) => { - try { - await renderer.update(tree) - } catch (error) { - console.error("Failed to update message.", error) - } - } - - const container = reconciler.createContainer( - { root, render }, - 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, - ) - - const instance: ReacordInstance = { - render: (content: ReactNode) => { - reconciler.updateContainer(content, container) - }, - deactivate: async () => { - this.instances.delete(instance) - try { - await renderer.deactivate() - } catch (error) { - console.error("Failed to deactivate message.", error) - } - }, - destroy: async () => { - this.instances.delete(instance) - try { - await renderer.destroy() - } catch (error) { - console.error("Failed to destroy message.", error) - } - }, - } - - if (initialContent !== undefined) { - instance.render(initialContent) - } - - if (this.instances.size > this.options.maxInstances) { - ;[...this.instances][0]?.deactivate() - } - - return instance - } -} diff --git a/packages/reacord/library.new/core/reconciler.ts b/packages/reacord/library.new/core/reconciler.ts deleted file mode 100644 index 86dc2b8..0000000 --- a/packages/reacord/library.new/core/reconciler.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* eslint-disable unicorn/prefer-modern-dom-apis */ -import ReactReconciler from "react-reconciler" -import { DefaultEventPriority } from "react-reconciler/constants" -import type { Node } from "./node" -import type { ReacordHostElementProps } from "./reacord-element" -import { ReacordElementConfig } from "./reacord-element" -import { TextNode } from "./text-node" - -// technically elements of any shape can go through the reconciler, -// so I'm typing this as unknown to ensure we validate the props -// before using them -type ReconcilerProps = { - [_ in keyof ReacordHostElementProps]?: unknown -} - -type ReconcilerContainer = { - root: Node - - // We need to pass in a render callback, so the reconciler can tell us - // when it's done modifying elements, after which we'll update - // the message in Discord - render: (root: Node) => void -} - -export const reconciler = ReactReconciler< - string, // Type - ReconcilerProps, // Props - ReconcilerContainer, // Container - Node, // Instance - TextNode, // TextInstance - never, // SuspenseInstance - never, // HydratableInstance - never, // PublicInstance - {}, // HostContext - true, // UpdatePayload - never, // ChildSet - NodeJS.Timeout, // TimeoutHandle - -1 // NoTimeout ->({ - isPrimaryRenderer: true, - supportsMutation: true, - supportsHydration: false, - supportsPersistence: false, - scheduleTimeout: setTimeout, - cancelTimeout: clearTimeout, - noTimeout: -1, - - createInstance(type, props) { - return ReacordElementConfig.parse(props.config).create() - }, - - createTextInstance(text) { - return new TextNode({ text }) - }, - - appendInitialChild(parent, child) { - parent.add(child) - }, - - appendChild(parent, child) { - parent.add(child) - }, - - appendChildToContainer(container, child) { - container.root.add(child) - }, - - insertBefore(parent, child, beforeChild) { - parent.insertBefore(child, beforeChild) - }, - - insertInContainerBefore(container, child, beforeChild) { - container.root.insertBefore(child, beforeChild) - }, - - removeChild(parent, child) { - parent.remove(child) - }, - - removeChildFromContainer(container, child) { - container.root.remove(child) - }, - - clearContainer(container) { - container.root.clear() - }, - - commitTextUpdate(node, oldText, newText) { - node.props.text = newText - }, - - commitUpdate(node, updatePayload, type, prevProps, nextProps) { - node.props = ReacordElementConfig.parse(nextProps.config).props - }, - - prepareForCommit() { - // eslint-disable-next-line unicorn/no-null - return null - }, - - resetAfterCommit(container) { - container.render(container.root.clone()) - }, - - finalizeInitialChildren() { - return false - }, - - prepareUpdate() { - return true - }, - - shouldSetTextContent() { - return false - }, - - getRootHostContext() { - return {} - }, - - getChildHostContext() { - return {} - }, - - getPublicInstance() { - throw new Error("Refs are not supported") - }, - - preparePortalMount() {}, - - getCurrentEventPriority() { - return DefaultEventPriority - }, - - getInstanceFromNode() { - return undefined - }, - - beforeActiveInstanceBlur() {}, - afterActiveInstanceBlur() {}, - prepareScopeUpdate() {}, - getInstanceFromScope() { - // eslint-disable-next-line unicorn/no-null - return null - }, - detachDeletedInstance() {}, -}) diff --git a/packages/reacord/library.new/core/text-node.ts b/packages/reacord/library.new/core/text-node.ts deleted file mode 100644 index 165a626..0000000 --- a/packages/reacord/library.new/core/text-node.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Node } from "./node" - -export class TextNode extends Node<{ text: string }> {} diff --git a/packages/reacord/library.new/djs/channel-message-renderer.ts b/packages/reacord/library.new/djs/channel-message-renderer.ts deleted file mode 100644 index 1617a31..0000000 --- a/packages/reacord/library.new/djs/channel-message-renderer.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { AsyncQueue } from "@reacord/helpers/async-queue" -import type { Client, Message, TextBasedChannel } from "discord.js" -import { makeMessageUpdatePayload } from "../core/make-message-payload.js" -import type { Node } from "../core/node.js" -import type { ReacordMessageRenderer } from "../core/reacord-instance-pool.js" - -export class ChannelMessageRenderer implements ReacordMessageRenderer { - private message: Message | undefined - private channel: TextBasedChannel | undefined - private active = true - private readonly queue = new AsyncQueue() - - constructor( - private readonly client: Client, - private readonly channelId: string, - ) {} - - update(root: Node) { - return this.queue.add(async () => { - const { content, embeds, components } = makeMessageUpdatePayload(root) - - if (!this.active) { - return - } - - if (this.message) { - await this.message.edit({ content, embeds, components }) - return - } - - const channel = await this.getChannel() - this.message = await channel.send({ content, embeds, components }) - }) - } - - destroy() { - return this.queue.add(async () => { - this.active = false - await this.message?.delete() - }) - } - - deactivate() { - return this.queue.add(async () => { - this.active = false - // TODO: disable message components - }) - } - - private async getChannel() { - if (this.channel) { - return this.channel - } - - const channel = - this.client.channels.cache.get(this.channelId) ?? - (await this.client.channels.fetch(this.channelId)) - - if (!channel) { - throw new Error(`Channel ${this.channelId} not found`) - } - if (!channel.isTextBased()) { - throw new Error(`Channel ${this.channelId} is not a text channel`) - } - return (this.channel = channel) - } -} diff --git a/packages/reacord/library.new/djs/reacord-discord-js.ts b/packages/reacord/library.new/djs/reacord-discord-js.ts deleted file mode 100644 index f6be369..0000000 --- a/packages/reacord/library.new/djs/reacord-discord-js.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Client, Interaction } from "discord.js" -import type { ReactNode } from "react" -import type { ReacordOptions } from "../core/reacord-instance-pool" -import { ReacordInstancePool } from "../core/reacord-instance-pool" -import { ChannelMessageRenderer } from "./channel-message-renderer" - -export class ReacordDiscordJs { - private instances - - constructor(private readonly client: Client, options: ReacordOptions = {}) { - this.instances = new ReacordInstancePool(options) - } - - send(channelId: string, initialContent?: ReactNode) { - const renderer = new ChannelMessageRenderer(this.client, channelId) - return this.instances.create({ initialContent, renderer }) - } - - reply(interaction: Interaction, initialContent?: ReactNode) {} - - ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {} -} diff --git a/packages/reacord/library.new/main.ts b/packages/reacord/library.new/main.ts deleted file mode 100644 index 1cc401f..0000000 --- a/packages/reacord/library.new/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { Button, type ButtonProps } from "./core/button" -export { type ButtonSharedProps } from "./core/button-shared-props" -export { - type ReacordInstance, - type ReacordOptions, -} from "./core/reacord-instance-pool" -export { ReacordDiscordJs } from "./djs/reacord-discord-js" diff --git a/packages/reacord/library/components/embed-child.ts b/packages/reacord/library/components/embed-child.ts deleted file mode 100644 index f0249b7..0000000 --- a/packages/reacord/library/components/embed-child.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Node } from "../internal/node.js" -import type { EmbedOptions } from "./embed-options" - -export abstract class EmbedChildNode extends Node { - abstract modifyEmbedOptions(options: EmbedOptions): void -} diff --git a/packages/reacord/library/components/embed-options.ts b/packages/reacord/library/components/embed-options.ts deleted file mode 100644 index 91170fd..0000000 --- a/packages/reacord/library/components/embed-options.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Except, SnakeCasedPropertiesDeep } from "type-fest" -import type { EmbedProps } from "./embed" - -export type EmbedOptions = SnakeCasedPropertiesDeep< - Except & { - timestamp?: string - } -> diff --git a/packages/reacord/library/core/component-event.ts b/packages/reacord/library/core/component-event.ts deleted file mode 100644 index c877708..0000000 --- a/packages/reacord/library/core/component-event.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ReactNode } from "react" -import type { ReacordInstance } from "./instance" - -/** - * @category Component Event - */ -export type ComponentEvent = { - /** - * Create a new reply to this event. - */ - reply(content?: ReactNode): ReacordInstance - - /** - * Create an ephemeral reply to this event, - * shown only to the user who triggered it. - */ - ephemeralReply(content?: ReactNode): ReacordInstance -} diff --git a/packages/reacord/library/core/instance.ts b/packages/reacord/library/core/instance.ts deleted file mode 100644 index d0aa740..0000000 --- a/packages/reacord/library/core/instance.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ReactNode } from "react" - -/** - * 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: ReactNode) => void - - /** Remove this message */ - destroy: () => void - - /** - * Same as destroy, but keeps the message and disables the components on it. - * This prevents it from listening to user interactions. - */ - deactivate: () => void -} diff --git a/packages/reacord/library/core/reacord.tsx b/packages/reacord/library/core/reacord.tsx deleted file mode 100644 index 1dfede6..0000000 --- a/packages/reacord/library/core/reacord.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { ReactNode } from "react" -import React 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" -import { InstanceProvider } from "./instance-context" - -/** - * @category Core - */ -export type ReacordConfig = { - /** - * The max number of active instances. - * When this limit is exceeded, the oldest instances will be disabled. - */ - maxInstances?: number -} - -/** - * The main Reacord class that other Reacord adapters should extend. - * Only use this directly if you're making [a custom adapter](/guides/custom-adapters). - */ -export abstract class Reacord { - private renderers: Renderer[] = [] - - constructor(private readonly config: ReacordConfig = {}) {} - - abstract send(...args: unknown[]): ReacordInstance - abstract reply(...args: unknown[]): ReacordInstance - abstract ephemeralReply(...args: unknown[]): ReacordInstance - - protected handleComponentInteraction(interaction: ComponentInteraction) { - for (const renderer of this.renderers) { - if (renderer.handleComponentInteraction(interaction)) return - } - } - - private get maxInstances() { - return this.config.maxInstances ?? 50 - } - - protected createInstance(renderer: Renderer, initialContent?: ReactNode) { - if (this.renderers.length > this.maxInstances) { - this.deactivate(this.renderers[0]!) - } - - this.renderers.push(renderer) - - const container = reconciler.createContainer( - renderer, - 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, - ) - - const instance: ReacordInstance = { - render: (content: ReactNode) => { - reconciler.updateContainer( - {content}, - container, - ) - }, - deactivate: () => { - this.deactivate(renderer) - }, - destroy: () => { - this.renderers = this.renderers.filter((it) => it !== renderer) - renderer.destroy() - }, - } - - if (initialContent !== undefined) { - instance.render(initialContent) - } - - return instance - } - - private deactivate(renderer: Renderer) { - this.renderers = this.renderers.filter((it) => it !== renderer) - renderer.deactivate() - } -} diff --git a/packages/reacord/library/djs/reacord-discord-js.ts b/packages/reacord/library/djs/reacord-discord-js.ts deleted file mode 100644 index a509966..0000000 --- a/packages/reacord/library/djs/reacord-discord-js.ts +++ /dev/null @@ -1,311 +0,0 @@ -/* eslint-disable class-methods-use-this */ -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 { ReacordInstance } from "../core/instance" -import type { ReacordConfig } from "../core/reacord" -import { Reacord } from "../core/reacord" -import type { ComponentInteraction } from "../internal/interaction" -import type { - Message, - MessageButtonOptions, - MessageOptions, -} from "../internal/message" -import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer" -import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer" - -/** - * The Reacord adapter for Discord.js. - * @category Core - */ -export class ReacordDiscordJs extends Reacord { - constructor(private client: Discord.Client, config: ReacordConfig = {}) { - super(config) - - client.on("interactionCreate", (interaction) => { - if (interaction.isButton() || interaction.isSelectMenu()) { - this.handleComponentInteraction( - this.createReacordComponentInteraction(interaction), - ) - } - }) - - client.ws.on( - Discord.GatewayDispatchEvents.InteractionCreate, - (data: Discord.APIInteraction) => { - if (data.type === Discord.InteractionType.MessageComponent) { - data - // this.handleComponentInteraction( - // this.createReacordComponentInteraction(data), - // ) - } - }, - ) - } - - /** - * Sends a message to a channel. - * @see https://reacord.mapleleaf.dev/guides/sending-messages - */ - override send( - channelId: string, - initialContent?: React.ReactNode, - ): ReacordInstance { - return this.createInstance( - this.createChannelRenderer(channelId), - initialContent, - ) - } - - /** - * Sends a message as a reply to a command interaction. - * @see https://reacord.mapleleaf.dev/guides/sending-messages - */ - override reply( - interaction: Discord.CommandInteraction, - initialContent?: React.ReactNode, - ): ReacordInstance { - return this.createInstance( - this.createInteractionReplyRenderer(interaction), - initialContent, - ) - } - - /** - * Sends an ephemeral message as a reply to a command interaction. - * @see https://reacord.mapleleaf.dev/guides/sending-messages - */ - override ephemeralReply( - interaction: Discord.CommandInteraction, - initialContent?: React.ReactNode, - ): ReacordInstance { - return this.createInstance( - 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.isTextBased()) { - 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 = { - 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: MessageOptions) => { - const message = await interaction.reply({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - followUp: async (options: MessageOptions) => { - 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", - event: { - ...baseProps.event, - interaction: - interaction.toJSON() as APIMessageComponentButtonInteraction, - }, - } - } - - if (interaction.isSelectMenu()) { - return { - ...baseProps, - type: "select", - event: { - ...baseProps.event, - values: interaction.values, - interaction: - interaction.toJSON() as APIMessageComponentSelectMenuInteraction, - }, - } - } - - raise(`Unsupported component interaction type: ${interaction.type}`) - } -} - -function createReacordMessage(message: Discord.Message): Message { - return { - edit: async (options) => { - await message.edit(getDiscordMessageOptions(options)) - }, - delete: async () => { - await message.delete() - }, - } -} - -function createEphemeralReacordMessage(): Message { - return { - edit: () => { - console.warn("Ephemeral messages can't be edited") - return Promise.resolve() - }, - delete: () => { - console.warn("Ephemeral messages can't be deleted") - return Promise.resolve() - }, - } -} - -function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) { - const styleMap = { - primary: Discord.ButtonStyle.Primary, - secondary: Discord.ButtonStyle.Secondary, - success: Discord.ButtonStyle.Success, - danger: Discord.ButtonStyle.Danger, - } as const - - return styleMap[style ?? "secondary"] -} - -// TODO: this could be a part of the core library, -// and also handle some edge cases, e.g. empty messages -function getDiscordMessageOptions(reacordOptions: MessageOptions) { - const options = { - // eslint-disable-next-line unicorn/no-null - content: reacordOptions.content || null, - embeds: reacordOptions.embeds, - components: reacordOptions.actionRows.map((row) => ({ - type: Discord.ComponentType.ActionRow, - components: row.map( - (component): Discord.MessageActionRowComponentData => { - if (component.type === "button") { - return { - type: Discord.ComponentType.Button, - customId: component.customId, - label: component.label ?? "", - style: convertButtonStyleToEnum(component.style), - disabled: component.disabled, - emoji: component.emoji, - } - } - - if (component.type === "select") { - return { - ...component, - type: Discord.ComponentType.SelectMenu, - options: component.options.map((option) => ({ - ...option, - default: component.values?.includes(option.value), - })), - } - } - - raise(`Unsupported component type: ${component.type}`) - }, - ), - })), - } - - if (!options.content && !options.embeds?.length) { - options.content = "_ _" - } - - return options -} diff --git a/packages/reacord/library/internal/channel.ts b/packages/reacord/library/internal/channel.ts deleted file mode 100644 index b574496..0000000 --- a/packages/reacord/library/internal/channel.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Message, MessageOptions } from "./message" - -export type Channel = { - send(message: MessageOptions): Promise -} diff --git a/packages/reacord/library/internal/container.ts b/packages/reacord/library/internal/container.ts deleted file mode 100644 index 8941fcd..0000000 --- a/packages/reacord/library/internal/container.ts +++ /dev/null @@ -1,37 +0,0 @@ -export class Container { - private items: T[] = [] - - add(...items: T[]) { - this.items.push(...items) - } - - addBefore(item: T, before: T) { - let index = this.items.indexOf(before) - if (index === -1) { - index = this.items.length - } - this.items.splice(index, 0, item) - } - - remove(toRemove: T) { - this.items = this.items.filter((item) => item !== toRemove) - } - - clear() { - this.items = [] - } - - find(predicate: (item: T) => boolean): T | undefined { - return this.items.find(predicate) - } - - findType(type: new (...args: any[]) => U): U | undefined { - for (const item of this.items) { - if (item instanceof type) return item - } - } - - [Symbol.iterator]() { - return this.items[Symbol.iterator]() - } -} diff --git a/packages/reacord/library/internal/interaction.ts b/packages/reacord/library/internal/interaction.ts deleted file mode 100644 index 06c2daf..0000000 --- a/packages/reacord/library/internal/interaction.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 = BaseInteraction<"command"> - -export type ButtonInteraction = BaseComponentInteraction< - "button", - ButtonClickEvent -> - -export type SelectInteraction = BaseComponentInteraction< - "select", - SelectChangeEvent -> - -export type BaseInteraction = { - type: Type - id: string - reply(messageOptions: MessageOptions): Promise - followUp(messageOptions: MessageOptions): Promise -} - -export type BaseComponentInteraction< - Type extends string, - Event extends ComponentEvent, -> = BaseInteraction & { - event: Event - customId: string - update(options: MessageOptions): Promise - deferUpdate(): Promise -} diff --git a/packages/reacord/library/internal/limited-collection.ts b/packages/reacord/library/internal/limited-collection.ts deleted file mode 100644 index 2150d3f..0000000 --- a/packages/reacord/library/internal/limited-collection.ts +++ /dev/null @@ -1,24 +0,0 @@ -export class LimitedCollection { - 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]() - } -} diff --git a/packages/reacord/library/internal/message.ts b/packages/reacord/library/internal/message.ts deleted file mode 100644 index 89b5e70..0000000 --- a/packages/reacord/library/internal/message.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { last } from "@reacord/helpers/last" -import type { Except } from "type-fest" -import type { EmbedOptions } from "../core/components/embed-options" -import type { SelectProps } from "../core/components/select" - -export type MessageOptions = { - content: string - embeds: EmbedOptions[] - actionRows: ActionRow[] -} - -export type ActionRow = ActionRowItem[] - -export type ActionRowItem = - | MessageButtonOptions - | MessageLinkOptions - | MessageSelectOptions - -export type MessageButtonOptions = { - type: "button" - customId: string - label?: string - style?: "primary" | "secondary" | "success" | "danger" - disabled?: boolean - emoji?: string -} - -export type MessageLinkOptions = { - type: "link" - url: string - label?: string - emoji?: string - disabled?: boolean -} - -export type MessageSelectOptions = Except & { - type: "select" - customId: string - options: MessageSelectOptionOptions[] -} - -export type MessageSelectOptionOptions = { - label: string - value: string - description?: string - emoji?: string -} - -export type Message = { - edit(options: MessageOptions): Promise - delete(): Promise -} - -export function getNextActionRow(options: MessageOptions): ActionRow { - let actionRow = last(options.actionRows) - if ( - actionRow == undefined || - actionRow.length >= 5 || - actionRow[0]?.type === "select" - ) { - actionRow = [] - options.actionRows.push(actionRow) - } - return actionRow -} diff --git a/packages/reacord/library/internal/renderers/channel-message-renderer.ts b/packages/reacord/library/internal/renderers/channel-message-renderer.ts deleted file mode 100644 index 32fafe1..0000000 --- a/packages/reacord/library/internal/renderers/channel-message-renderer.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Channel } from "../channel" -import type { Message, MessageOptions } from "../message" -import { Renderer } from "./renderer" - -export class ChannelMessageRenderer extends Renderer { - constructor(private channel: Channel) { - super() - } - - protected createMessage(options: MessageOptions): Promise { - return this.channel.send(options) - } -} diff --git a/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts b/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts deleted file mode 100644 index 163c78a..0000000 --- a/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Interaction } from "../interaction" -import type { Message, MessageOptions } from "../message" -import { Renderer } from "./renderer" - -// keep track of interaction ids which have replies, -// so we know whether to call reply() or followUp() -const repliedInteractionIds = new Set() - -export class InteractionReplyRenderer extends Renderer { - constructor(private interaction: Interaction) { - super() - } - - protected createMessage(options: MessageOptions): Promise { - if (repliedInteractionIds.has(this.interaction.id)) { - return this.interaction.followUp(options) - } - - repliedInteractionIds.add(this.interaction.id) - return this.interaction.reply(options) - } -} diff --git a/packages/reacord/library/internal/renderers/renderer.ts b/packages/reacord/library/internal/renderers/renderer.ts deleted file mode 100644 index fb9146d..0000000 --- a/packages/reacord/library/internal/renderers/renderer.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Subject } from "rxjs" -import { concatMap } from "rxjs/operators" -import { Container } from "../container.js" -import type { ComponentInteraction } from "../interaction" -import type { Message, MessageOptions } from "../message" -import type { Node } from "../node.js" - -type UpdatePayload = - | { action: "update" | "deactivate"; options: MessageOptions } - | { action: "deferUpdate"; interaction: ComponentInteraction } - | { action: "destroy" } - -export abstract class Renderer { - readonly nodes = new Container>() - private componentInteraction?: ComponentInteraction - private message?: Message - private active = true - private updates = new Subject() - - private updateSubscription = this.updates - .pipe(concatMap((payload) => this.updateMessage(payload))) - .subscribe({ error: console.error }) - - render() { - if (!this.active) { - console.warn("Attempted to update a deactivated message") - return - } - - this.updates.next({ - options: this.getMessageOptions(), - action: "update", - }) - } - - deactivate() { - this.active = false - this.updates.next({ - options: this.getMessageOptions(), - action: "deactivate", - }) - } - - destroy() { - this.active = false - this.updates.next({ action: "destroy" }) - } - - handleComponentInteraction(interaction: ComponentInteraction) { - this.componentInteraction = interaction - - setTimeout(() => { - this.updates.next({ action: "deferUpdate", interaction }) - }, 500) - - for (const node of this.nodes) { - if (node.handleComponentInteraction(interaction)) { - return true - } - } - } - - protected abstract createMessage(options: MessageOptions): Promise - - private getMessageOptions(): MessageOptions { - const options: MessageOptions = { - content: "", - embeds: [], - actionRows: [], - } - for (const node of this.nodes) { - node.modifyMessageOptions(options) - } - return options - } - - private async updateMessage(payload: UpdatePayload) { - if (payload.action === "destroy") { - this.updateSubscription.unsubscribe() - await this.message?.delete() - return - } - - if (payload.action === "deactivate") { - this.updateSubscription.unsubscribe() - - await this.message?.edit({ - ...payload.options, - actionRows: payload.options.actionRows.map((row) => - row.map((component) => ({ - ...component, - disabled: true, - })), - ), - }) - - return - } - - if (payload.action === "deferUpdate") { - await payload.interaction.deferUpdate() - return - } - - if (this.componentInteraction) { - const promise = this.componentInteraction.update(payload.options) - this.componentInteraction = undefined - await promise - return - } - - if (this.message) { - await this.message.edit(payload.options) - return - } - - this.message = await this.createMessage(payload.options) - } -} diff --git a/packages/reacord/library/internal/timeout.ts b/packages/reacord/library/internal/timeout.ts deleted file mode 100644 index c3b178c..0000000 --- a/packages/reacord/library/internal/timeout.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class Timeout { - private timeoutId?: NodeJS.Timeout - - constructor( - private readonly time: number, - private readonly callback: () => void, - ) {} - - run() { - this.cancel() - this.timeoutId = setTimeout(this.callback, this.time) - } - - cancel() { - if (this.timeoutId) { - clearTimeout(this.timeoutId) - this.timeoutId = undefined - } - } -} diff --git a/packages/reacord/library/make-message-update-payload.ts b/packages/reacord/library/make-message-update-payload.ts index 08c9e00..2881175 100644 --- a/packages/reacord/library/make-message-update-payload.ts +++ b/packages/reacord/library/make-message-update-payload.ts @@ -6,28 +6,28 @@ import type { APISelectMenuOption, } from "discord-api-types/v10" import { ButtonStyle, ComponentType } from "discord-api-types/v10" -import { ActionRowNode } from "./components/action-row" -import type { ButtonProps } from "./components/button" -import { ButtonNode } from "./components/button" -import { EmbedNode } from "./components/embed" -import { EmbedAuthorNode } from "./components/embed-author" +import type { Node } from "./node" +import { ActionRowNode } from "./react/action-row" +import type { ButtonProps } from "./react/button" +import { ButtonNode } from "./react/button" +import { EmbedNode } from "./react/embed" +import { EmbedAuthorNode } from "./react/embed-author" import { EmbedFieldNameNode, EmbedFieldNode, EmbedFieldValueNode, -} from "./components/embed-field" -import { EmbedFooterNode } from "./components/embed-footer" -import { EmbedImageNode } from "./components/embed-image" -import { EmbedThumbnailNode } from "./components/embed-thumbnail" -import { EmbedTitleNode } from "./components/embed-title" -import { LinkNode } from "./components/link" +} from "./react/embed-field" +import { EmbedFooterNode } from "./react/embed-footer" +import { EmbedImageNode } from "./react/embed-image" +import { EmbedThumbnailNode } from "./react/embed-thumbnail" +import { EmbedTitleNode } from "./react/embed-title" +import { LinkNode } from "./react/link" import { OptionDescriptionNode, OptionLabelNode, OptionNode, -} from "./components/option" -import { SelectNode } from "./components/select" -import type { Node } from "./node" +} from "./react/option" +import { SelectNode } from "./react/select" export type MessageUpdatePayload = { content: string diff --git a/packages/reacord/library/reacord-client.ts b/packages/reacord/library/reacord-client.ts index 5f8a65d..1d965e8 100644 --- a/packages/reacord/library/reacord-client.ts +++ b/packages/reacord/library/reacord-client.ts @@ -6,9 +6,9 @@ import { InteractionType, } from "discord.js" import * as React from "react" -import { InstanceProvider } from "./core/instance-context" import type { ReacordInstance } from "./reacord-instance.js" import { ReacordInstancePrivate } from "./reacord-instance.js" +import { InstanceProvider } from "./react/instance-context" import type { Renderer } from "./renderer.js" import { ChannelMessageRenderer, diff --git a/packages/reacord/library/reacord-instance.ts b/packages/reacord/library/reacord-instance.ts index db170fa..80bb1f9 100644 --- a/packages/reacord/library/reacord-instance.ts +++ b/packages/reacord/library/reacord-instance.ts @@ -4,13 +4,13 @@ import type { APIMessageComponentSelectMenuInteraction, } from "discord.js" import { ComponentType } from "discord.js" -import { ButtonNode } from "./components/button" -import type { SelectChangeEvent } from "./components/select" -import { SelectNode } from "./components/select" -import type { ComponentEvent } from "./core/component-event" import { Node } from "./node" import type { ReacordClient } from "./reacord-client" -import { reconciler } from "./reconciler" +import { ButtonNode } from "./react/button" +import type { ComponentEvent } from "./react/component-event" +import { reconciler } from "./react/reconciler" +import type { SelectChangeEvent } from "./react/select" +import { SelectNode } from "./react/select" import type { Renderer } from "./renderer" /** diff --git a/packages/reacord/library/components/action-row.tsx b/packages/reacord/library/react/action-row.tsx similarity index 94% rename from packages/reacord/library/components/action-row.tsx rename to packages/reacord/library/react/action-row.tsx index e16a3ca..4f465b7 100644 --- a/packages/reacord/library/components/action-row.tsx +++ b/packages/reacord/library/react/action-row.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react" import React from "react" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * Props for an action row diff --git a/packages/reacord/library/components/button-shared-props.ts b/packages/reacord/library/react/button-shared-props.ts similarity index 100% rename from packages/reacord/library/components/button-shared-props.ts rename to packages/reacord/library/react/button-shared-props.ts diff --git a/packages/reacord/library/components/button.tsx b/packages/reacord/library/react/button.tsx similarity index 94% rename from packages/reacord/library/components/button.tsx rename to packages/reacord/library/react/button.tsx index e70b62e..3dccb7f 100644 --- a/packages/reacord/library/components/button.tsx +++ b/packages/reacord/library/react/button.tsx @@ -1,10 +1,10 @@ import type { APIMessageComponentButtonInteraction } from "discord.js" import { randomUUID } from "node:crypto" import React from "react" -import type { ComponentEvent } from "../core/component-event.js" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" import type { ButtonSharedProps } from "./button-shared-props" +import type { ComponentEvent } from "./component-event.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Button diff --git a/packages/reacord/library.new.new/core/component-event.ts b/packages/reacord/library/react/component-event.ts similarity index 85% rename from packages/reacord/library.new.new/core/component-event.ts rename to packages/reacord/library/react/component-event.ts index 0dc8b4c..3cf8f36 100644 --- a/packages/reacord/library.new.new/core/component-event.ts +++ b/packages/reacord/library/react/component-event.ts @@ -1,5 +1,5 @@ import type { ReactNode } from "react" -import type { ReacordInstance } from "./reacord-instance" +import type { ReacordInstance } from "../reacord-instance.js" /** * @category Component Event diff --git a/packages/reacord/library/components/embed-author.tsx b/packages/reacord/library/react/embed-author.tsx similarity index 90% rename from packages/reacord/library/components/embed-author.tsx rename to packages/reacord/library/react/embed-author.tsx index 450715b..6a15751 100644 --- a/packages/reacord/library/components/embed-author.tsx +++ b/packages/reacord/library/react/embed-author.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react" import React from "react" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/components/embed-field.tsx b/packages/reacord/library/react/embed-field.tsx similarity index 95% rename from packages/reacord/library/components/embed-field.tsx rename to packages/reacord/library/react/embed-field.tsx index 782296b..9a1c0fe 100644 --- a/packages/reacord/library/components/embed-field.tsx +++ b/packages/reacord/library/react/embed-field.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react" import React from "react" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/components/embed-footer.tsx b/packages/reacord/library/react/embed-footer.tsx similarity index 94% rename from packages/reacord/library/components/embed-footer.tsx rename to packages/reacord/library/react/embed-footer.tsx index 5e944dc..b5720e5 100644 --- a/packages/reacord/library/components/embed-footer.tsx +++ b/packages/reacord/library/react/embed-footer.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react" import React from "react" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/components/embed-image.tsx b/packages/reacord/library/react/embed-image.tsx similarity index 87% rename from packages/reacord/library/components/embed-image.tsx rename to packages/reacord/library/react/embed-image.tsx index 3906c0f..3e15e88 100644 --- a/packages/reacord/library/components/embed-image.tsx +++ b/packages/reacord/library/react/embed-image.tsx @@ -1,6 +1,6 @@ import React from "react" import { Node } from "../node" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/components/embed-thumbnail.tsx b/packages/reacord/library/react/embed-thumbnail.tsx similarity index 88% rename from packages/reacord/library/components/embed-thumbnail.tsx rename to packages/reacord/library/react/embed-thumbnail.tsx index 4c6bec4..fe46157 100644 --- a/packages/reacord/library/components/embed-thumbnail.tsx +++ b/packages/reacord/library/react/embed-thumbnail.tsx @@ -1,6 +1,6 @@ import React from "react" import { Node } from "../node" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/components/embed-title.tsx b/packages/reacord/library/react/embed-title.tsx similarity index 90% rename from packages/reacord/library/components/embed-title.tsx rename to packages/reacord/library/react/embed-title.tsx index 09d33be..0f8d61d 100644 --- a/packages/reacord/library/components/embed-title.tsx +++ b/packages/reacord/library/react/embed-title.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react" import React from "react" import type { Except } from "type-fest" import { Node } from "../node" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/components/embed.tsx b/packages/reacord/library/react/embed.tsx similarity index 96% rename from packages/reacord/library/components/embed.tsx rename to packages/reacord/library/react/embed.tsx index 8995953..2be2211 100644 --- a/packages/reacord/library/components/embed.tsx +++ b/packages/reacord/library/react/embed.tsx @@ -1,6 +1,6 @@ import React from "react" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Embed diff --git a/packages/reacord/library/core/instance-context.tsx b/packages/reacord/library/react/instance-context.ts similarity index 89% rename from packages/reacord/library/core/instance-context.tsx rename to packages/reacord/library/react/instance-context.ts index 0e62a39..4112b06 100644 --- a/packages/reacord/library/core/instance-context.tsx +++ b/packages/reacord/library/react/instance-context.ts @@ -1,6 +1,6 @@ import { raise } from "@reacord/helpers/raise" import * as React from "react" -import type { ReacordInstance } from "./instance" +import type { ReacordInstance } from "../reacord-instance.js" const Context = React.createContext(undefined) diff --git a/packages/reacord/library/components/link.tsx b/packages/reacord/library/react/link.tsx similarity index 92% rename from packages/reacord/library/components/link.tsx rename to packages/reacord/library/react/link.tsx index 81ec27f..89f3fd3 100644 --- a/packages/reacord/library/components/link.tsx +++ b/packages/reacord/library/react/link.tsx @@ -1,8 +1,8 @@ import React from "react" import type { Except } from "type-fest" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" import type { ButtonSharedProps } from "./button-shared-props" +import { ReacordElement } from "./reacord-element.js" /** * @category Link diff --git a/packages/reacord/library/components/option.tsx b/packages/reacord/library/react/option.tsx similarity index 96% rename from packages/reacord/library/components/option.tsx rename to packages/reacord/library/react/option.tsx index 2af4384..16f7285 100644 --- a/packages/reacord/library/components/option.tsx +++ b/packages/reacord/library/react/option.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react" import React from "react" import { Node } from "../node" -import { ReacordElement } from "../reacord-element" +import { ReacordElement } from "./reacord-element" /** * @category Select diff --git a/packages/reacord/library/reacord-element.ts b/packages/reacord/library/react/reacord-element.ts similarity index 87% rename from packages/reacord/library/reacord-element.ts rename to packages/reacord/library/react/reacord-element.ts index ab5f9ac..0fd7bdd 100644 --- a/packages/reacord/library/reacord-element.ts +++ b/packages/reacord/library/react/reacord-element.ts @@ -1,6 +1,6 @@ import type { ReactNode } from "react" import React from "react" -import type { Node } from "./node" +import type { Node } from "../node" export function ReacordElement(props: { props: Props diff --git a/packages/reacord/library/reconciler.ts b/packages/reacord/library/react/reconciler.ts similarity index 96% rename from packages/reacord/library/reconciler.ts rename to packages/reacord/library/react/reconciler.ts index 57d014f..63b6f69 100644 --- a/packages/reacord/library/reconciler.ts +++ b/packages/reacord/library/react/reconciler.ts @@ -2,8 +2,8 @@ import { raise } from "@reacord/helpers/raise.js" import ReactReconciler from "react-reconciler" import { DefaultEventPriority } from "react-reconciler/constants" -import { Node, TextNode } from "./node.js" -import type { ReacordInstancePrivate } from "./reacord-instance.js" +import { Node, TextNode } from "../node.js" +import type { ReacordInstancePrivate } from "../reacord-instance.js" export const reconciler = ReactReconciler< string, // Type, diff --git a/packages/reacord/library/components/select.tsx b/packages/reacord/library/react/select.tsx similarity index 95% rename from packages/reacord/library/components/select.tsx rename to packages/reacord/library/react/select.tsx index be5c6c3..e00bf38 100644 --- a/packages/reacord/library/components/select.tsx +++ b/packages/reacord/library/react/select.tsx @@ -2,9 +2,9 @@ import type { APIMessageComponentSelectMenuInteraction } from "discord.js" import { randomUUID } from "node:crypto" import type { ReactNode } from "react" import React from "react" -import type { ComponentEvent } from "../core/component-event.js" import { Node } from "../node.js" -import { ReacordElement } from "../reacord-element.js" +import type { ComponentEvent } from "./component-event.js" +import { ReacordElement } from "./reacord-element.js" /** * @category Select diff --git a/packages/reacord/library/renderer.ts b/packages/reacord/library/renderer.ts index 13a1a44..b4e4e07 100644 --- a/packages/reacord/library/renderer.ts +++ b/packages/reacord/library/renderer.ts @@ -1,4 +1,4 @@ -import { AsyncQueue } from "@reacord/helpers/async-queue" +import { AsyncQueue } from "@reacord/helpers/async-queue.js" import type { Client, Message } from "discord.js" import { TextChannel } from "discord.js" import { makeMessageUpdatePayload } from "./make-message-update-payload.js" diff --git a/packages/reacord/scripts/generate-exports.ts b/packages/reacord/scripts/generate-exports.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/reacord/scripts/manual-test.tsx b/packages/reacord/scripts/manual-test.tsx index 2d179f3..07f5809 100644 --- a/packages/reacord/scripts/manual-test.tsx +++ b/packages/reacord/scripts/manual-test.tsx @@ -3,11 +3,11 @@ import "dotenv/config" import { kebabCase } from "lodash-es" import * as React from "react" import { useState } from "react" -import { Button } from "../library/components/button" -import { Option } from "../library/components/option" -import { Select } from "../library/components/select" -import { useInstance } from "../library/core/instance-context" import { ReacordClient } from "../library/reacord-client" +import { Button } from "../library/react/button" +import { useInstance } from "../library/react/instance-context" +import { Option } from "../library/react/option" +import { Select } from "../library/react/select" const client = new Client({ intents: IntentsBitField.Flags.Guilds })