From 3682f67bfe78c72b0d3c33418865bc325b09124f Mon Sep 17 00:00:00 2001 From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Mon, 27 Dec 2021 19:22:21 -0600 Subject: [PATCH] add channel renderer + try to simplify adapter generics --- library/core/adapters/adapter.ts | 17 ++++++++-- library/core/adapters/discord-js-adapter.ts | 21 +++++++++++- library/core/reacord.ts | 34 +++++++++++++------- library/internal/channel-message-renderer.ts | 13 ++++++++ library/internal/channel.ts | 5 +++ library/internal/command-reply-renderer.ts | 22 +++++++++++++ library/internal/reconciler.ts | 2 +- library/internal/renderer.ts | 30 ++++++----------- library/testing.ts | 28 +++++++++++++++- playground/main.tsx | 4 +-- test/channel-message-renderer.tsx | 2 ++ test/reacord.test.tsx | 4 +-- test/setup-testing.ts | 4 +-- 13 files changed, 143 insertions(+), 43 deletions(-) create mode 100644 library/internal/channel-message-renderer.ts create mode 100644 library/internal/channel.ts create mode 100644 library/internal/command-reply-renderer.ts create mode 100644 test/channel-message-renderer.tsx diff --git a/library/core/adapters/adapter.ts b/library/core/adapters/adapter.ts index 3eb3434..beef898 100644 --- a/library/core/adapters/adapter.ts +++ b/library/core/adapters/adapter.ts @@ -1,9 +1,15 @@ +import type { Channel } from "../../internal/channel" import type { CommandInteraction, ComponentInteraction, } from "../../internal/interaction" -export type Adapter = { +export type AdapterGenerics = { + commandReplyInit: unknown + channelInit: unknown +} + +export type Adapter = { /** * @internal */ @@ -14,5 +20,12 @@ export type Adapter = { /** * @internal */ - createCommandInteraction(init: CommandReplyInit): CommandInteraction + createCommandInteraction( + init: Generics["commandReplyInit"], + ): CommandInteraction + + /** + * @internal + */ + createChannel(init: Generics["channelInit"]): Channel } diff --git a/library/core/adapters/discord-js-adapter.ts b/library/core/adapters/discord-js-adapter.ts index dff7a06..8be3fd4 100644 --- a/library/core/adapters/discord-js-adapter.ts +++ b/library/core/adapters/discord-js-adapter.ts @@ -1,6 +1,7 @@ import type * as Discord from "discord.js" import { raise } from "../../../helpers/raise" import { toUpper } from "../../../helpers/to-upper" +import type { Channel } from "../../internal/channel" import type { CommandInteraction, ComponentInteraction, @@ -8,7 +9,12 @@ import type { import type { Message, MessageOptions } from "../../internal/message" import type { Adapter } from "./adapter" -export class DiscordJsAdapter implements Adapter { +type DiscordJsAdapterGenerics = { + commandReplyInit: Discord.CommandInteraction + channelInit: Discord.TextBasedChannel +} + +export class DiscordJsAdapter implements Adapter { constructor(private client: Discord.Client) {} /** @@ -51,6 +57,19 @@ export class DiscordJsAdapter implements Adapter { }, } } + + /** + * @internal + */ + // eslint-disable-next-line class-methods-use-this + createChannel(channel: Discord.TextBasedChannel): Channel { + return { + send: async (options) => { + const message = await channel.send(getDiscordMessageOptions(options)) + return createReacordMessage(message) + }, + } + } } function createReacordComponentInteraction( diff --git a/library/core/reacord.ts b/library/core/reacord.ts index 8e23191..9ee41d7 100644 --- a/library/core/reacord.ts +++ b/library/core/reacord.ts @@ -1,10 +1,12 @@ import type { ReactNode } from "react" +import { ChannelMessageRenderer } from "../internal/channel-message-renderer" +import { CommandReplyRenderer } from "../internal/command-reply-renderer.js" import { reconciler } from "../internal/reconciler.js" -import { Renderer } from "../internal/renderer.js" -import type { Adapter } from "./adapters/adapter" +import type { Renderer } from "../internal/renderer" +import type { Adapter, AdapterGenerics } from "./adapters/adapter" -export type ReacordConfig = { - adapter: Adapter +export type ReacordConfig = { + adapter: Adapter /** * The max number of active instances. @@ -19,10 +21,10 @@ export type ReacordInstance = { destroy: () => void } -export class Reacord { +export class Reacord { private renderers: Renderer[] = [] - constructor(private readonly config: ReacordConfig) { + constructor(private readonly config: ReacordConfig) { config.adapter.addComponentInteractionListener((interaction) => { for (const renderer of this.renderers) { if (renderer.handleComponentInteraction(interaction)) return @@ -34,15 +36,25 @@ export class Reacord { return this.config.maxInstances ?? 50 } - createCommandReply(target: InteractionInit): ReacordInstance { + send(init: Generics["channelInit"]): ReacordInstance { + return this.createInstance( + new ChannelMessageRenderer(this.config.adapter.createChannel(init)), + ) + } + + reply(init: Generics["commandReplyInit"]): ReacordInstance { + return this.createInstance( + new CommandReplyRenderer( + this.config.adapter.createCommandInteraction(init), + ), + ) + } + + private createInstance(renderer: Renderer) { 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, {}) diff --git a/library/internal/channel-message-renderer.ts b/library/internal/channel-message-renderer.ts new file mode 100644 index 0000000..8368082 --- /dev/null +++ b/library/internal/channel-message-renderer.ts @@ -0,0 +1,13 @@ +import type { Channel } from "./channel" +import type { Message, MessageOptions } from "./message" +import { Renderer } from "./renderer" + +export class ChannelMessageRenderer extends Renderer { + constructor(private channel: Channel) { + super() + } + + protected createMessage(options: MessageOptions): Promise { + return this.channel.send(options) + } +} diff --git a/library/internal/channel.ts b/library/internal/channel.ts new file mode 100644 index 0000000..b574496 --- /dev/null +++ b/library/internal/channel.ts @@ -0,0 +1,5 @@ +import type { Message, MessageOptions } from "./message" + +export type Channel = { + send(message: MessageOptions): Promise +} diff --git a/library/internal/command-reply-renderer.ts b/library/internal/command-reply-renderer.ts new file mode 100644 index 0000000..77e263a --- /dev/null +++ b/library/internal/command-reply-renderer.ts @@ -0,0 +1,22 @@ +import type { CommandInteraction } from "./interaction" +import type { Message, MessageOptions } from "./message" +import { Renderer } from "./renderer" + +// keep track of interaction ids which have replies, +// so we know whether to call reply() or followUp() +const repliedInteractionIds = new Set() + +export class CommandReplyRenderer extends Renderer { + constructor(private interaction: CommandInteraction) { + super() + } + + protected createMessage(options: MessageOptions): Promise { + if (repliedInteractionIds.has(this.interaction.id)) { + return this.interaction.followUp(options) + } + + repliedInteractionIds.add(this.interaction.id) + return this.interaction.reply(options) + } +} diff --git a/library/internal/reconciler.ts b/library/internal/reconciler.ts index f8e845d..9e397a6 100644 --- a/library/internal/reconciler.ts +++ b/library/internal/reconciler.ts @@ -2,7 +2,7 @@ import type { HostConfig } from "react-reconciler" import ReactReconciler from "react-reconciler" import { raise } from "../../helpers/raise.js" import { Node } from "./node.js" -import type { Renderer } from "./renderer.js" +import type { Renderer } from "./renderer" import { TextNode } from "./text-node.js" const config: HostConfig< diff --git a/library/internal/renderer.ts b/library/internal/renderer.ts index f2ad0b1..61177f6 100644 --- a/library/internal/renderer.ts +++ b/library/internal/renderer.ts @@ -1,32 +1,24 @@ -import type { Subscription } from "rxjs" import { Subject } from "rxjs" import { concatMap } from "rxjs/operators" import { Container } from "./container.js" -import type { CommandInteraction, ComponentInteraction } from "./interaction" +import type { ComponentInteraction } from "./interaction" import type { Message, MessageOptions } from "./message" import type { Node } from "./node.js" -// keep track of interaction ids which have replies, -// so we know whether to call reply() or followUp() -const repliedInteractionIds = new Set() - type UpdatePayload = | { action: "update" | "deactivate"; options: MessageOptions } | { action: "destroy" } -export class Renderer { +export abstract class Renderer { readonly nodes = new Container>() private componentInteraction?: ComponentInteraction private message?: Message - private updates = new Subject() - private updateSubscription: Subscription private active = true + private updates = new Subject() - constructor(private interaction: CommandInteraction) { - this.updateSubscription = this.updates - .pipe(concatMap((payload) => this.updateMessage(payload))) - .subscribe({ error: console.error }) - } + private updateSubscription = this.updates + .pipe(concatMap((payload) => this.updateMessage(payload))) + .subscribe({ error: console.error }) render() { if (!this.active) { @@ -62,6 +54,8 @@ export class Renderer { } } + protected abstract createMessage(options: MessageOptions): Promise + private getMessageOptions(): MessageOptions { const options: MessageOptions = { content: "", @@ -99,12 +93,6 @@ export class Renderer { return } - if (repliedInteractionIds.has(this.interaction.id)) { - this.message = await this.interaction.followUp(payload.options) - return - } - - repliedInteractionIds.add(this.interaction.id) - this.message = await this.interaction.reply(payload.options) + this.message = await this.createMessage(payload.options) } } diff --git a/library/testing.ts b/library/testing.ts index 8f8cd48..05cb745 100644 --- a/library/testing.ts +++ b/library/testing.ts @@ -3,6 +3,7 @@ import { nanoid } from "nanoid" import { raise } from "../helpers/raise" import type { Adapter } from "./core/adapters/adapter" +import type { Channel } from "./internal/channel" import type { ButtonInteraction, CommandInteraction, @@ -16,9 +17,20 @@ import type { MessageSelectOptions, } from "./internal/message" -export class TestAdapter implements Adapter { +type TestAdapterGenerics = { + commandReplyInit: TestCommandInteraction + channelInit: TestChannel +} + +export class TestAdapter implements Adapter { messages: TestMessage[] = [] + private constructor() {} + + static create(): Adapter & TestAdapter { + return new TestAdapter() + } + private componentInteractionListener: ( interaction: ComponentInteraction, ) => void = () => {} @@ -35,6 +47,10 @@ export class TestAdapter implements Adapter { return interaction } + createChannel(channel: TestChannel): Channel { + return channel + } + findButtonByLabel(label: string) { for (const message of this.messages) { for (const component of message.options.actionRows.flat()) { @@ -162,3 +178,13 @@ export class TestSelectInteraction implements SelectInteraction { this.message.options = options } } + +export class TestChannel implements Channel { + constructor(private adapter: TestAdapter) {} + + async send(messageOptions: MessageOptions): Promise { + const message = new TestMessage(messageOptions, this.adapter) + this.adapter.messages.push(message) + return message + } +} diff --git a/playground/main.tsx b/playground/main.tsx index 2893acd..f84cb69 100644 --- a/playground/main.tsx +++ b/playground/main.tsx @@ -20,7 +20,7 @@ createCommandHandler(client, [ name: "counter", description: "shows a counter button", run: (interaction) => { - const reply = reacord.createCommandReply(interaction) + const reply = reacord.reply(interaction) reply.render( reply.destroy()} />) }, }, @@ -28,7 +28,7 @@ createCommandHandler(client, [ name: "select", description: "shows a select", run: (interaction) => { - reacord.createCommandReply(interaction).render() + reacord.reply(interaction).render() }, }, ]) diff --git a/test/channel-message-renderer.tsx b/test/channel-message-renderer.tsx new file mode 100644 index 0000000..e5e96f3 --- /dev/null +++ b/test/channel-message-renderer.tsx @@ -0,0 +1,2 @@ +test.todo("channel message renderer") +export {} diff --git a/test/reacord.test.tsx b/test/reacord.test.tsx index 922cf84..030570e 100644 --- a/test/reacord.test.tsx +++ b/test/reacord.test.tsx @@ -6,7 +6,7 @@ import { setupReacordTesting } from "./setup-testing" test("rendering behavior", async () => { const { reacord, adapter, assertMessages } = setupReacordTesting() - const reply = reacord.createCommandReply(new TestCommandInteraction(adapter)) + const reply = reacord.reply(new TestCommandInteraction(adapter)) reply.render( reply.deactivate()} />) await assertMessages([ @@ -244,7 +244,7 @@ test("rendering behavior", async () => { test("delete", async () => { const { reacord, adapter, assertMessages } = setupReacordTesting() - const reply = reacord.createCommandReply(new TestCommandInteraction(adapter)) + const reply = reacord.reply(new TestCommandInteraction(adapter)) reply.render( <> some text diff --git a/test/setup-testing.ts b/test/setup-testing.ts index 5d23075..e2f331b 100644 --- a/test/setup-testing.ts +++ b/test/setup-testing.ts @@ -9,9 +9,9 @@ import { TestAdapter, TestCommandInteraction } from "../library/testing" const nextTickPromise = promisify(nextTick) export function setupReacordTesting() { - const adapter = new TestAdapter() + const adapter = TestAdapter.create() const reacord = new Reacord({ adapter }) - const reply = reacord.createCommandReply(new TestCommandInteraction(adapter)) + const reply = reacord.reply(new TestCommandInteraction(adapter)) async function assertMessages(expected: ReturnType) { await nextTickPromise() // wait for the render to complete