embeds + decentralized element definition

This commit is contained in:
MapleLeaf
2021-12-25 03:11:01 -06:00
parent 18bcf4828c
commit 6f3c97812c
12 changed files with 231 additions and 116 deletions

View File

@@ -1,12 +1,36 @@
import * as React from "react"
import { Button } from "../src.new/components/button.js"
import { Button } from "../src.new/button.js"
import { Embed } from "../src.new/embed.js"
export function Counter() {
const [count, setCount] = React.useState(0)
const [embedVisible, setEmbedVisible] = React.useState(false)
return (
<>
this button was clicked {count} times
<Button label="clicc" onClick={() => setCount(count + 1)} />
{embedVisible && (
<Embed
title="the counter"
fields={[
{
name: "is it even?",
value: count % 2 === 0 ? "yes" : "no",
},
]}
/>
)}
{embedVisible && (
<Button label="hide embed" onClick={() => setEmbedVisible(false)} />
)}
<Button
style="primary"
label="clicc"
onClick={() => setCount(count + 1)}
/>
{!embedVisible && (
<Button label="show embed" onClick={() => setEmbedVisible(true)} />
)}
</>
)
}

73
src.new/button.tsx Normal file
View File

@@ -0,0 +1,73 @@
import type {
ButtonInteraction,
CacheType,
EmojiResolvable,
MessageButtonStyle,
MessageComponentInteraction,
MessageOptions,
} from "discord.js"
import { MessageActionRow } from "discord.js"
import { nanoid } from "nanoid"
import React from "react"
import { last } from "../src/helpers/last.js"
import { toUpper } from "../src/helpers/to-upper.js"
import { Node } from "./node.js"
export type ButtonProps = {
label?: string
style?: Exclude<Lowercase<MessageButtonStyle>, "link">
disabled?: boolean
emoji?: EmojiResolvable
onClick: (interaction: ButtonInteraction) => void
}
export function Button(props: ButtonProps) {
return (
<reacord-element props={props} createNode={() => new ButtonNode(props)} />
)
}
class ButtonNode extends Node<ButtonProps> {
private customId = nanoid()
private get buttonOptions() {
return {
type: "BUTTON",
customId: this.customId,
style: toUpper(this.props.style ?? "secondary"),
disabled: this.props.disabled,
emoji: this.props.emoji,
label: this.props.label,
} as const
}
override modifyMessageOptions(options: MessageOptions): void {
options.components ??= []
let actionRow = last(options.components)
if (
!actionRow ||
actionRow.components.length >= 5 ||
actionRow.components[0]?.type === "SELECT_MENU"
) {
actionRow = new MessageActionRow()
options.components.push(actionRow)
}
if (actionRow instanceof MessageActionRow) {
actionRow.addComponents(this.buttonOptions)
} else {
actionRow.components.push(this.buttonOptions)
}
}
override handleInteraction(
interaction: MessageComponentInteraction<CacheType>,
) {
if (interaction.isButton() && interaction.customId === this.customId) {
this.props.onClick(interaction)
return true
}
}
}

View File

@@ -1,31 +0,0 @@
import type {
EmojiResolvable,
MessageButtonStyle,
MessageComponentInteraction,
} from "discord.js"
import { nanoid } from "nanoid"
import React from "react"
import { Node } from "../node.js"
export type ButtonProps = {
label?: string
style?: Exclude<Lowercase<MessageButtonStyle>, "link">
disabled?: boolean
emoji?: EmojiResolvable
onClick?: (interaction: MessageComponentInteraction) => void
}
export const ButtonTag = "reacord-button"
export function Button(props: ButtonProps) {
return React.createElement(ButtonTag, props)
}
export class ButtonNode extends Node {
readonly name = "button"
readonly customId = nanoid()
constructor(public props: ButtonProps) {
super()
}
}

View File

@@ -1 +0,0 @@
export type Context = {}

44
src.new/embed.tsx Normal file
View File

@@ -0,0 +1,44 @@
import type { MessageOptions } from "discord.js"
import React from "react"
import { Node } from "./node.js"
export type EmbedProps = {
title?: string
description?: string
url?: string
timestamp?: Date
color?: number
footer?: {
text: string
iconURL?: string
}
image?: {
url: string
}
thumbnail?: {
url: string
}
author?: {
name: string
url?: string
iconURL?: string
}
fields?: Array<{
name: string
value: string
inline?: boolean
}>
}
export function Embed(props: EmbedProps) {
return (
<reacord-element props={props} createNode={() => new EmbedNode(props)} />
)
}
class EmbedNode extends Node<EmbedProps> {
override modifyMessageOptions(options: MessageOptions): void {
options.embeds ??= []
options.embeds.push(this.props)
}
}

5
src.new/jsx.d.ts vendored
View File

