untested rewrite
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { Node } from "../internal/node.js"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* Props for an action row
|
||||
@@ -31,17 +30,10 @@ export type ActionRowProps = {
|
||||
*/
|
||||
export function ActionRow(props: ActionRowProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new ActionRowNode(props)}>
|
||||
<ReacordElement props={{}} createNode={() => new ActionRowNode({})}>
|
||||
{props.children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class ActionRowNode extends Node<{}> {
|
||||
override modifyMessageOptions(options: MessageOptions): void {
|
||||
options.actionRows.push([])
|
||||
for (const child of this.children) {
|
||||
child.modifyMessageOptions(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
export class ActionRowNode extends Node<{}> {}
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { APIMessageComponentButtonInteraction } from "discord.js"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import React from "react"
|
||||
import type { ComponentEvent } from "../core/component-event.js"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
import type { ButtonSharedProps } from "./button-shared-props"
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { Node } from "../internal/node.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -21,21 +19,9 @@ export type EmbedAuthorProps = {
|
||||
export function EmbedAuthor(props: EmbedAuthorProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
|
||||
<ReacordElement props={{}} createNode={() => new AuthorTextNode({})}>
|
||||
{props.name ?? props.children}
|
||||
</ReacordElement>
|
||||
{props.name ?? props.children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.author = {
|
||||
name: this.children.findType(AuthorTextNode)?.text ?? "",
|
||||
url: this.props.url,
|
||||
icon_url: this.props.iconUrl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorTextNode extends Node<{}> {}
|
||||
export class EmbedAuthorNode extends Node<EmbedAuthorProps> {}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { Node } from "../internal/node.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -21,26 +19,26 @@ export type EmbedFieldProps = {
|
||||
export function EmbedField(props: EmbedFieldProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
|
||||
<ReacordElement props={{}} createNode={() => new FieldNameNode({})}>
|
||||
<ReacordElement props={{}} createNode={() => new EmbedFieldNameNode({})}>
|
||||
{props.name}
|
||||
</ReacordElement>
|
||||
<ReacordElement props={{}} createNode={() => new FieldValueNode({})}>
|
||||
{props.value || props.children}
|
||||
<ReacordElement props={{}} createNode={() => new EmbedFieldValueNode({})}>
|
||||
{props.value ?? props.children}
|
||||
</ReacordElement>
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.fields ??= []
|
||||
options.fields.push({
|
||||
name: this.children.findType(FieldNameNode)?.text ?? "",
|
||||
value: this.children.findType(FieldValueNode)?.text ?? "",
|
||||
inline: this.props.inline,
|
||||
})
|
||||
}
|
||||
export class EmbedFieldNode extends Node<EmbedFieldProps> {
|
||||
// override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
// options.fields ??= []
|
||||
// options.fields.push({
|
||||
// name: this.children.findType(FieldNameNode)?.text ?? "",
|
||||
// value: this.children.findType(FieldValueNode)?.text ?? "",
|
||||
// inline: this.props.inline,
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
class FieldNameNode extends Node<{}> {}
|
||||
class FieldValueNode extends Node<{}> {}
|
||||
export class EmbedFieldNameNode extends Node<{}> {}
|
||||
export class EmbedFieldValueNode extends Node<{}> {}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { Node } from "../internal/node.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -21,25 +19,21 @@ export type EmbedFooterProps = {
|
||||
export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
|
||||
<ReacordElement props={{}} createNode={() => new FooterTextNode({})}>
|
||||
{text ?? children}
|
||||
</ReacordElement>
|
||||
{text ?? children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedFooterNode extends EmbedChildNode<
|
||||
export class EmbedFooterNode extends Node<
|
||||
Omit<EmbedFooterProps, "text" | "children">
|
||||
> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.footer = {
|
||||
text: this.children.findType(FooterTextNode)?.text ?? "",
|
||||
icon_url: this.props.iconUrl,
|
||||
}
|
||||
options.timestamp = this.props.timestamp
|
||||
? new Date(this.props.timestamp).toISOString()
|
||||
: undefined
|
||||
}
|
||||
// override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
// options.footer = {
|
||||
// text: this.children.findType(FooterTextNode)?.text ?? "",
|
||||
// icon_url: this.props.iconUrl,
|
||||
// }
|
||||
// options.timestamp = this.props.timestamp
|
||||
// ? new Date(this.props.timestamp).toISOString()
|
||||
// : undefined
|
||||
// }
|
||||
}
|
||||
|
||||
class FooterTextNode extends Node<{}> {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import { Node } from "../node"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -22,8 +21,4 @@ export function EmbedImage(props: EmbedImageProps) {
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedImageNode extends EmbedChildNode<EmbedImageProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.image = { url: this.props.url }
|
||||
}
|
||||
}
|
||||
export class EmbedImageNode extends Node<EmbedImageProps> {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import { Node } from "../node"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -22,8 +21,4 @@ export function EmbedThumbnail(props: EmbedThumbnailProps) {
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedThumbnailNode extends EmbedChildNode<EmbedThumbnailProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.thumbnail = { url: this.props.url }
|
||||
}
|
||||
}
|
||||
export class EmbedThumbnailNode extends Node<EmbedThumbnailProps> {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { Node } from "../internal/node.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import type { Except } from "type-fest"
|
||||
import { Node } from "../node"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -19,18 +18,9 @@ export type EmbedTitleProps = {
|
||||
export function EmbedTitle({ children, ...props }: EmbedTitleProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
|
||||
<ReacordElement props={{}} createNode={() => new TitleTextNode({})}>
|
||||
{children}
|
||||
</ReacordElement>
|
||||
{children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.title = this.children.findType(TitleTextNode)?.text ?? ""
|
||||
options.url = this.props.url
|
||||
}
|
||||
}
|
||||
|
||||
class TitleTextNode extends Node<{}> {}
|
||||
export class EmbedTitleNode extends Node<Except<EmbedTitleProps, "children">> {}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { snakeCaseDeep } from "@reacord/helpers/convert-object-property-case.js"
|
||||
import { omit } from "@reacord/helpers/omit.js"
|
||||
import React from "react"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import { Node } from "../node.js"
|
||||
import { TextNode } from "../text-node"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Embed
|
||||
@@ -39,24 +33,22 @@ export function Embed(props: EmbedProps) {
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedNode extends Node<EmbedProps> {
|
||||
override modifyMessageOptions(options: MessageOptions): void {
|
||||
const embed: EmbedOptions = {
|
||||
...snakeCaseDeep(omit(this.props, ["children", "timestamp"])),
|
||||
timestamp: this.props.timestamp
|
||||
? new Date(this.props.timestamp).toISOString()
|
||||
: undefined,
|
||||
}
|
||||
|
||||
for (const child of this.children) {
|
||||
if (child instanceof EmbedChildNode) {
|
||||
child.modifyEmbedOptions(embed)
|
||||
}
|
||||
if (child instanceof TextNode) {
|
||||
embed.description = (embed.description || "") + child.props
|
||||
}
|
||||
}
|
||||
|
||||
options.embeds.push(embed)
|
||||
}
|
||||
export class EmbedNode extends Node<EmbedProps> {
|
||||
// override modifyMessageOptions(options: MessageOptions): void {
|
||||
// const embed: EmbedOptions = {
|
||||
// ...snakeCaseDeep(omit(this.props, ["children", "timestamp"])),
|
||||
// timestamp: this.props.timestamp
|
||||
// ? new Date(this.props.timestamp).toISOString()
|
||||
// : undefined,
|
||||
// }
|
||||
// for (const child of this.children) {
|
||||
// if (child instanceof EmbedChildNode) {
|
||||
// child.modifyEmbedOptions(embed)
|
||||
// }
|
||||
// if (child instanceof TextNode) {
|
||||
// embed.description = (embed.description || "") + child.props
|
||||
// }
|
||||
// }
|
||||
// options.embeds.push(embed)
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { getNextActionRow } from "../internal/message"
|
||||
import { Node } from "../internal/node.js"
|
||||
import type { Except } from "type-fest"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
import type { ButtonSharedProps } from "./button-shared-props"
|
||||
|
||||
/**
|
||||
@@ -21,23 +20,9 @@ export type LinkProps = ButtonSharedProps & {
|
||||
export function Link({ label, children, ...props }: LinkProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new LinkNode(props)}>
|
||||
<ReacordElement props={{}} createNode={() => new LinkTextNode({})}>
|
||||
{label || children}
|
||||
</ReacordElement>
|
||||
{label || children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
|
||||
override modifyMessageOptions(options: MessageOptions): void {
|
||||
getNextActionRow(options).push({
|
||||
type: "link",
|
||||
disabled: this.props.disabled,
|
||||
emoji: this.props.emoji,
|
||||
label: this.children.findType(LinkTextNode)?.text,
|
||||
url: this.props.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class LinkTextNode extends Node<{}> {}
|
||||
export class LinkNode extends Node<Except<LinkProps, "label" | "children">> {}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { MessageSelectOptionOptions } from "../../internal/message"
|
||||
import { Node } from "../node"
|
||||
import type { OptionProps } from "./option"
|
||||
|
||||
export class OptionNode extends Node<
|
||||
Omit<OptionProps, "children" | "label" | "description">
|
||||
> {
|
||||
get options(): MessageSelectOptionOptions {
|
||||
return {
|
||||
label: this.children.findType(OptionLabelNode)?.text ?? this.props.value,
|
||||
value: this.props.value,
|
||||
description: this.children.findType(OptionDescriptionNode)?.text,
|
||||
emoji: this.props.emoji,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionLabelNode extends Node<{}> {}
|
||||
export class OptionDescriptionNode extends Node<{}> {}
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element"
|
||||
import {
|
||||
OptionDescriptionNode,
|
||||
OptionLabelNode,
|
||||
OptionNode,
|
||||
} from "./option-node"
|
||||
import { Node } from "../node"
|
||||
import { ReacordElement } from "../reacord-element"
|
||||
|
||||
/**
|
||||
* @category Select
|
||||
@@ -60,3 +56,9 @@ export function Option({
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
export class OptionNode extends Node<
|
||||
Omit<OptionProps, "children" | "label" | "description">
|
||||
> {}
|
||||
export class OptionLabelNode extends Node<{}> {}
|
||||
export class OptionDescriptionNode extends Node<{}> {}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { isInstanceOf } from "@reacord/helpers/is-instance-of.js"
|
||||
import type { APIMessageComponentSelectMenuInteraction } from "discord.js"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../internal/element.js"
|
||||
import type { ComponentInteraction } from "../../internal/interaction"
|
||||
import type {
|
||||
ActionRow,
|
||||
ActionRowItem,
|
||||
MessageOptions,
|
||||
} from "../../internal/message"
|
||||
import { Node } from "../internal/node.js"
|
||||
import type { ComponentEvent } from "../component-event"
|
||||
import { OptionNode } from "./option-node"
|
||||
import type { ComponentEvent } from "../core/component-event.js"
|
||||
import { Node } from "../node.js"
|
||||
import { ReacordElement } from "../reacord-element.js"
|
||||
|
||||
/**
|
||||
* @category Select
|
||||
@@ -100,64 +92,4 @@ export function Select(props: SelectProps) {
|
||||
|
||||
export class SelectNode extends Node<SelectProps> {
|
||||
readonly customId = randomUUID()
|
||||
|
||||
override modifyMessageOptions(message: MessageOptions): void {
|
||||
const actionRow: ActionRow = []
|
||||
message.actionRows.push(actionRow)
|
||||
|
||||
const options = [...this.children]
|
||||
.filter(isInstanceOf(OptionNode))
|
||||
.map((node) => node.options)
|
||||
|
||||
const {
|
||||
multiple,
|
||||
value,
|
||||
values,
|
||||
minValues = 0,
|
||||
maxValues = 25,
|
||||
children,
|
||||
onChange,
|
||||
onChangeValue,
|
||||
onChangeMultiple,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
const item: ActionRowItem = {
|
||||
...props,
|
||||
type: "select",
|
||||
customId: this.customId,
|
||||
options,
|
||||
values: [],
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
item.minValues = minValues
|
||||
item.maxValues = maxValues
|
||||
if (values) item.values = values
|
||||
}
|
||||
|
||||
if (!multiple && value != undefined) {
|
||||
item.values = [value]
|
||||
}
|
||||
|
||||
actionRow.push(item)
|
||||
}
|
||||
|
||||
override handleComponentInteraction(
|
||||
interaction: ComponentInteraction,
|
||||
): boolean {
|
||||
const isSelectInteraction =
|
||||
interaction.type === "select" &&
|
||||
interaction.customId === this.customId &&
|
||||
!this.props.disabled
|
||||
|
||||
if (!isSelectInteraction) return false
|
||||
|
||||
this.props.onChange?.(interaction.event)
|
||||
this.props.onChangeMultiple?.(interaction.event.values, interaction.event)
|
||||
if (interaction.event.values[0]) {
|
||||
this.props.onChangeValue?.(interaction.event.values[0], interaction.event)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
244
packages/reacord/library/make-message-update-payload.ts
Normal file
244
packages/reacord/library/make-message-update-payload.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIButtonComponent,
|
||||
APIEmbed,
|
||||
APISelectMenuComponent,
|
||||
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 {
|
||||
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"
|
||||
import {
|
||||
OptionDescriptionNode,
|
||||
OptionLabelNode,
|
||||
OptionNode,
|
||||
} from "./components/option"
|
||||
import { SelectNode } from "./components/select"
|
||||
import type { Node } from "./node"
|
||||
|
||||
export type MessageUpdatePayload = {
|
||||
content: string
|
||||
embeds: APIEmbed[]
|
||||
components: Array<
|
||||
APIActionRowComponent<APIButtonComponent | APISelectMenuComponent>
|
||||
>
|
||||
}
|
||||
|
||||
export function makeMessageUpdatePayload(root: Node): MessageUpdatePayload {
|
||||
return {
|
||||
content: root.extractText(),
|
||||
embeds: makeEmbeds(root),
|
||||
components: makeActionRows(root),
|
||||
}
|
||||
}
|
||||
|
||||
function makeEmbeds(root: Node) {
|
||||
const embeds: APIEmbed[] = []
|
||||
|
||||
for (const node of root.children) {
|
||||
if (node instanceof EmbedNode) {
|
||||
const { props, children } = node
|
||||
|
||||
const embed: APIEmbed = {
|
||||
author: props.author && {
|
||||
name: props.author.name,
|
||||
icon_url: props.author.iconUrl,
|
||||
url: props.author.url,
|
||||
},
|
||||
color: props.color,
|
||||
description: props.description,
|
||||
fields: props.fields?.map(({ name, value, inline }) => ({
|
||||
name,
|
||||
value,
|
||||
inline,
|
||||
})),
|
||||
footer: props.footer && {
|
||||
text: props.footer.text,
|
||||
icon_url: props.footer.iconUrl,
|
||||
},
|
||||
image: props.image,
|
||||
thumbnail: props.thumbnail,
|
||||
title: props.title,
|
||||
url: props.url,
|
||||
video: props.video,
|
||||
}
|
||||
|
||||
if (props.timestamp !== undefined) {
|
||||
embed.timestamp = normalizeDatePropToISOString(props.timestamp)
|
||||
}
|
||||
|
||||
applyEmbedChildren(embed, children)
|
||||
|
||||
embeds.push(embed)
|
||||
}
|
||||
}
|
||||
|
||||
return embeds
|
||||
}
|
||||
|
||||
function applyEmbedChildren(embed: APIEmbed, children: Node[]) {
|
||||
for (const child of children) {
|
||||
if (child instanceof EmbedAuthorNode) {
|
||||
embed.author = {
|
||||
name: child.extractText(),
|
||||
icon_url: child.props.iconUrl,
|
||||
url: child.props.url,
|
||||
}
|
||||
}
|
||||
|
||||
if (child instanceof EmbedFieldNode) {
|
||||
embed.fields ??= []
|
||||
embed.fields.push({
|
||||
name: child.findInstanceOf(EmbedFieldNameNode)?.extractText() ?? "",
|
||||
value: child.findInstanceOf(EmbedFieldValueNode)?.extractText() ?? "",
|
||||
inline: child.props.inline,
|
||||
})
|
||||
}
|
||||
|
||||
if (child instanceof EmbedFooterNode) {
|
||||
embed.footer = {
|
||||
text: child.extractText(),
|
||||
icon_url: child.props.iconUrl,
|
||||
}
|
||||
if (child.props.timestamp != undefined) {
|
||||
embed.timestamp = normalizeDatePropToISOString(child.props.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
if (child instanceof EmbedImageNode) {
|
||||
embed.image = { url: child.props.url }
|
||||
}
|
||||
|
||||
if (child instanceof EmbedThumbnailNode) {
|
||||
embed.thumbnail = { url: child.props.url }
|
||||
}
|
||||
|
||||
if (child instanceof EmbedTitleNode) {
|
||||
embed.title = child.extractText()
|
||||
embed.url = child.props.url
|
||||
}
|
||||
|
||||
if (child instanceof EmbedNode) {
|
||||
applyEmbedChildren(embed, child.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDatePropToISOString(value: string | number | Date) {
|
||||
return value instanceof Date
|
||||
? value.toISOString()
|
||||
: new Date(value).toISOString()
|
||||
}
|
||||
|
||||
function makeActionRows(root: Node) {
|
||||
const actionRows: Array<
|
||||
APIActionRowComponent<APIButtonComponent | APISelectMenuComponent>
|
||||
> = []
|
||||
|
||||
for (const node of root.children) {
|
||||
let currentRow = actionRows[actionRows.length - 1]
|
||||
if (
|
||||
!currentRow ||
|
||||
currentRow.components.length >= 5 ||
|
||||
currentRow.components[0]?.type === ComponentType.SelectMenu
|
||||
) {
|
||||
currentRow = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [],
|
||||
}
|
||||
actionRows.push(currentRow)
|
||||
}
|
||||
|
||||
if (node instanceof ButtonNode) {
|
||||
currentRow.components.push({
|
||||
type: ComponentType.Button,
|
||||
custom_id: node.customId,
|
||||
label: node.extractText(Number.POSITIVE_INFINITY),
|
||||
emoji: { name: node.props.emoji },
|
||||
style: translateButtonStyle(node.props.style ?? "secondary"),
|
||||
disabled: node.props.disabled,
|
||||
})
|
||||
}
|
||||
|
||||
if (node instanceof LinkNode) {
|
||||
currentRow.components.push({
|
||||
type: ComponentType.Button,
|
||||
label: node.extractText(Number.POSITIVE_INFINITY),
|
||||
url: node.props.url,
|
||||
style: ButtonStyle.Link,
|
||||
disabled: node.props.disabled,
|
||||
})
|
||||
}
|
||||
|
||||
if (node instanceof SelectNode) {
|
||||
const actionRow: APIActionRowComponent<APISelectMenuComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [],
|
||||
}
|
||||
actionRows.push(actionRow)
|
||||
|
||||
let selectedValues: string[] = []
|
||||
if (node.props.multiple && node.props.values) {
|
||||
selectedValues = node.props.values ?? []
|
||||
}
|
||||
if (!node.props.multiple && node.props.value != undefined) {
|
||||
selectedValues = [node.props.value]
|
||||
}
|
||||
|
||||
const options = [...node.children]
|
||||
.flatMap((child) => (child instanceof OptionNode ? child : []))
|
||||
.map<APISelectMenuOption>((child) => ({
|
||||
label: child.findInstanceOf(OptionLabelNode)?.extractText() ?? "",
|
||||
description: child
|
||||
.findInstanceOf(OptionDescriptionNode)
|
||||
?.extractText(),
|
||||
value: child.props.value,
|
||||
default: selectedValues.includes(child.props.value),
|
||||
emoji: { name: child.props.emoji },
|
||||
}))
|
||||
|
||||
const select: APISelectMenuComponent = {
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: node.customId,
|
||||
options,
|
||||
disabled: node.props.disabled,
|
||||
}
|
||||
|
||||
if (node.props.multiple) {
|
||||
select.min_values = node.props.minValues
|
||||
select.max_values = node.props.maxValues
|
||||
}
|
||||
|
||||
actionRow.components.push(select)
|
||||
}
|
||||
|
||||
if (node instanceof ActionRowNode) {
|
||||
actionRows.push(...makeActionRows(node))
|
||||
}
|
||||
}
|
||||
|
||||
return actionRows
|
||||
}
|
||||
|
||||
function translateButtonStyle(style: NonNullable<ButtonProps["style"]>) {
|
||||
const styleMap = {
|
||||
primary: ButtonStyle.Primary,
|
||||
secondary: ButtonStyle.Secondary,
|
||||
danger: ButtonStyle.Danger,
|
||||
success: ButtonStyle.Success,
|
||||
} as const
|
||||
return styleMap[style]
|
||||
}
|
||||
@@ -38,4 +38,20 @@ export class Node<Props = unknown> {
|
||||
yield* child.walk()
|
||||
}
|
||||
}
|
||||
|
||||
findInstanceOf<T extends Node>(
|
||||
cls: new (...args: any[]) => T,
|
||||
): T | undefined {
|
||||
for (const child of this.children) {
|
||||
if (child instanceof cls) return child
|
||||
}
|
||||
}
|
||||
|
||||
extractText(depth = 1): string {
|
||||
if (this instanceof TextNode) return this.props.text
|
||||
if (depth <= 0) return ""
|
||||
return this.children.map((child) => child.extractText(depth - 1)).join("")
|
||||
}
|
||||
}
|
||||
|
||||
export class TextNode extends Node<{ text: string }> {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
InteractionType,
|
||||
} from "discord.js"
|
||||
import * as React from "react"
|
||||
import { InstanceProvider } from "./core/instance-context.js"
|
||||
import { InstanceProvider } from "./core/instance-context"
|
||||
import type { ReacordInstance } from "./reacord-instance.js"
|
||||
import { ReacordInstancePrivate } from "./reacord-instance.js"
|
||||
import type { Renderer } from "./renderer.js"
|
||||
@@ -79,7 +79,7 @@ export class ReacordClient {
|
||||
|
||||
send(channelId: string, initialContent?: React.ReactNode): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new ChannelMessageRenderer(channelId),
|
||||
new ChannelMessageRenderer(channelId, this.client),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
@@ -127,7 +127,11 @@ export class ReacordClient {
|
||||
const publicInstance: ReacordInstance = {
|
||||
render: (content: React.ReactNode) => {
|
||||
instance.render(
|
||||
<InstanceProvider value={publicInstance}>{content}</InstanceProvider>,
|
||||
React.createElement(
|
||||
InstanceProvider,
|
||||
{ value: publicInstance },
|
||||
content,
|
||||
),
|
||||
)
|
||||
},
|
||||
deactivate: () => {
|
||||
@@ -4,14 +4,14 @@ import type {
|
||||
APIMessageComponentSelectMenuInteraction,
|
||||
} from "discord.js"
|
||||
import { ComponentType } from "discord.js"
|
||||
import { ButtonNode } from "./components/button.js"
|
||||
import type { SelectChangeEvent } from "./components/select.js"
|
||||
import { SelectNode } from "./components/select.js"
|
||||
import type { ComponentEvent } from "./core/component-event.js"
|
||||
import { reconciler } from "./internal/reconciler.js"
|
||||
import { Node } from "./node.js"
|
||||
import type { ReacordClient } from "./reacord-client.js"
|
||||
import type { Renderer } from "./renderer.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 type { Renderer } from "./renderer"
|
||||
|
||||
/**
|
||||
* Represents an interactive message, which can later be replaced or deleted.
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import { raise } from "@reacord/helpers/raise.js"
|
||||
import ReactReconciler from "react-reconciler"
|
||||
import { DefaultEventPriority } from "react-reconciler/constants"
|
||||
import { Node } from "./node.js"
|
||||
import { Node, TextNode } from "./node.js"
|
||||
import type { ReacordInstancePrivate } from "./reacord-instance.js"
|
||||
import { TextNode } from "./text-node.js"
|
||||
|
||||
export const reconciler = ReactReconciler<
|
||||
string, // Type,
|
||||
|
||||
@@ -1,35 +1,74 @@
|
||||
import { AsyncQueue } from "@reacord/helpers/async-queue"
|
||||
import type { Node } from "./internal/node.js"
|
||||
import type { Client, Message } from "discord.js"
|
||||
import { TextChannel } from "discord.js"
|
||||
import { makeMessageUpdatePayload } from "./make-message-update-payload.js"
|
||||
import type { Node } from "./node.js"
|
||||
import type { InteractionInfo } from "./reacord-client.js"
|
||||
|
||||
export type Renderer = {
|
||||
update(tree: Node<unknown>): Promise<void>
|
||||
update(tree: Node): Promise<void>
|
||||
deactivate(): Promise<void>
|
||||
destroy(): Promise<void>
|
||||
}
|
||||
|
||||
export class ChannelMessageRenderer implements Renderer {
|
||||
private readonly queue = new AsyncQueue()
|
||||
private channel: TextChannel | undefined
|
||||
private message: Message | undefined
|
||||
private active = true
|
||||
|
||||
constructor(private readonly channelId: string) {}
|
||||
constructor(
|
||||
private readonly channelId: string,
|
||||
private readonly client: Client,
|
||||
) {}
|
||||
|
||||
update(tree: Node<unknown>): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
private async getChannel(): Promise<TextChannel> {
|
||||
if (this.channel) return this.channel
|
||||
|
||||
const channel =
|
||||
this.client.channels.cache.get(this.channelId) ??
|
||||
(await this.client.channels.fetch(this.channelId))
|
||||
|
||||
if (!(channel instanceof TextChannel)) {
|
||||
throw new TypeError(`Channel ${this.channelId} is not a text channel`)
|
||||
}
|
||||
|
||||
this.channel = channel
|
||||
return channel
|
||||
}
|
||||
|
||||
deactivate(): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
update(tree: Node) {
|
||||
const payload = makeMessageUpdatePayload(tree)
|
||||
return this.queue.add(async () => {
|
||||
if (!this.active) return
|
||||
if (this.message) {
|
||||
await this.message.edit(payload)
|
||||
} else {
|
||||
const channel = await this.getChannel()
|
||||
this.message = await channel.send(payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
destroy(): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
async deactivate() {
|
||||
return this.queue.add(async () => {
|
||||
this.active = false
|
||||
// TODO: disable message components
|
||||
})
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
return this.queue.add(async () => {
|
||||
this.active = false
|
||||
await this.message?.delete()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class InteractionReplyRenderer implements Renderer {
|
||||
constructor(private readonly interaction: InteractionInfo) {}
|
||||
|
||||
update(tree: Node<unknown>): Promise<void> {
|
||||
update(tree: Node): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
@@ -45,7 +84,7 @@ export class InteractionReplyRenderer implements Renderer {
|
||||
export class EphemeralInteractionReplyRenderer implements Renderer {
|
||||
constructor(private readonly interaction: InteractionInfo) {}
|
||||
|
||||
update(tree: Node<unknown>): Promise<void> {
|
||||
update(tree: Node): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Node } from "./node.js"
|
||||
|
||||
export class TextNode extends Node<{ text: string }> {}
|
||||
@@ -38,7 +38,7 @@
|
||||
"scripts": {
|
||||
"build": "cp ../../README.md . && cp ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --dts --sourcemap",
|
||||
"build-watch": "pnpm build --watch",
|
||||
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
||||
"test-manual": "tsx watch ./scripts/discordjs-manual-test.tsx",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"release": "bash scripts/release.sh"
|
||||
},
|
||||
@@ -62,17 +62,17 @@
|
||||
"devDependencies": {
|
||||
"@reacord/helpers": "workspace:*",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"discord.js": "^14.0.3",
|
||||
"discord.js": "^14.1.2",
|
||||
"dotenv": "^16.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nodemon": "^2.0.19",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-ms": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"release-it": "^15.1.3",
|
||||
"tsup": "^6.1.3",
|
||||
"release-it": "^15.2.0",
|
||||
"tsup": "^6.2.1",
|
||||
"tsx": "^3.8.0",
|
||||
"type-fest": "^2.17.0",
|
||||
"type-fest": "^2.18.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -3,16 +3,17 @@ import "dotenv/config"
|
||||
import { kebabCase } from "lodash-es"
|
||||
import * as React from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
Option,
|
||||
ReacordDiscordJs,
|
||||
Select,
|
||||
useInstance,
|
||||
} from "../library/main"
|
||||
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"
|
||||
|
||||
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
||||
const reacord = new ReacordDiscordJs(client)
|
||||
|
||||
const reacord = new ReacordClient({
|
||||
token: process.env.TEST_BOT_TOKEN!,
|
||||
})
|
||||
|
||||
type TestCase = {
|
||||
name: string
|
||||
@@ -180,6 +181,7 @@ await Promise.all([
|
||||
tests.map(async (test, index) => {
|
||||
const channelName = getTestCaseChannelName(test, index)
|
||||
const channel = await getTestCaseChannel(channelName, index)
|
||||
console.info("running test:", test.name)
|
||||
await test.run(channel)
|
||||
}),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user