beginnings: rendering text to message
This commit is contained in:
28
src/container.ts
Normal file
28
src/container.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { TextBasedChannels } from "discord.js"
|
||||
import type { ReacordInstance } from "./instance.js"
|
||||
|
||||
export class ReacordContainer {
|
||||
channel: TextBasedChannels
|
||||
instances = new Set<ReacordInstance>()
|
||||
|
||||
constructor(channel: TextBasedChannels) {
|
||||
this.channel = channel
|
||||
}
|
||||
|
||||
add(instance: ReacordInstance) {
|
||||
this.instances.add(instance)
|
||||
instance.render(this.channel)
|
||||
}
|
||||
|
||||
remove(instance: ReacordInstance) {
|
||||
this.instances.delete(instance)
|
||||
instance.destroy()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.instances.forEach((instance) => {
|
||||
instance.destroy()
|
||||
})
|
||||
this.instances.clear()
|
||||
}
|
||||
}
|
||||
5
src/helpers/raise.ts
Normal file
5
src/helpers/raise.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { toError } from "./to-error.js"
|
||||
|
||||
export function raise(error: unknown): never {
|
||||
throw toError(error)
|
||||
}
|
||||
6
src/helpers/reject-after.ts
Normal file
6
src/helpers/reject-after.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
|
||||
export async function rejectAfter(timeMs: number) {
|
||||
await setTimeout(timeMs)
|
||||
return Promise.reject(`rejected after ${timeMs}ms`)
|
||||
}
|
||||
3
src/helpers/to-error.ts
Normal file
3
src/helpers/to-error.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function toError(value: unknown) {
|
||||
return value instanceof Error ? value : new Error(String(value))
|
||||
}
|
||||
1
src/helpers/types.ts
Normal file
1
src/helpers/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type MaybePromise<T> = T | Promise<T>
|
||||
10
src/helpers/wait-for-with-timeout.ts
Normal file
10
src/helpers/wait-for-with-timeout.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { rejectAfter } from "./reject-after.js"
|
||||
import type { MaybePromise } from "./types.js"
|
||||
import { waitFor } from "./wait-for.js"
|
||||
|
||||
export function waitForWithTimeout(
|
||||
condition: () => MaybePromise<boolean>,
|
||||
timeout = 1000,
|
||||
) {
|
||||
return Promise.race([waitFor(condition), rejectAfter(timeout)])
|
||||
}
|
||||
8
src/helpers/wait-for.ts
Normal file
8
src/helpers/wait-for.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
import type { MaybePromise } from "./types.js"
|
||||
|
||||
export async function waitFor(condition: () => MaybePromise<boolean>) {
|
||||
while (!(await condition())) {
|
||||
await setTimeout()
|
||||
}
|
||||
}
|
||||
25
src/instance.ts
Normal file
25
src/instance.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Message, TextBasedChannels } from "discord.js"
|
||||
|
||||
export class ReacordInstance {
|
||||
message?: Message
|
||||
content: string
|
||||
|
||||
constructor(content: string) {
|
||||
this.content = content
|
||||
}
|
||||
|
||||
render(channel: TextBasedChannels) {
|
||||
if (this.message) {
|
||||
this.message.edit(this.content).catch(console.error)
|
||||
} else {
|
||||
channel.send(this.content).then((message) => {
|
||||
this.message = message
|
||||
}, console.error)
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.message?.delete().catch(console.error)
|
||||
this.message?.channel.messages.cache.delete(this.message?.id)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import test from "ava"
|
||||
import { result } from "./main.js"
|
||||
|
||||
test("it is b", (t) => {
|
||||
t.deepEqual(result, ["b"])
|
||||
})
|
||||
@@ -1,4 +1 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { matchSorter } from "match-sorter"
|
||||
|
||||
export const result = matchSorter(["a", "b", "c"], "b")
|
||||
export * from "./render.js"
|
||||
|
||||
62
src/reconciler.ts
Normal file
62
src/reconciler.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import ReactReconciler from "react-reconciler"
|
||||
import type { ReacordContainer } from "./container.js"
|
||||
import { ReacordInstance } from "./instance.js"
|
||||
|
||||
export const reconciler = ReactReconciler<
|
||||
unknown,
|
||||
Record<string, unknown>,
|
||||
ReacordContainer,
|
||||
ReacordInstance,
|
||||
ReacordInstance,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown
|
||||
>({
|
||||
now: Date.now,
|
||||
supportsMutation: true,
|
||||
isPrimaryRenderer: true,
|
||||
noTimeout: -1,
|
||||
supportsHydration: false,
|
||||
supportsPersistence: false,
|
||||
|
||||
getRootHostContext: () => ({}),
|
||||
getChildHostContext: () => ({}),
|
||||
shouldSetTextContent: () => false,
|
||||
|
||||
createInstance: (
|
||||
type,
|
||||
props,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle,
|
||||
) => {
|
||||
throw new Error("Not implemented")
|
||||
},
|
||||
|
||||
createTextInstance: (
|
||||
text,
|
||||
rootContainerInstance,
|
||||
hostContext,
|
||||
internalInstanceHandle,
|
||||
) => {
|
||||
return new ReacordInstance(text)
|
||||
},
|
||||
|
||||
prepareForCommit: () => null,
|
||||
resetAfterCommit: () => null,
|
||||
|
||||
clearContainer: (container) => {
|
||||
container.clear()
|
||||
},
|
||||
appendChildToContainer: (container, child) => {
|
||||
container.add(child)
|
||||
},
|
||||
removeChildFromContainer: (container, child) => {
|
||||
container.remove(child)
|
||||
},
|
||||
})
|
||||
52
src/render.test.ts
Normal file
52
src/render.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import test from "ava"
|
||||
import { Client, TextChannel } from "discord.js"
|
||||
import { nanoid } from "nanoid"
|
||||
import { testBotToken, testChannelId } from "../test/env.js"
|
||||
import { raise } from "./helpers/raise.js"
|
||||
import { waitForWithTimeout } from "./helpers/wait-for-with-timeout.js"
|
||||
import { render } from "./render.js"
|
||||
|
||||
const client = new Client({
|
||||
intents: ["GUILDS"],
|
||||
})
|
||||
|
||||
let channel: TextChannel
|
||||
|
||||
test.before(async () => {
|
||||
await client.login(testBotToken)
|
||||
|
||||
const result =
|
||||
client.channels.cache.get(testChannelId) ??
|
||||
(await client.channels.fetch(testChannelId)) ??
|
||||
raise("Channel not found")
|
||||
|
||||
if (!(result instanceof TextChannel)) {
|
||||
throw new Error("Channel must be a text channel")
|
||||
}
|
||||
|
||||
channel = result
|
||||
})
|
||||
|
||||
test.after(() => {
|
||||
client.destroy()
|
||||
})
|
||||
|
||||
test("rendering text", async (t) => {
|
||||
const content = nanoid()
|
||||
const root = render(content, channel)
|
||||
|
||||
await waitForWithTimeout(
|
||||
() => channel.messages.cache.some((m) => m.content === content),
|
||||
4000,
|
||||
)
|
||||
|
||||
root.destroy()
|
||||
|
||||
await waitForWithTimeout(() => {
|
||||
return channel.messages.cache
|
||||
.filter((m) => !m.deleted)
|
||||
.every((m) => m.content !== content)
|
||||
}, 4000)
|
||||
|
||||
t.pass()
|
||||
})
|
||||
16
src/render.ts
Normal file
16
src/render.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { TextBasedChannels } from "discord.js"
|
||||
import { ReacordContainer } from "./container"
|
||||
import { reconciler } from "./reconciler"
|
||||
|
||||
export type ReacordRenderTarget = TextBasedChannels
|
||||
|
||||
export function render(content: string, target: ReacordRenderTarget) {
|
||||
const container = new ReacordContainer(target)
|
||||
const containerId = reconciler.createContainer(container, 0, false, null)
|
||||
reconciler.updateContainer(content, containerId)
|
||||
return {
|
||||
destroy: () => {
|
||||
reconciler.updateContainer(null, containerId)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user