untested rewrite

This commit is contained in:
itsMapleLeaf
2022-08-04 10:29:06 -05:00
parent 5852b4a616
commit 14d6f87dda
24 changed files with 640 additions and 508 deletions

View File

@@ -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<{}> {}

View File

@@ -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"
/**

View File

@@ -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> {}

View File

@@ -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<{}> {}

View File

@@ -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<{}> {}

View File

@@ -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> {}

View File

@@ -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> {}

View File

@@ -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">> {}

View File

@@ -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)
// }
}

View File

@@ -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">> {}

View File

@@ -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<{}> {}

View File

@@ -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<{}> {}

View File

@@ -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
}
}

View 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]
}

View File

@@ -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 }> {}

View File

@@ -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: () => {

View File

@@ -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.

View File

@@ -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,

View File

@@ -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.")
}

View File

@@ -1,3 +0,0 @@
import { Node } from "./node.js"
export class TextNode extends Node<{ text: string }> {}

View File

@@ -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": {

View File

@@ -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)
}),
),