@@ -1,12 +1,13 @@
import type { ReactNode } from "react"
import type { Node } from "./node"
import type { Node } from "./node.js"
declare global {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements {
"reacord-element": {
createNode: () => Node
props: Record<string, unknown>
createNode: (props: Record<string, unknown>) => Node
children?: ReactNode
}
}

View File

@@ -1,4 +1,22 @@
export abstract class Node {
abstract get name(): string
abstract props: Record<string, unknown>
/* eslint-disable class-methods-use-this */
import type { MessageComponentInteraction, MessageOptions } from "discord.js"
export abstract class Node<Props> {
protected props: Props
constructor(initialProps: Props) {
this.props = initialProps
}
setProps(props: Props) {
this.props = props
}
modifyMessageOptions(options: MessageOptions) {}
handleInteraction(
interaction: MessageComponentInteraction,
): true | undefined {
return undefined
}
}

View File

@@ -1,16 +1,15 @@
import type { HostConfig } from "react-reconciler"
import ReactReconciler from "react-reconciler"
import { raise } from "../src/helpers/raise.js"
import { ButtonNode } from "./components/button.js"
import type { Node } from "./node.js"
import { Node } from "./node.js"
import type { Renderer } from "./renderer.js"
import { TextNode } from "./text-node.js"
import { TextNode } from "./text.js"
const config: HostConfig<
string, // Type,
Record<string, unknown>, // Props,
Renderer, // Container,
Node, // Instance,
Node<unknown>, // Instance,
TextNode, // TextInstance,
never, // SuspenseInstance,
never, // HydratableInstance,
@@ -35,8 +34,20 @@ const config: HostConfig<
getChildHostContext: () => ({}),
createInstance: (type, props) => {
if (type === "reacord-button") return new ButtonNode(props)
raise(`Unknown type: ${type}`)
if (type !== "reacord-element") {
raise(`Unknown element type: ${type}`)
}
if (typeof props.createNode !== "function") {
raise(`Missing createNode function`)
}
const node = props.createNode(props.props)
if (!(node instanceof Node)) {
raise(`createNode function did not return a Node`)
}
return node
},
createTextInstance: (text) => new TextNode(text),
shouldSetTextContent: () => false,
@@ -50,14 +61,17 @@ const config: HostConfig<
removeChildFromContainer: (renderer, child) => {
renderer.remove(child)
},
insertInContainerBefore: (renderer, child, before) => {
renderer.addBefore(child, before)
},
// eslint-disable-next-line unicorn/no-null
prepareUpdate: () => true,
commitUpdate: (node, payload, type, oldProps, newProps) => {
node.props = newProps
node.setProps(newProps.props)
},
commitTextUpdate: (node, oldText, newText) => {
node.text = newText
node.setProps(newText)
},
// eslint-disable-next-line unicorn/no-null

View File

@@ -3,29 +3,32 @@ import type {
MessageComponentInteraction,
MessageOptions,
} from "discord.js"
import { MessageActionRow } from "discord.js"
import { last } from "../src/helpers/last.js"
import { toUpper } from "../src/helpers/to-upper.js"
import { ButtonNode } from "./components/button.js"
import type { Node } from "./node.js"
import { TextNode } from "./text-node.js"
export class Renderer {
private nodes = new Set<Node | TextNode>()
private nodes: Array<Node<unknown>> = []
private componentInteraction?: MessageComponentInteraction
constructor(private interaction: CommandInteraction) {}
add(child: Node | TextNode) {
this.nodes.add(child)
add(node: Node<unknown>) {
this.nodes.push(node)
}
remove(child: Node | TextNode) {
this.nodes.delete(child)
addBefore(node: Node<unknown>, before: Node<unknown>) {
let index = this.nodes.indexOf(before)
if (index === -1) {
index = this.nodes.length
}
this.nodes.splice(index, 0, node)
}
remove(node: Node<unknown>) {
this.nodes = this.nodes.filter((n) => n !== node)
}
clear() {
this.nodes.clear()
this.nodes = []
}
render() {
@@ -41,54 +44,23 @@ export class Renderer {
}
handleInteraction(interaction: MessageComponentInteraction) {
if (interaction.isButton()) {
for (const node of this.nodes) {
this.componentInteraction = interaction
this.getButtonCallback(interaction.customId)?.(interaction)
if (node.handleInteraction(interaction)) {
return true
}
return false
}
}
private getMessageOptions(): MessageOptions {
let content = ""
let components: MessageActionRow[] = []
for (const child of this.nodes) {
if (child instanceof TextNode) {
content += child.text
}
if (child instanceof ButtonNode) {
let actionRow = last(components)
if (
!actionRow ||
actionRow.components.length >= 5 ||
actionRow.components[0]?.type === "SELECT_MENU"
) {
actionRow = new MessageActionRow()
components.push(actionRow)
}
actionRow.addComponents({
type: "BUTTON",
customId: child.customId,
style: toUpper(child.props.style ?? "secondary"),
disabled: child.props.disabled,
emoji: child.props.emoji,
label: child.props.label,
})
}
}
return { content, components }
}
private getButtonCallback(customId: string) {
for (const child of this.nodes) {
if (child instanceof ButtonNode && child.customId === customId) {
return child.props.onClick
const options: MessageOptions = {
content: "",
embeds: [],
components: [],
}
for (const node of this.nodes) {
node.modifyMessageOptions(options)
}
return options
}
}

View File

@@ -1,4 +0,0 @@
export class TextNode {
readonly name = "text"
constructor(public text: string) {}
}

8
src.new/text.ts Normal file
View File

@@ -0,0 +1,8 @@
import type { MessageOptions } from "discord.js"
import { Node } from "./node.js"
export class TextNode extends Node<string> {
override modifyMessageOptions(options: MessageOptions) {
options.content = (options.content ?? "") + this.props
}
}

21
src/jsx.d.ts vendored
View File

@@ -1,14 +1,11 @@
import type { ReactNode } from "react"
import type { Node } from "./node"
declare global {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements {
"reacord-element": {
createNode: () => Node
children?: ReactNode
}
}
}
// namespace JSX {
// // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
// interface IntrinsicElements {
// "reacord-element": {
// createNode: () => Node
// children?: ReactNode
// }
// }
// }
}