Merge pull request #40 from domin-mnd/main
This commit is contained in:
@@ -33,15 +33,23 @@ export interface ComponentEvent {
|
||||
guild?: GuildInfo
|
||||
|
||||
/** Create a new reply to this event. */
|
||||
reply(content?: ReactNode): ReacordInstance
|
||||
reply(content?: ReactNode, options?: ReplyInfo): ReacordInstance
|
||||
|
||||
/**
|
||||
* Create an ephemeral reply to this event, shown only to the user who
|
||||
* triggered it.
|
||||
*
|
||||
* @deprecated Use event.reply(content, { ephemeral: true })
|
||||
*/
|
||||
ephemeralReply(content?: ReactNode): ReacordInstance
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface ReplyInfo {
|
||||
ephemeral?: boolean
|
||||
tts?: boolean
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface ChannelInfo {
|
||||
id: string
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ReactNode } from "react"
|
||||
*/
|
||||
export interface ReacordInstance {
|
||||
/** Render some JSX to this instance (edits the message) */
|
||||
render: (content: ReactNode) => void
|
||||
render: (content: ReactNode) => ReacordInstance
|
||||
|
||||
/** Remove this message */
|
||||
destroy: () => void
|
||||
|
||||
@@ -18,19 +18,49 @@ import type {
|
||||
GuildInfo,
|
||||
GuildMemberInfo,
|
||||
MessageInfo,
|
||||
ReplyInfo,
|
||||
UserInfo,
|
||||
} from "./component-event"
|
||||
import type { ReacordInstance } from "./instance"
|
||||
import type { ReacordConfig } from "./reacord"
|
||||
import { Reacord } from "./reacord"
|
||||
|
||||
interface SendOptions {
|
||||
/**
|
||||
* Options for the channel message.
|
||||
*
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
export interface LegacyCreateChannelMessageOptions
|
||||
extends CreateChannelMessageOptions {
|
||||
/**
|
||||
* Send message as a reply. Requires the use of message event instead of
|
||||
* channel id provided as argument.
|
||||
*
|
||||
* @deprecated Use reacord.createMessageReply()
|
||||
*/
|
||||
reply?: boolean
|
||||
}
|
||||
|
||||
interface ReplyOptions {
|
||||
ephemeral?: boolean
|
||||
}
|
||||
/**
|
||||
* Options for the channel message.
|
||||
*
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
export interface CreateChannelMessageOptions {}
|
||||
|
||||
/**
|
||||
* Options for the message reply method.
|
||||
*
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
export interface CreateMessageReplyOptions {}
|
||||
|
||||
/**
|
||||
* Custom options for the interaction reply method.
|
||||
*
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
export type CreateInteractionReplyOptions = ReplyInfo
|
||||
|
||||
/**
|
||||
* The Reacord adapter for Discord.js.
|
||||
@@ -54,17 +84,67 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a channel. Alternatively replies to message event.
|
||||
* Sends a message to a channel.
|
||||
*
|
||||
* @param target - Discord channel object.
|
||||
* @param [options] - Options for the channel message
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override send(
|
||||
channelId: string,
|
||||
initialContent?: React.ReactNode,
|
||||
options?: SendOptions,
|
||||
public createChannelMessage(
|
||||
target: Discord.Channel,
|
||||
options: CreateChannelMessageOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createChannelRenderer(channelId, options),
|
||||
this.createChannelMessageRenderer(target, options),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replies to a message by sending a message.
|
||||
*
|
||||
* @param message - Discord message event object.
|
||||
* @param [options] - Options for the message reply method.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
public createMessageReply(
|
||||
message: Discord.Message,
|
||||
options: CreateMessageReplyOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createMessageReplyRenderer(message, options),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replies to a command interaction by sending a message.
|
||||
*
|
||||
* @param interaction - Discord command interaction object.
|
||||
* @param [options] - Custom options for the interaction reply method.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
public createInteractionReply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
options: CreateInteractionReplyOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction, options),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a channel. Alternatively replies to message event.
|
||||
*
|
||||
* @deprecated Use reacord.createChannelMessage() or
|
||||
* reacord.createMessageReply() instead.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
public send(
|
||||
event: string | Discord.Message,
|
||||
initialContent?: React.ReactNode,
|
||||
options: LegacyCreateChannelMessageOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createMessageReplyRenderer(event, options),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
@@ -72,12 +152,13 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* Sends a message as a reply to a command interaction.
|
||||
*
|
||||
* @deprecated Use reacord.createInteractionReply() instead.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override reply(
|
||||
public reply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
initialContent?: React.ReactNode,
|
||||
options?: ReplyOptions,
|
||||
options: CreateInteractionReplyOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction, options),
|
||||
@@ -88,13 +169,14 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* Sends an ephemeral message as a reply to a command interaction.
|
||||
*
|
||||
* @deprecated Use reacord.reply(interaction, content, { ephemeral: true })
|
||||
* @deprecated Use reacord.createInteractionReply(interaction, content, {
|
||||
* ephemeral: true })
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override ephemeralReply(
|
||||
public ephemeralReply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
initialContent?: React.ReactNode,
|
||||
options?: Omit<ReplyOptions, "ephemeral">,
|
||||
options?: Omit<CreateInteractionReplyOptions, "ephemeral">,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction, {
|
||||
@@ -105,9 +187,25 @@ export class ReacordDiscordJs extends Reacord {
|
||||
)
|
||||
}
|
||||
|
||||
private createChannelRenderer(
|
||||
private createChannelMessageRenderer(
|
||||
channel: Discord.Channel,
|
||||
_opts?: CreateMessageReplyOptions,
|
||||
) {
|
||||
return new ChannelMessageRenderer({
|
||||
send: async (options) => {
|
||||
if (!channel.isTextBased()) {
|
||||
raise(`Channel ${channel.id} is not a text channel`)
|
||||
}
|
||||
|
||||
const message = await channel.send(getDiscordMessageOptions(options))
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private createMessageReplyRenderer(
|
||||
event: string | Discord.Message,
|
||||
opts?: SendOptions,
|
||||
opts: CreateChannelMessageOptions | LegacyCreateChannelMessageOptions,
|
||||
) {
|
||||
return new ChannelMessageRenderer({
|
||||
send: async (options) => {
|
||||
@@ -124,7 +222,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
raise(`Channel ${channel.id} is not a text channel`)
|
||||
}
|
||||
|
||||
if (opts?.reply) {
|
||||
if ("reply" in opts && opts.reply) {
|
||||
if (typeof event === "string") {
|
||||
raise("Cannot send reply with channel ID provided")
|
||||
}
|
||||
@@ -142,7 +240,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
interaction:
|
||||
| Discord.CommandInteraction
|
||||
| Discord.MessageComponentInteraction,
|
||||
opts?: ReplyOptions,
|
||||
opts: CreateInteractionReplyOptions,
|
||||
) {
|
||||
return new InteractionReplyRenderer({
|
||||
type: "command",
|
||||
@@ -150,16 +248,16 @@ export class ReacordDiscordJs extends Reacord {
|
||||
reply: async (options) => {
|
||||
const message = await interaction.reply({
|
||||
...getDiscordMessageOptions(options),
|
||||
...opts,
|
||||
fetchReply: true,
|
||||
ephemeral: opts?.ephemeral,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
followUp: async (options) => {
|
||||
const message = await interaction.followUp({
|
||||
...getDiscordMessageOptions(options),
|
||||
...opts,
|
||||
fetchReply: true,
|
||||
ephemeral: opts?.ephemeral,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
@@ -283,12 +381,13 @@ export class ReacordDiscordJs extends Reacord {
|
||||
user,
|
||||
guild,
|
||||
|
||||
reply: (content?: ReactNode) =>
|
||||
reply: (content?: ReactNode, options?: ReplyInfo) =>
|
||||
this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, options ?? {}),
|
||||
content,
|
||||
),
|
||||
|
||||
/** @deprecated Use event.reply(content, { ephemeral: true }) */
|
||||
ephemeralReply: (content: ReactNode) =>
|
||||
this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction, {
|
||||
|
||||
@@ -23,10 +23,6 @@ export abstract class Reacord {
|
||||
|
||||
constructor(private readonly config: ReacordConfig = {}) {}
|
||||
|
||||
abstract send(...args: unknown[]): ReacordInstance
|
||||
abstract reply(...args: unknown[]): ReacordInstance
|
||||
abstract ephemeralReply(...args: unknown[]): ReacordInstance
|
||||
|
||||
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
||||
for (const renderer of this.renderers) {
|
||||
if (renderer.handleComponentInteraction(interaction)) return
|
||||
@@ -61,6 +57,7 @@ export abstract class Reacord {
|
||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
||||
container,
|
||||
)
|
||||
return instance
|
||||
},
|
||||
deactivate: () => {
|
||||
this.deactivate(renderer)
|
||||
|
||||
@@ -50,7 +50,7 @@ const createTest = async (
|
||||
}
|
||||
|
||||
await createTest("basic", (channel) => {
|
||||
reacord.send(channel.id, "Hello, world!")
|
||||
reacord.createChannelMessage(channel).render("Hello, world!")
|
||||
})
|
||||
|
||||
await createTest("counter", (channel) => {
|
||||
@@ -73,7 +73,7 @@ await createTest("counter", (channel) => {
|
||||
</>
|
||||
)
|
||||
}
|
||||
reacord.send(channel.id, <Counter />)
|
||||
reacord.createChannelMessage(channel).render(<Counter />)
|
||||
})
|
||||
|
||||
await createTest("select", (channel) => {
|
||||
@@ -102,8 +102,7 @@ await createTest("select", (channel) => {
|
||||
)
|
||||
}
|
||||
|
||||
const instance = reacord.send(
|
||||
channel.id,
|
||||
const instance = reacord.createChannelMessage(channel).render(
|
||||
<FruitSelect
|
||||
onConfirm={(value) => {
|
||||
instance.render(`you chose ${value}`)
|
||||
@@ -114,8 +113,7 @@ await createTest("select", (channel) => {
|
||||
})
|
||||
|
||||
await createTest("ephemeral button", (channel) => {
|
||||
reacord.send(
|
||||
channel.id,
|
||||
reacord.createChannelMessage(channel).render(
|
||||
<>
|
||||
<Button
|
||||
label="public clic"
|
||||
@@ -125,7 +123,7 @@ await createTest("ephemeral button", (channel) => {
|
||||
/>
|
||||
<Button
|
||||
label="clic"
|
||||
onClick={(event) => event.ephemeralReply("you clic")}
|
||||
onClick={(event) => event.reply("you clic", { ephemeral: true })}
|
||||
/>
|
||||
</>,
|
||||
)
|
||||
@@ -136,9 +134,11 @@ await createTest("delete this", (channel) => {
|
||||
const instance = useInstance()
|
||||
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||
}
|
||||
reacord.send(channel.id, <DeleteThis />)
|
||||
reacord.createChannelMessage(channel).render(<DeleteThis />)
|
||||
})
|
||||
|
||||
await createTest("link", (channel) => {
|
||||
reacord.send(channel.id, <Link label="hi" url="https://mapleleaf.dev" />)
|
||||
reacord
|
||||
.createChannelMessage(channel)
|
||||
.render(<Link label="hi" url="https://mapleleaf.dev" />)
|
||||
})
|
||||
|
||||
@@ -6,8 +6,9 @@ import { test } from "vitest"
|
||||
test("rendering behavior", async () => {
|
||||
const tester = new ReacordTester()
|
||||
|
||||
const reply = tester.reply()
|
||||
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||
const reply = tester
|
||||
.createInteractionReply()
|
||||
.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||
|
||||
await tester.assertMessages([
|
||||
{
|
||||
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
|
||||
test("delete", async () => {
|
||||
const tester = new ReacordTester()
|
||||
|
||||
const reply = tester.reply()
|
||||
reply.render(
|
||||
const reply = tester.createInteractionReply().render(
|
||||
<>
|
||||
some text
|
||||
<Embed>some embed</Embed>
|
||||
|
||||
@@ -53,9 +53,7 @@ test("single select", async () => {
|
||||
])
|
||||
}
|
||||
|
||||
const reply = tester.reply()
|
||||
|
||||
reply.render(<TestSelect />)
|
||||
tester.createInteractionReply().render(<TestSelect />)
|
||||
await assertSelect([])
|
||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||
|
||||
@@ -119,9 +117,7 @@ test("multiple select", async () => {
|
||||
])
|
||||
}
|
||||
|
||||
const reply = tester.reply()
|
||||
|
||||
reply.render(<TestSelect />)
|
||||
tester.createInteractionReply().render(<TestSelect />)
|
||||
await assertSelect([])
|
||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||
|
||||
@@ -148,7 +144,7 @@ test("multiple select", async () => {
|
||||
|
||||
test("optional onSelect + unknown value", async () => {
|
||||
const tester = new ReacordTester()
|
||||
tester.reply().render(<Select placeholder="select" />)
|
||||
tester.createInteractionReply().render(<Select placeholder="select" />)
|
||||
await tester.findSelectByPlaceholder("select").select("something")
|
||||
await tester.assertMessages([
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ChannelInfo,
|
||||
GuildInfo,
|
||||
MessageInfo,
|
||||
ReplyInfo,
|
||||
UserInfo,
|
||||
} from "../library/core/component-event"
|
||||
import type { ButtonClickEvent } from "../library/core/components/button"
|
||||
@@ -42,26 +43,26 @@ export class ReacordTester extends Reacord {
|
||||
return [...this.messageContainer]
|
||||
}
|
||||
|
||||
override send(initialContent?: ReactNode): ReacordInstance {
|
||||
public createChannelMessage(): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
|
||||
override reply(initialContent?: ReactNode): ReacordInstance {
|
||||
public createMessageReply(): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||
)
|
||||
}
|
||||
|
||||
public createInteractionReply(_options?: ReplyInfo): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new InteractionReplyRenderer(
|
||||
new TestCommandInteraction(this.messageContainer),
|
||||
),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
|
||||
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
|
||||
return this.reply(initialContent)
|
||||
}
|
||||
|
||||
assertMessages(expected: MessageSample[]) {
|
||||
return waitFor(() => {
|
||||
expect(this.sampleMessages()).toEqual(expected)
|
||||
@@ -69,7 +70,7 @@ export class ReacordTester extends Reacord {
|
||||
}
|
||||
|
||||
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
||||
const instance = this.reply()
|
||||
const instance = this.createInteractionReply()
|
||||
instance.render(content)
|
||||
await this.assertMessages(expected)
|
||||
instance.destroy()
|
||||
@@ -254,11 +255,13 @@ class TestComponentEvent {
|
||||
guild: GuildInfo = {} as GuildInfo // todo
|
||||
|
||||
reply(content?: ReactNode): ReacordInstance {
|
||||
return this.tester.reply(content)
|
||||
return this.tester.createInteractionReply().render(content)
|
||||
}
|
||||
|
||||
ephemeralReply(content?: ReactNode): ReacordInstance {
|
||||
return this.tester.ephemeralReply(content)
|
||||
return this.tester
|
||||
.createInteractionReply({ ephemeral: true })
|
||||
.render(content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ describe("useInstance", () => {
|
||||
}
|
||||
|
||||
const tester = new ReacordTester()
|
||||
const instance = tester.send(<TestComponent name="parent" />)
|
||||
const instance = tester
|
||||
.createChannelMessage()
|
||||
.render(<TestComponent name="parent" />)
|
||||
|
||||
await tester.assertMessages([messageOutput("parent")])
|
||||
expect(instanceFromHook).toBe(instance)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,5 +3,5 @@ export type Props = astroHTML.JSX.AnchorHTMLAttributes
|
||||
---
|
||||
|
||||
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
|
||||
<slot />
|
||||
<slot />
|
||||
</a>
|
||||
|
||||
@@ -7,32 +7,32 @@ const guides = await getCollection("guides")
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="isolate">
|
||||
<header
|
||||
class="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex"
|
||||
>
|
||||
<div class="container">
|
||||
<MainNavigation />
|
||||
</div>
|
||||
</header>
|
||||
<main class="container mt-8 flex items-start gap-4">
|
||||
<nav class="w-48 sticky top-24 hidden md:block">
|
||||
<h2 class="text-2xl">Guides</h2>
|
||||
<ul class="mt-3 flex flex-col gap-2 items-start">
|
||||
{
|
||||
guides.map((guide) => (
|
||||
<li>
|
||||
<a class="link" href={`/guides/${guide.slug}`}>
|
||||
{guide.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="prose prose-invert pb-8 flex-1 min-w-0">
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<div class="isolate">
|
||||
<header
|
||||
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
|
||||
>
|
||||
<div class="container">
|
||||
<MainNavigation />
|
||||
</div>
|
||||
</header>
|
||||
<main class="container mt-8 flex items-start gap-4">
|
||||
<nav class="sticky top-24 hidden w-48 md:block">
|
||||
<h2 class="text-2xl">Guides</h2>
|
||||
<ul class="mt-3 flex flex-col items-start gap-2">
|
||||
{
|
||||
guides.map((guide) => (
|
||||
<li>
|
||||
<a class="link" href={`/guides/${guide.slug}`}>
|
||||
{guide.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="prose prose-invert min-w-0 flex-1 pb-8">
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -7,7 +7,7 @@ import faviconUrl from "~/assets/favicon.png"
|
||||
import "~/styles/tailwind.css"
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" class="bg-slate-900 text-slate-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<details class="md:hidden relative" data-menu>
|
||||
<summary
|
||||
class="list-none p-2 -m-2 cursor-pointer hover:text-emerald-500 transition"
|
||||
>
|
||||
<slot name="button" />
|
||||
</summary>
|
||||
<div
|
||||
class="w-48 max-h-[calc(100vh-5rem)] bg-slate-800 shadow rounded-lg overflow-x-hidden overflow-y-auto top-[calc(100%+8px)] right-0 absolute z-10"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<details class="relative md:hidden" data-menu>
|
||||
<summary
|
||||
class="-m-2 cursor-pointer list-none p-2 transition hover:text-emerald-500"
|
||||
>
|
||||
<slot name="button" />
|
||||
</summary>
|
||||
<div
|
||||
class="absolute right-0 top-[calc(100%+8px)] z-10 max-h-[calc(100vh-5rem)] w-48 overflow-y-auto overflow-x-hidden rounded-lg bg-slate-800 shadow"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<script>
|
||||
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
|
||||
"[data-menu]",
|
||||
)) {
|
||||
window.addEventListener("click", (event) => {
|
||||
if (!menu.contains(event.target as Node)) {
|
||||
menu.open = false
|
||||
}
|
||||
})
|
||||
menu.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
menu.open = false
|
||||
menu.querySelector("summary")!.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
|
||||
"[data-menu]",
|
||||
)) {
|
||||
window.addEventListener("click", (event) => {
|
||||
if (!menu.contains(event.target as Node)) {
|
||||
menu.open = false
|
||||
}
|
||||
})
|
||||
menu.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
menu.open = false
|
||||
menu.querySelector("summary")!.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,14 +9,13 @@ slug: sending-messages
|
||||
You can send messages via Reacord to a channel like so.
|
||||
|
||||
```jsx
|
||||
const channelId = "abc123deadbeef"
|
||||
|
||||
client.on("ready", () => {
|
||||
reacord.send(channelId, "Hello, world!")
|
||||
const channel = await client.channels.fetch("abc123deadbeef")
|
||||
reacord.createChannelMessage(channel).render("Hello, world!")
|
||||
})
|
||||
```
|
||||
|
||||
The `.send()` function creates a **Reacord instance**. You can pass strings, numbers, or anything that can be rendered by React, such as JSX!
|
||||
The `.createChannelMessage()` function creates a **Reacord instance**. You can pass strings, numbers, or anything that can be rendered by React, such as JSX!
|
||||
|
||||
Components rendered through this instance can include state and effects, and the message on Discord will update automatically.
|
||||
|
||||
@@ -36,7 +35,9 @@ function Uptime() {
|
||||
}
|
||||
|
||||
client.on("ready", () => {
|
||||
reacord.send(channelId, <Uptime />)
|
||||
const instance = reacord.createChannelMessage(channel)
|
||||
|
||||
instance.render(<Uptime />)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -46,12 +47,27 @@ The instance can be rendered to multiple times, which will update the message ea
|
||||
const Hello = ({ subject }) => <>Hello, {subject}!</>
|
||||
|
||||
client.on("ready", () => {
|
||||
const instance = reacord.send(channel)
|
||||
const instance = reacord.createChannelMessage(channel)
|
||||
|
||||
instance.render(<Hello subject="World" />)
|
||||
instance.render(<Hello subject="Moon" />)
|
||||
})
|
||||
```
|
||||
|
||||
## Replying to Messages
|
||||
|
||||
Instead of sending messages to a channel, you may want to reply to a specific message instead. To do this, create an instance using `.createMessageReply()` instead:
|
||||
|
||||
```jsx
|
||||
const Hello = ({ username }) => <>Hello, {username}!</>
|
||||
|
||||
client.on("messageCreate", (message) => {
|
||||
reacord
|
||||
.createMessageReply(message)
|
||||
.render(<Hello username={message.author.displayName} />)
|
||||
})
|
||||
```
|
||||
|
||||
## Cleaning Up Instances
|
||||
|
||||
If you no longer want to use the instance, you can clean it up in a few ways:
|
||||
@@ -75,7 +91,7 @@ const reacord = new ReacordDiscordJs(client, {
|
||||
This section also applies to other kinds of application commands, such as context menu commands.
|
||||
</aside>
|
||||
|
||||
To reply to a command interaction, use the `.reply()` function. This function returns an instance that works the same way as the one from `.send()`. Here's an example:
|
||||
To reply to a command interaction, use the `.createInteractionReply()` function. This function returns an instance that works the same way as the one from `.createChannelMessage()` and `.createMessageReply()`. Here's an example:
|
||||
|
||||
```jsx
|
||||
import { Client } from "discord.js"
|
||||
@@ -94,8 +110,8 @@ client.on("ready", () => {
|
||||
|
||||
client.on("interactionCreate", (interaction) => {
|
||||
if (interaction.isCommand() && interaction.commandName === "ping") {
|
||||
// Use the reply() function instead of send
|
||||
reacord.reply(interaction, <>pong!</>)
|
||||
// Use the createInteractionReply() function instead of createChannelMessage
|
||||
reacord.createInteractionReply(interaction).render(<>pong!</>)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -134,33 +150,55 @@ handleCommands(client, [
|
||||
name: "ping",
|
||||
description: "pong!",
|
||||
run: (interaction) => {
|
||||
reacord.reply(interaction, <>pong!</>)
|
||||
reacord.createInteractionReply(interaction).render(<>pong!</>)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hi",
|
||||
description: "say hi",
|
||||
run: (interaction) => {
|
||||
reacord.reply(interaction, <>hi</>)
|
||||
reacord.createInteractionReply(interaction).render(<>hi</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
## Ephemeral Command Replies
|
||||
## Interaction Options
|
||||
|
||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.ephemeralReply()` function.
|
||||
Just like `.createChannelMessage()` and `.createMessageReply()`, interaction replies provide a way to specify certain `interaction.reply()` options.
|
||||
|
||||
```tsx
|
||||
### Ephemeral Command Replies
|
||||
|
||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.createInteractionReply()` function and provide `ephemeral` option.
|
||||
|
||||
```jsx
|
||||
handleCommands(client, [
|
||||
{
|
||||
name: "pong",
|
||||
description: "pong, but in secret",
|
||||
run: (interaction) => {
|
||||
reacord.ephemeralReply(interaction, <>(pong)</>)
|
||||
reacord
|
||||
.createInteractionReply(interaction, { ephemeral: true })
|
||||
.render(<>(pong)</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
The `ephemeralReply` function also returns an instance, but ephemeral replies cannot be updated via `instance.render()`. You can `.deactivate()` them, but `.destroy()` will not delete the message; only the user can hide it from view.
|
||||
### Text-to-Speech Command Replies
|
||||
|
||||
Additionally interaction replies may have `tts` option to turn on text-to-speech ability for the reply. To create such reply, use `.createInteractionReply()` function and provide `tts` option.
|
||||
|
||||
```jsx
|
||||
handleCommands(client, [
|
||||
{
|
||||
name: "pong",
|
||||
description: "pong, but converted into audio",
|
||||
run: (interaction) => {
|
||||
reacord
|
||||
.createInteractionReply(interaction, { tts: true })
|
||||
.render(<>pong!</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
@@ -24,7 +24,9 @@ function FancyMessage({ title, description }) {
|
||||
```
|
||||
|
||||
```jsx
|
||||
reacord.send(channelId, <FancyMessage title="Hello" description="World" />)
|
||||
reacord
|
||||
.createChannelMessage(channel)
|
||||
.render(<FancyMessage title="Hello" description="World" />)
|
||||
```
|
||||
|
||||
Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition:
|
||||
@@ -52,8 +54,7 @@ function FancyMessage({ children }) {
|
||||
```
|
||||
|
||||
```jsx
|
||||
reacord.send(
|
||||
channelId,
|
||||
reacord.createChannelMessage(channel).render(
|
||||
<FancyMessage>
|
||||
<FancyDetails title="Hello" description="World" />
|
||||
</FancyMessage>,
|
||||
|
||||
@@ -35,7 +35,9 @@ function TheButton() {
|
||||
const publicReply = event.reply(`${name} clicked the button. wow`)
|
||||
setTimeout(() => publicReply.destroy(), 3000)
|
||||
|
||||
const privateReply = event.ephemeralReply("good job, you clicked it")
|
||||
const privateReply = event.reply("good job, you clicked it", {
|
||||
ephemeral: true,
|
||||
})
|
||||
privateReply.deactivate() // we don't need to listen to updates on this
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,7 @@ export function FruitSelect({ onConfirm }) {
|
||||
```
|
||||
|
||||
```jsx
|
||||
const instance = reacord.send(
|
||||
channelId,
|
||||
const instance = reacord.createChannelMessage(channel).render(
|
||||
<FruitSelect
|
||||
onConfirm={(value) => {
|
||||
instance.render(`you chose ${value}`)
|
||||
@@ -49,7 +48,7 @@ const instance = reacord.send(
|
||||
|
||||
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
|
||||
|
||||
```tsx
|
||||
```jsx
|
||||
export function FruitSelect({ onConfirm }) {
|
||||
const [values, setValues] = useState([])
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ function SelfDestruct() {
|
||||
)
|
||||
}
|
||||
|
||||
reacord.send(channelId, <SelfDestruct />)
|
||||
reacord.createChannelMessage(channel).render(<SelfDestruct />)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user