move stuff around
This commit is contained in:
18
library/core/adapters/adapter.ts
Normal file
18
library/core/adapters/adapter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type {
|
||||
CommandInteraction,
|
||||
ComponentInteraction,
|
||||
} from "../../internal/interaction"
|
||||
|
||||
export type Adapter<CommandReplyInit> = {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
addComponentInteractionListener(
|
||||
listener: (interaction: ComponentInteraction) => void,
|
||||
): void
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
createCommandInteraction(init: CommandReplyInit): CommandInteraction
|
||||
}
|
||||
112
library/core/adapters/discord-js-adapter.ts
Normal file
112
library/core/adapters/discord-js-adapter.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type * as Discord from "discord.js"
|
||||
import { raise } from "../../../helpers/raise"
|
||||
import { toUpper } from "../../../helpers/to-upper"
|
||||
import type {
|
||||
CommandInteraction,
|
||||
ComponentInteraction,
|
||||
} from "../../internal/interaction"
|
||||
import type { Message, MessageOptions } from "../../internal/message"
|
||||
import type { Adapter } from "./adapter"
|
||||
|
||||
export class DiscordJsAdapter implements Adapter<Discord.CommandInteraction> {
|
||||
constructor(private client: Discord.Client) {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
addComponentInteractionListener(
|
||||
listener: (interaction: ComponentInteraction) => void,
|
||||
) {
|
||||
this.client.on("interactionCreate", (interaction) => {
|
||||
if (interaction.isButton()) {
|
||||
listener(createReacordComponentInteraction(interaction))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
createCommandInteraction(
|
||||
interaction: Discord.CommandInteraction,
|
||||
): CommandInteraction {
|
||||
return {
|
||||
type: "command",
|
||||
id: interaction.id,
|
||||
channelId: interaction.channelId,
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createReacordComponentInteraction(
|
||||
interaction: Discord.MessageComponentInteraction,
|
||||
): ComponentInteraction {
|
||||
return {
|
||||
type: "button",
|
||||
id: interaction.id,
|
||||
channelId: interaction.channelId,
|
||||
customId: interaction.customId,
|
||||
update: async (options) => {
|
||||
await interaction.update(getDiscordMessageOptions(options))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createReacordMessage(message: Discord.Message): Message {
|
||||
return {
|
||||
edit: async (options) => {
|
||||
await message.edit(getDiscordMessageOptions(options))
|
||||
},
|
||||
disableComponents: async () => {
|
||||
for (const actionRow of message.components) {
|
||||
for (const component of actionRow.components) {
|
||||
component.setDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
await message.edit({
|
||||
components: message.components,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function getDiscordMessageOptions(
|
||||
options: MessageOptions,
|
||||
): Discord.MessageOptions {
|
||||
return {
|
||||
content: options.content,
|
||||
embeds: options.embeds,
|
||||
components: options.actionRows.map((row) => ({
|
||||
type: "ACTION_ROW",
|
||||
components: row.map((component) => {
|
||||
if (component.type === "button") {
|
||||
return {
|
||||
type: "BUTTON",
|
||||
customId: component.customId,
|
||||
label: component.label ?? "",
|
||||
style: toUpper(component.style ?? "secondary"),
|
||||
disabled: component.disabled,
|
||||
emoji: component.emoji,
|
||||
}
|
||||
}
|
||||
raise(`Unsupported component type: ${component.type}`)
|
||||
}),
|
||||
})),
|
||||
}
|
||||
}
|
||||
62
library/core/components/button.tsx
Normal file
62
library/core/components/button.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { nanoid } from "nanoid"
|
||||
import React from "react"
|
||||
import { last } from "../../../helpers/last.js"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import type { ComponentInteraction } from "../../internal/interaction"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { Node } from "../../internal/node.js"
|
||||
|
||||
export type ButtonProps = {
|
||||
label?: string
|
||||
style?: "primary" | "secondary" | "success" | "danger"
|
||||
disabled?: boolean
|
||||
emoji?: string
|
||||
onClick: (event: ButtonClickEvent) => void
|
||||
}
|
||||
|
||||
export type ButtonClickEvent = {}
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new ButtonNode(props)} />
|
||||
)
|
||||
}
|
||||
|
||||
class ButtonNode extends Node<ButtonProps> {
|
||||
private customId = nanoid()
|
||||
|
||||
override modifyMessageOptions(options: MessageOptions): void {
|
||||
options.actionRows ??= []
|
||||
|
||||
let actionRow = last(options.actionRows)
|
||||
|
||||
if (
|
||||
actionRow == undefined ||
|
||||
actionRow.length >= 5 ||
|
||||
actionRow[0]?.type === "select"
|
||||
) {
|
||||
actionRow = []
|
||||
options.actionRows.push(actionRow)
|
||||
}
|
||||
|
||||
actionRow.push({
|
||||
type: "button",
|
||||
customId: this.customId,
|
||||
style: this.props.style ?? "secondary",
|
||||
disabled: this.props.disabled,
|
||||
emoji: this.props.emoji,
|
||||
label: this.props.label,
|
||||
})
|
||||
}
|
||||
|
||||
override handleComponentInteraction(interaction: ComponentInteraction) {
|
||||
if (
|
||||
interaction.type === "button" &&
|
||||
interaction.customId === this.customId
|
||||
) {
|
||||
this.props.onClick(interaction)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
30
library/core/components/embed-author.tsx
Normal file
30
library/core/components/embed-author.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export type EmbedAuthorProps = {
|
||||
name?: string
|
||||
children?: string
|
||||
url?: string
|
||||
iconUrl?: string
|
||||
}
|
||||
|
||||
export function EmbedAuthor(props: EmbedAuthorProps) {
|
||||
return (
|
||||
<ReacordElement
|
||||
props={props}
|
||||
createNode={() => new EmbedAuthorNode(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.author = {
|
||||
name: this.props.name ?? this.props.children ?? "",
|
||||
url: this.props.url,
|
||||
icon_url: this.props.iconUrl,
|
||||
}
|
||||
}
|
||||
}
|
||||
6
library/core/components/embed-child.ts
Normal file
6
library/core/components/embed-child.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Node } from "../../internal/node.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export abstract class EmbedChildNode<Props> extends Node<Props> {
|
||||
abstract modifyEmbedOptions(options: EmbedOptions): void
|
||||
}
|
||||
31
library/core/components/embed-field.tsx
Normal file
31
library/core/components/embed-field.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export type EmbedFieldProps = {
|
||||
name: string
|
||||
value?: string
|
||||
inline?: boolean
|
||||
children?: string
|
||||
}
|
||||
|
||||
export function EmbedField(props: EmbedFieldProps) {
|
||||
return (
|
||||
<ReacordElement
|
||||
props={props}
|
||||
createNode={() => new EmbedFieldNode(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.fields ??= []
|
||||
options.fields.push({
|
||||
name: this.props.name,
|
||||
value: this.props.value ?? this.props.children ?? "",
|
||||
inline: this.props.inline,
|
||||
})
|
||||
}
|
||||
}
|
||||
32
library/core/components/embed-footer.tsx
Normal file
32
library/core/components/embed-footer.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export type EmbedFooterProps = {
|
||||
text?: string
|
||||
children?: string
|
||||
iconUrl?: string
|
||||
timestamp?: string | number | Date
|
||||
}
|
||||
|
||||
export function EmbedFooter(props: EmbedFooterProps) {
|
||||
return (
|
||||
<ReacordElement
|
||||
props={props}
|
||||
createNode={() => new EmbedFooterNode(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedFooterNode extends EmbedChildNode<EmbedFooterProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.footer = {
|
||||
text: this.props.text ?? this.props.children ?? "",
|
||||
icon_url: this.props.iconUrl,
|
||||
}
|
||||
options.timestamp = this.props.timestamp
|
||||
? new Date(this.props.timestamp).toISOString()
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
23
library/core/components/embed-image.tsx
Normal file
23
library/core/components/embed-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export type EmbedImageProps = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export function EmbedImage(props: EmbedImageProps) {
|
||||
return (
|
||||
<ReacordElement
|
||||
props={props}
|
||||
createNode={() => new EmbedImageNode(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedImageNode extends EmbedChildNode<EmbedImageProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.image = { url: this.props.url }
|
||||
}
|
||||
}
|
||||
8
library/core/components/embed-options.ts
Normal file
8
library/core/components/embed-options.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
|
||||
import type { EmbedProps } from "./embed"
|
||||
|
||||
export type EmbedOptions = SnakeCasedPropertiesDeep<
|
||||
Except<EmbedProps, "timestamp" | "children"> & {
|
||||
timestamp?: string
|
||||
}
|
||||
>
|
||||
23
library/core/components/embed-thumbnail.tsx
Normal file
23
library/core/components/embed-thumbnail.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export type EmbedThumbnailProps = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export function EmbedThumbnail(props: EmbedThumbnailProps) {
|
||||
return (
|
||||
<ReacordElement
|
||||
props={props}
|
||||
createNode={() => new EmbedThumbnailNode(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedThumbnailNode extends EmbedChildNode<EmbedThumbnailProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.thumbnail = { url: this.props.url }
|
||||
}
|
||||
}
|
||||
25
library/core/components/embed-title.tsx
Normal file
25
library/core/components/embed-title.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
export type EmbedTitleProps = {
|
||||
children: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export function EmbedTitle(props: EmbedTitleProps) {
|
||||
return (
|
||||
<ReacordElement
|
||||
props={props}
|
||||
createNode={() => new EmbedTitleNode(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class EmbedTitleNode extends EmbedChildNode<EmbedTitleProps> {
|
||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||
options.title = this.props.children
|
||||
options.url = this.props.url
|
||||
}
|
||||
}
|
||||
50
library/core/components/embed.tsx
Normal file
50
library/core/components/embed.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from "react"
|
||||
import { snakeCaseDeep } from "../../../helpers/convert-object-property-case"
|
||||
import { omit } from "../../../helpers/omit"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { Node } from "../../internal/node.js"
|
||||
import { EmbedChildNode } from "./embed-child.js"
|
||||
import type { EmbedOptions } from "./embed-options"
|
||||
|
||||
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?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Embed(props: EmbedProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new EmbedNode(props)}>
|
||||
{props.children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
options.embeds.push(embed)
|
||||
}
|
||||
}
|
||||
40
library/core/components/link.tsx
Normal file
40
library/core/components/link.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react"
|
||||
import { last } from "../../../helpers/last.js"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { Node } from "../../internal/node.js"
|
||||
|
||||
export type LinkProps = {
|
||||
label?: string
|
||||
children?: string
|
||||
emoji?: string
|
||||
disabled?: boolean
|
||||
url: string
|
||||
}
|
||||
|
||||
export function Link(props: LinkProps) {
|
||||
return <ReacordElement props={props} createNode={() => new LinkNode(props)} />
|
||||
}
|
||||
|
||||
class LinkNode extends Node<LinkProps> {
|
||||
override modifyMessageOptions(options: MessageOptions): void {
|
||||
let actionRow = last(options.actionRows)
|
||||
|
||||
if (
|
||||
actionRow == undefined ||
|
||||
actionRow.length >= 5 ||
|
||||
actionRow[0]?.type === "select"
|
||||
) {
|
||||
actionRow = []
|
||||
options.actionRows.push(actionRow)
|
||||
}
|
||||
|
||||
actionRow.push({
|
||||
type: "link",
|
||||
disabled: this.props.disabled,
|
||||
emoji: this.props.emoji,
|
||||
label: this.props.label || this.props.children,
|
||||
url: this.props.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
63
library/core/reacord.ts
Normal file
63
library/core/reacord.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { ReactNode } from "react"
|
||||
import { reconciler } from "../internal/reconciler.js"
|
||||
import { Renderer } from "../internal/renderer.js"
|
||||
import type { Adapter } from "./adapters/adapter"
|
||||
|
||||
export type ReacordConfig<InteractionInit> = {
|
||||
adapter: Adapter<InteractionInit>
|
||||
|
||||
/**
|
||||
* The max number of active instances.
|
||||
* When this limit is exceeded, the oldest instances will be disabled.
|
||||
*/
|
||||
maxInstances?: number
|
||||
}
|
||||
|
||||
export type ReacordInstance = {
|
||||
render: (content: ReactNode) => void
|
||||
deactivate: () => void
|
||||
}
|
||||
|
||||
export class Reacord<InteractionInit> {
|
||||
private renderers: Renderer[] = []
|
||||
|
||||
constructor(private readonly config: ReacordConfig<InteractionInit>) {
|
||||
config.adapter.addComponentInteractionListener((interaction) => {
|
||||
for (const renderer of this.renderers) {
|
||||
if (renderer.handleComponentInteraction(interaction)) return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private get maxInstances() {
|
||||
return this.config.maxInstances ?? 50
|
||||
}
|
||||
|
||||
createCommandReply(target: InteractionInit): ReacordInstance {
|
||||
if (this.renderers.length > this.maxInstances) {
|
||||
this.deactivate(this.renderers[0]!)
|
||||
}
|
||||
|
||||
const renderer = new Renderer(
|
||||
this.config.adapter.createCommandInteraction(target),
|
||||
)
|
||||
|
||||
this.renderers.push(renderer)
|
||||
|
||||
const container = reconciler.createContainer(renderer, 0, false, {})
|
||||
|
||||
return {
|
||||
render: (content: ReactNode) => {
|
||||
reconciler.updateContainer(content, container)
|
||||
},
|
||||
deactivate: () => {
|
||||
this.deactivate(renderer)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private deactivate(renderer: Renderer) {
|
||||
this.renderers = this.renderers.filter((it) => it !== renderer)
|
||||
renderer.deactivate()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user