initial hacked-together draft
This commit is contained in:
211
packages/reacord/library.new/discord-js.ts
Normal file
211
packages/reacord/library.new/discord-js.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import type {
|
||||||
|
Client,
|
||||||
|
Interaction,
|
||||||
|
Message,
|
||||||
|
MessageEditOptions,
|
||||||
|
MessageOptions,
|
||||||
|
} from "discord.js"
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
import ReactReconciler from "react-reconciler"
|
||||||
|
import { DefaultEventPriority } from "react-reconciler/constants"
|
||||||
|
|
||||||
|
export function createReacordDiscordJs(client: Client) {
|
||||||
|
return {
|
||||||
|
send(channelId: string, initialContent?: ReactNode) {
|
||||||
|
let message: Message | undefined
|
||||||
|
|
||||||
|
const tree: MessageTree = {
|
||||||
|
children: [],
|
||||||
|
render: async () => {
|
||||||
|
const messageOptions: MessageOptions & MessageEditOptions = {
|
||||||
|
content: tree.children.map((child) => child.text).join(""),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (message) {
|
||||||
|
await message.edit(messageOptions)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = client.channels.cache.get(channelId)
|
||||||
|
if (!channel) {
|
||||||
|
channel = (await client.channels.fetch(channelId)) ?? undefined
|
||||||
|
}
|
||||||
|
if (!channel) {
|
||||||
|
throw new Error(`Channel ${channelId} not found`)
|
||||||
|
}
|
||||||
|
if (!channel.isTextBased()) {
|
||||||
|
throw new Error(`Channel ${channelId} is not a text channel`)
|
||||||
|
}
|
||||||
|
message = await channel.send(messageOptions)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Reacord encountered an error while rendering.",
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = reconciler.createContainer(
|
||||||
|
tree,
|
||||||
|
0,
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
null,
|
||||||
|
"reacord",
|
||||||
|
() => {},
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
render(content: ReactNode) {
|
||||||
|
reconciler.updateContainer(content, container)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialContent !== undefined) {
|
||||||
|
instance.render(initialContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
reply(interaction: Interaction, initialContent?: ReactNode) {},
|
||||||
|
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageTree = {
|
||||||
|
children: TextNode[]
|
||||||
|
render: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextNode = {
|
||||||
|
type: "text"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const reconciler = ReactReconciler<
|
||||||
|
string, // Type
|
||||||
|
Record<string, unknown>, // Props
|
||||||
|
MessageTree, // Container
|
||||||
|
never, // Instance
|
||||||
|
TextNode, // TextInstance
|
||||||
|
never, // SuspenseInstance
|
||||||
|
never, // HydratableInstance
|
||||||
|
never, // PublicInstance
|
||||||
|
{}, // HostContext
|
||||||
|
true, // UpdatePayload
|
||||||
|
never, // ChildSet
|
||||||
|
NodeJS.Timeout, // TimeoutHandle
|
||||||
|
-1 // NoTimeout
|
||||||
|
>({
|
||||||
|
isPrimaryRenderer: true,
|
||||||
|
supportsMutation: true,
|
||||||
|
supportsHydration: false,
|
||||||
|
supportsPersistence: false,
|
||||||
|
scheduleTimeout: setTimeout,
|
||||||
|
cancelTimeout: clearTimeout,
|
||||||
|
noTimeout: -1,
|
||||||
|
|
||||||
|
createInstance() {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
},
|
||||||
|
|
||||||
|
createTextInstance(text) {
|
||||||
|
return { type: "text", text }
|
||||||
|
},
|
||||||
|
|
||||||
|
appendInitialChild(parent, child) {},
|
||||||
|
|
||||||
|
appendChild(parentInstance, child) {},
|
||||||
|
|
||||||
|
appendChildToContainer(container, child) {
|
||||||
|
container.children.push(child)
|
||||||
|
},
|
||||||
|
|
||||||
|
insertBefore(parentInstance, child, beforeChild) {},
|
||||||
|
|
||||||
|
insertInContainerBefore(container, child, beforeChild) {
|
||||||
|
const index = container.children.indexOf(beforeChild)
|
||||||
|
if (index !== -1) container.children.splice(index, 0, child)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChild(parentInstance, child) {},
|
||||||
|
|
||||||
|
removeChildFromContainer(container, child) {
|
||||||
|
container.children = container.children.filter((c) => c !== child)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearContainer(container) {
|
||||||
|
container.children = []
|
||||||
|
},
|
||||||
|
|
||||||
|
commitTextUpdate(textInstance, oldText, newText) {
|
||||||
|
textInstance.text = newText
|
||||||
|
},
|
||||||
|
|
||||||
|
commitUpdate(
|
||||||
|
instance,
|
||||||
|
updatePayload,
|
||||||
|
type,
|
||||||
|
prevProps,
|
||||||
|
nextProps,
|
||||||
|
internalHandle,
|
||||||
|
) {},
|
||||||
|
|
||||||
|
prepareForCommit() {
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
resetAfterCommit(container) {
|
||||||
|
container.render()
|
||||||
|
},
|
||||||
|
|
||||||
|
finalizeInitialChildren() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareUpdate() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldSetTextContent() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
getRootHostContext() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildHostContext() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPublicInstance() {
|
||||||
|
throw new Error("Refs are not supported")
|
||||||
|
},
|
||||||
|
|
||||||
|
preparePortalMount() {},
|
||||||
|
|
||||||
|
getCurrentEventPriority() {
|
||||||
|
return DefaultEventPriority
|
||||||
|
},
|
||||||
|
|
||||||
|
getInstanceFromNode() {
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeActiveInstanceBlur() {},
|
||||||
|
afterActiveInstanceBlur() {},
|
||||||
|
prepareScopeUpdate() {},
|
||||||
|
getInstanceFromScope() {
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
detachDeletedInstance() {},
|
||||||
|
})
|
||||||
134
packages/reacord/scripts/discordjs-manual-test.old.tsx
Normal file
134
packages/reacord/scripts/discordjs-manual-test.old.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import type { TextChannel } from "discord.js"
|
||||||
|
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
||||||
|
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"
|
||||||
|
|
||||||
|
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
||||||
|
const reacord = new ReacordDiscordJs(client)
|
||||||
|
|
||||||
|
await client.login(process.env.TEST_BOT_TOKEN)
|
||||||
|
|
||||||
|
const guild = await client.guilds.fetch(process.env.TEST_GUILD_ID!)
|
||||||
|
|
||||||
|
const category = await guild.channels.fetch(process.env.TEST_CATEGORY_ID!)
|
||||||
|
if (category?.type !== ChannelType.GuildCategory) {
|
||||||
|
throw new Error(
|
||||||
|
`channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, channel] of category.children.cache) {
|
||||||
|
await channel.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = 0
|
||||||
|
const createTest = async (
|
||||||
|
name: string,
|
||||||
|
block: (channel: TextChannel) => void | Promise<unknown>,
|
||||||
|
) => {
|
||||||
|
prefix += 1
|
||||||
|
const channel = await category.children.create({
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
name: `${String(prefix).padStart(3, "0")}-${kebabCase(name)}`,
|
||||||
|
})
|
||||||
|
await block(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTest("basic", (channel) => {
|
||||||
|
reacord.send(channel.id, "Hello, world!")
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTest("counter", (channel) => {
|
||||||
|
const Counter = () => {
|
||||||
|
const [count, setCount] = React.useState(0)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
count: {count}
|
||||||
|
<Button
|
||||||
|
style="primary"
|
||||||
|
emoji="➕"
|
||||||
|
onClick={() => setCount(count + 1)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style="primary"
|
||||||
|
emoji="➖"
|
||||||
|
onClick={() => setCount(count - 1)}
|
||||||
|
/>
|
||||||
|
<Button label="reset" onClick={() => setCount(0)} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
reacord.send(channel.id, <Counter />)
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTest("select", (channel) => {
|
||||||
|
function FruitSelect({ onConfirm }: { onConfirm: (choice: string) => void }) {
|
||||||
|
const [value, setValue] = useState<string>()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
placeholder="choose a fruit"
|
||||||
|
value={value}
|
||||||
|
onChangeValue={setValue}
|
||||||
|
>
|
||||||
|
<Option value="🍎" emoji="🍎" label="apple" description="it red" />
|
||||||
|
<Option value="🍌" emoji="🍌" label="banana" description="bnanbna" />
|
||||||
|
<Option value="🍒" emoji="🍒" label="cherry" description="heh" />
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
label="confirm"
|
||||||
|
disabled={value == undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (value) onConfirm(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = reacord.send(
|
||||||
|
channel.id,
|
||||||
|
<FruitSelect
|
||||||
|
onConfirm={(value) => {
|
||||||
|
instance.render(`you chose ${value}`)
|
||||||
|
instance.deactivate()
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTest("ephemeral button", (channel) => {
|
||||||
|
reacord.send(
|
||||||
|
channel.id,
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label="public clic"
|
||||||
|
onClick={(event) =>
|
||||||
|
event.reply(`${event.guild?.member.displayName} clic`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="clic"
|
||||||
|
onClick={(event) => event.ephemeralReply("you clic")}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTest("delete this", (channel) => {
|
||||||
|
function DeleteThis() {
|
||||||
|
const instance = useInstance()
|
||||||
|
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||||
|
}
|
||||||
|
reacord.send(channel.id, <DeleteThis />)
|
||||||
|
})
|
||||||
@@ -2,18 +2,11 @@ import type { TextChannel } from "discord.js"
|
|||||||
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import { kebabCase } from "lodash-es"
|
import { kebabCase } from "lodash-es"
|
||||||
import * as React from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
import { useState } from "react"
|
import { createReacordDiscordJs } from "../library.new/discord-js"
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Option,
|
|
||||||
ReacordDiscordJs,
|
|
||||||
Select,
|
|
||||||
useInstance,
|
|
||||||
} from "../library/main"
|
|
||||||
|
|
||||||
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
||||||
const reacord = new ReacordDiscordJs(client)
|
const reacord = createReacordDiscordJs(client)
|
||||||
|
|
||||||
await client.login(process.env.TEST_BOT_TOKEN)
|
await client.login(process.env.TEST_BOT_TOKEN)
|
||||||
|
|
||||||
@@ -44,91 +37,18 @@ const createTest = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createTest("basic", (channel) => {
|
await createTest("basic", (channel) => {
|
||||||
reacord.send(channel.id, "Hello, world!")
|
function Timer() {
|
||||||
})
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
await createTest("counter", (channel) => {
|
useEffect(() => {
|
||||||
const Counter = () => {
|
const id = setInterval(() => {
|
||||||
const [count, setCount] = React.useState(0)
|
setCount((count) => count + 3)
|
||||||
return (
|
}, 3000)
|
||||||
<>
|
return () => clearInterval(id)
|
||||||
count: {count}
|
}, [])
|
||||||
<Button
|
|
||||||
style="primary"
|
|
||||||
emoji="➕"
|
|
||||||
onClick={() => setCount(count + 1)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
style="primary"
|
|
||||||
emoji="➖"
|
|
||||||
onClick={() => setCount(count - 1)}
|
|
||||||
/>
|
|
||||||
<Button label="reset" onClick={() => setCount(0)} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
reacord.send(channel.id, <Counter />)
|
|
||||||
})
|
|
||||||
|
|
||||||
await createTest("select", (channel) => {
|
return <>this component has been running for {count} seconds</>
|
||||||
function FruitSelect({ onConfirm }: { onConfirm: (choice: string) => void }) {
|
|
||||||
const [value, setValue] = useState<string>()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Select
|
|
||||||
placeholder="choose a fruit"
|
|
||||||
value={value}
|
|
||||||
onChangeValue={setValue}
|
|
||||||
>
|
|
||||||
<Option value="🍎" emoji="🍎" label="apple" description="it red" />
|
|
||||||
<Option value="🍌" emoji="🍌" label="banana" description="bnanbna" />
|
|
||||||
<Option value="🍒" emoji="🍒" label="cherry" description="heh" />
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
label="confirm"
|
|
||||||
disabled={value == undefined}
|
|
||||||
onClick={() => {
|
|
||||||
if (value) onConfirm(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = reacord.send(
|
reacord.send(channel.id, <Timer />)
|
||||||
channel.id,
|
|
||||||
<FruitSelect
|
|
||||||
onConfirm={(value) => {
|
|
||||||
instance.render(`you chose ${value}`)
|
|
||||||
instance.deactivate()
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
await createTest("ephemeral button", (channel) => {
|
|
||||||
reacord.send(
|
|
||||||
channel.id,
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
label="public clic"
|
|
||||||
onClick={(event) =>
|
|
||||||
event.reply(`${event.guild?.member.displayName} clic`)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="clic"
|
|
||||||
onClick={(event) => event.ephemeralReply("you clic")}
|
|
||||||
/>
|
|
||||||
</>,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
await createTest("delete this", (channel) => {
|
|
||||||
function DeleteThis() {
|
|
||||||
const instance = useInstance()
|
|
||||||
return <Button label="delete this" onClick={() => instance.destroy()} />
|
|
||||||
}
|
|
||||||
reacord.send(channel.id, <DeleteThis />)
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user