embeds + decentralized element definition
This commit is contained in:
@@ -1,12 +1,36 @@
|
|||||||
import * as React from "react"
|
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() {
|
export function Counter() {
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = React.useState(0)
|
||||||
|
const [embedVisible, setEmbedVisible] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
this button was clicked {count} times
|
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
73
src.new/button.tsx
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export type Context = {}
|
|
||||||
44
src.new/embed.tsx
Normal file
44
src.new/embed.tsx
Normal 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
5
src.new/jsx.d.ts
vendored
@@ -1,12 +1,13 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import type { Node } from "./node"
|
import type { Node } from "./node.js"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
"reacord-element": {
|
"reacord-element": {
|
||||||
createNode: () => Node
|
props: Record<string, unknown>
|
||||||
|
createNode: (props: Record<string, unknown>) => Node
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
export abstract class Node {
|
/* eslint-disable class-methods-use-this */
|
||||||
abstract get name(): string
|
import type { MessageComponentInteraction, MessageOptions } from "discord.js"
|
||||||
abstract props: Record<string, unknown>
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type { HostConfig } from "react-reconciler"
|
import type { HostConfig } from "react-reconciler"
|
||||||
import ReactReconciler from "react-reconciler"
|
import ReactReconciler from "react-reconciler"
|
||||||
import { raise } from "../src/helpers/raise.js"
|
import { raise } from "../src/helpers/raise.js"
|
||||||
import { ButtonNode } from "./components/button.js"
|
import { Node } from "./node.js"
|
||||||
import type { Node } from "./node.js"
|
|
||||||
import type { Renderer } from "./renderer.js"
|
import type { Renderer } from "./renderer.js"
|
||||||
import { TextNode } from "./text-node.js"
|
import { TextNode } from "./text.js"
|
||||||
|
|
||||||
const config: HostConfig<
|
const config: HostConfig<
|
||||||
string, // Type,
|
string, // Type,
|
||||||
Record<string, unknown>, // Props,
|
Record<string, unknown>, // Props,
|
||||||
Renderer, // Container,
|
Renderer, // Container,
|
||||||
Node, // Instance,
|
Node<unknown>, // Instance,
|
||||||
TextNode, // TextInstance,
|
TextNode, // TextInstance,
|
||||||
never, // SuspenseInstance,
|
never, // SuspenseInstance,
|
||||||
never, // HydratableInstance,
|
never, // HydratableInstance,
|
||||||
@@ -35,8 +34,20 @@ const config: HostConfig<
|
|||||||
getChildHostContext: () => ({}),
|
getChildHostContext: () => ({}),
|
||||||
|
|
||||||
createInstance: (type, props) => {
|
createInstance: (type, props) => {
|
||||||
if (type === "reacord-button") return new ButtonNode(props)
|
if (type !== "reacord-element") {
|
||||||
raise(`Unknown type: ${type}`)
|
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),
|
createTextInstance: (text) => new TextNode(text),
|
||||||
shouldSetTextContent: () => false,
|
shouldSetTextContent: () => false,
|
||||||
@@ -50,14 +61,17 @@ const config: HostConfig<
|
|||||||
removeChildFromContainer: (renderer, child) => {
|
removeChildFromContainer: (renderer, child) => {
|
||||||
renderer.remove(child)
|
renderer.remove(child)
|
||||||
},
|
},
|
||||||
|
insertInContainerBefore: (renderer, child, before) => {
|
||||||
|
renderer.addBefore(child, before)
|
||||||
|
},
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
// eslint-disable-next-line unicorn/no-null
|
||||||
prepareUpdate: () => true,
|
prepareUpdate: () => true,
|
||||||
commitUpdate: (node, payload, type, oldProps, newProps) => {
|
commitUpdate: (node, payload, type, oldProps, newProps) => {
|
||||||
node.props = newProps
|
node.setProps(newProps.props)
|
||||||
},
|
},
|
||||||
commitTextUpdate: (node, oldText, newText) => {
|
commitTextUpdate: (node, oldText, newText) => {
|
||||||
node.text = newText
|
node.setProps(newText)
|
||||||
},
|
},
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
|||||||
@@ -3,29 +3,32 @@ import type {
|
|||||||
MessageComponentInteraction,
|
MessageComponentInteraction,
|
||||||
MessageOptions,
|
MessageOptions,
|
||||||
} from "discord.js"
|
} 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 type { Node } from "./node.js"
|
||||||
import { TextNode } from "./text-node.js"
|
|
||||||
|
|
||||||
export class Renderer {
|
export class Renderer {
|
||||||
private nodes = new Set<Node | TextNode>()
|
private nodes: Array<Node<unknown>> = []
|
||||||
private componentInteraction?: MessageComponentInteraction
|
private componentInteraction?: MessageComponentInteraction
|
||||||
|
|
||||||
constructor(private interaction: CommandInteraction) {}
|
constructor(private interaction: CommandInteraction) {}
|
||||||
|
|
||||||
add(child: Node | TextNode) {
|
add(node: Node<unknown>) {
|
||||||
this.nodes.add(child)
|
this.nodes.push(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(child: Node | TextNode) {
|
addBefore(node: Node<unknown>, before: Node<unknown>) {
|
||||||
this.nodes.delete(child)
|
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() {
|
clear() {
|
||||||
this.nodes.clear()
|
this.nodes = []
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -41,54 +44,23 @@ export class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleInteraction(interaction: MessageComponentInteraction) {
|
handleInteraction(interaction: MessageComponentInteraction) {
|
||||||
if (interaction.isButton()) {
|
for (const node of this.nodes) {
|
||||||
this.componentInteraction = interaction
|
this.componentInteraction = interaction
|
||||||
this.getButtonCallback(interaction.customId)?.(interaction)
|
if (node.handleInteraction(interaction)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMessageOptions(): MessageOptions {
|
private getMessageOptions(): MessageOptions {
|
||||||
let content = ""
|
const options: MessageOptions = {
|
||||||
let components: MessageActionRow[] = []
|
content: "",
|
||||||
|
embeds: [],
|
||||||
for (const child of this.nodes) {
|
components: [],
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
node.modifyMessageOptions(options)
|
||||||
}
|
}
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export class TextNode {
|
|
||||||
readonly name = "text"
|
|
||||||
constructor(public text: string) {}
|
|
||||||
}
|
|
||||||
8
src.new/text.ts
Normal file
8
src.new/text.ts
Normal 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
21
src/jsx.d.ts
vendored
@@ -1,14 +1,11 @@
|
|||||||
import type { ReactNode } from "react"
|
|
||||||
import type { Node } from "./node"
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
// namespace JSX {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
// // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
interface IntrinsicElements {
|
// interface IntrinsicElements {
|
||||||
"reacord-element": {
|
// "reacord-element": {
|
||||||
createNode: () => Node
|
// createNode: () => Node
|
||||||
children?: ReactNode
|
// children?: ReactNode
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user