Merge pull request #40 from domin-mnd/main

This commit is contained in:
Darius
2023-10-28 13:04:33 -05:00
committed by GitHub
19 changed files with 301 additions and 150 deletions

View File

@@ -33,15 +33,23 @@ export interface ComponentEvent {
guild?: GuildInfo guild?: GuildInfo
/** Create a new reply to this event. */ /** 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 * Create an ephemeral reply to this event, shown only to the user who
* triggered it. * triggered it.
*
* @deprecated Use event.reply(content, { ephemeral: true })
*/ */
ephemeralReply(content?: ReactNode): ReacordInstance ephemeralReply(content?: ReactNode): ReacordInstance
} }
/** @category Component Event */
export interface ReplyInfo {
ephemeral?: boolean
tts?: boolean
}
/** @category Component Event */ /** @category Component Event */
export interface ChannelInfo { export interface ChannelInfo {
id: string id: string

View File

@@ -7,7 +7,7 @@ import type { ReactNode } from "react"
*/ */
export interface ReacordInstance { export interface ReacordInstance {
/** Render some JSX to this instance (edits the message) */ /** Render some JSX to this instance (edits the message) */
render: (content: ReactNode) => void render: (content: ReactNode) => ReacordInstance
/** Remove this message */ /** Remove this message */
destroy: () => void destroy: () => void

View File

@@ -18,19 +18,49 @@ import type {
GuildInfo, GuildInfo,
GuildMemberInfo, GuildMemberInfo,
MessageInfo, MessageInfo,
ReplyInfo,
UserInfo, UserInfo,
} from "./component-event" } from "./component-event"
import type { ReacordInstance } from "./instance" import type { ReacordInstance } from "./instance"
import type { ReacordConfig } from "./reacord" import type { ReacordConfig } from "./reacord"
import { Reacord } 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 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. * 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 * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override send( public createChannelMessage(
channelId: string, target: Discord.Channel,
initialContent?: React.ReactNode, options: CreateChannelMessageOptions = {},
options?: SendOptions,
): ReacordInstance { ): ReacordInstance {
return this.createInstance( 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, initialContent,
) )
} }
@@ -72,12 +152,13 @@ export class ReacordDiscordJs extends Reacord {
/** /**
* Sends a message as a reply to a command interaction. * Sends a message as a reply to a command interaction.
* *
* @deprecated Use reacord.createInteractionReply() instead.
* @see https://reacord.mapleleaf.dev/guides/sending-messages * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override reply( public reply(
interaction: Discord.CommandInteraction, interaction: Discord.CommandInteraction,
initialContent?: React.ReactNode, initialContent?: React.ReactNode,
options?: ReplyOptions, options: CreateInteractionReplyOptions = {},
): ReacordInstance { ): ReacordInstance {
return this.createInstance( return this.createInstance(
this.createInteractionReplyRenderer(interaction, options), this.createInteractionReplyRenderer(interaction, options),
@@ -88,13 +169,14 @@ export class ReacordDiscordJs extends Reacord {
/** /**
* Sends an ephemeral message as a reply to a command interaction. * 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 * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override ephemeralReply( public ephemeralReply(
interaction: Discord.CommandInteraction, interaction: Discord.CommandInteraction,
initialContent?: React.ReactNode, initialContent?: React.ReactNode,
options?: Omit<ReplyOptions, "ephemeral">, options?: Omit<CreateInteractionReplyOptions, "ephemeral">,
): ReacordInstance { ): ReacordInstance {
return this.createInstance( return this.createInstance(
this.createInteractionReplyRenderer(interaction, { 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, event: string | Discord.Message,
opts?: SendOptions, opts: CreateChannelMessageOptions | LegacyCreateChannelMessageOptions,
) { ) {
return new ChannelMessageRenderer({ return new ChannelMessageRenderer({
send: async (options) => { send: async (options) => {
@@ -124,7 +222,7 @@ export class ReacordDiscordJs extends Reacord {
raise(`Channel ${channel.id} is not a text channel`) raise(`Channel ${channel.id} is not a text channel`)
} }
if (opts?.reply) { if ("reply" in opts && opts.reply) {
if (typeof event === "string") { if (typeof event === "string") {
raise("Cannot send reply with channel ID provided") raise("Cannot send reply with channel ID provided")
} }
@@ -142,7 +240,7 @@ export class ReacordDiscordJs extends Reacord {
interaction: interaction:
| Discord.CommandInteraction | Discord.CommandInteraction
| Discord.MessageComponentInteraction, | Discord.MessageComponentInteraction,
opts?: ReplyOptions, opts: CreateInteractionReplyOptions,
) { ) {
return new InteractionReplyRenderer({ return new InteractionReplyRenderer({
type: "command", type: "command",
@@ -150,16 +248,16 @@ export class ReacordDiscordJs extends Reacord {
reply: async (options) => { reply: async (options) => {
const message = await interaction.reply({ const message = await interaction.reply({
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(options),
...opts,
fetchReply: true, fetchReply: true,
ephemeral: opts?.ephemeral,
}) })
return createReacordMessage(message) return createReacordMessage(message)
}, },
followUp: async (options) => { followUp: async (options) => {
const message = await interaction.followUp({ const message = await interaction.followUp({
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(options),
...opts,
fetchReply: true, fetchReply: true,
ephemeral: opts?.ephemeral,
}) })
return createReacordMessage(message) return createReacordMessage(message)
}, },
@@ -283,12 +381,13 @@ export class ReacordDiscordJs extends Reacord {
user, user,
guild, guild,
reply: (content?: ReactNode) => reply: (content?: ReactNode, options?: ReplyInfo) =>
this.createInstance( this.createInstance(
this.createInteractionReplyRenderer(interaction), this.createInteractionReplyRenderer(interaction, options ?? {}),
content, content,
), ),
/** @deprecated Use event.reply(content, { ephemeral: true }) */
ephemeralReply: (content: ReactNode) => ephemeralReply: (content: ReactNode) =>
this.createInstance( this.createInstance(
this.createInteractionReplyRenderer(interaction, { this.createInteractionReplyRenderer(interaction, {

View File

@@ -23,10 +23,6 @@ export abstract class Reacord {
constructor(private readonly config: ReacordConfig = {}) {} constructor(private readonly config: ReacordConfig = {}) {}
abstract send(...args: unknown[]): ReacordInstance
abstract reply(...args: unknown[]): ReacordInstance
abstract ephemeralReply(...args: unknown[]): ReacordInstance
protected handleComponentInteraction(interaction: ComponentInteraction) { protected handleComponentInteraction(interaction: ComponentInteraction) {
for (const renderer of this.renderers) { for (const renderer of this.renderers) {
if (renderer.handleComponentInteraction(interaction)) return if (renderer.handleComponentInteraction(interaction)) return
@@ -61,6 +57,7 @@ export abstract class Reacord {
<InstanceProvider value={instance}>{content}</InstanceProvider>, <InstanceProvider value={instance}>{content}</InstanceProvider>,
container, container,
) )
return instance
}, },
deactivate: () => { deactivate: () => {
this.deactivate(renderer) this.deactivate(renderer)

View File

@@ -50,7 +50,7 @@ const createTest = async (
} }
await createTest("basic", (channel) => { await createTest("basic", (channel) => {
reacord.send(channel.id, "Hello, world!") reacord.createChannelMessage(channel).render("Hello, world!")
}) })
await createTest("counter", (channel) => { 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) => { await createTest("select", (channel) => {
@@ -102,8 +102,7 @@ await createTest("select", (channel) => {
) )
} }
const instance = reacord.send( const instance = reacord.createChannelMessage(channel).render(
channel.id,
<FruitSelect <FruitSelect
onConfirm={(value) => { onConfirm={(value) => {
instance.render(`you chose ${value}`) instance.render(`you chose ${value}`)
@@ -114,8 +113,7 @@ await createTest("select", (channel) => {
}) })
await createTest("ephemeral button", (channel) => { await createTest("ephemeral button", (channel) => {
reacord.send( reacord.createChannelMessage(channel).render(
channel.id,
<> <>
<Button <Button
label="public clic" label="public clic"
@@ -125,7 +123,7 @@ await createTest("ephemeral button", (channel) => {
/> />
<Button <Button
label="clic" 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() const instance = useInstance()
return <Button label="delete this" onClick={() => instance.destroy()} /> return <Button label="delete this" onClick={() => instance.destroy()} />
} }
reacord.send(channel.id, <DeleteThis />) reacord.createChannelMessage(channel).render(<DeleteThis />)
}) })
await createTest("link", (channel) => { 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" />)
}) })

View File

@@ -6,8 +6,9 @@ import { test } from "vitest"
test("rendering behavior", async () => { test("rendering behavior", async () => {
const tester = new ReacordTester() const tester = new ReacordTester()
const reply = tester.reply() const reply = tester
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />) .createInteractionReply()
.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
await tester.assertMessages([ await tester.assertMessages([
{ {
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
test("delete", async () => { test("delete", async () => {
const tester = new ReacordTester() const tester = new ReacordTester()
const reply = tester.reply() const reply = tester.createInteractionReply().render(
reply.render(
<> <>
some text some text
<Embed>some embed</Embed> <Embed>some embed</Embed>

View File

@@ -53,9 +53,7 @@ test("single select", async () => {
]) ])
} }
const reply = tester.reply() tester.createInteractionReply().render(<TestSelect />)
reply.render(<TestSelect />)
await assertSelect([]) await assertSelect([])
expect(onSelect).toHaveBeenCalledTimes(0) expect(onSelect).toHaveBeenCalledTimes(0)
@@ -119,9 +117,7 @@ test("multiple select", async () => {
]) ])
} }
const reply = tester.reply() tester.createInteractionReply().render(<TestSelect />)
reply.render(<TestSelect />)
await assertSelect([]) await assertSelect([])
expect(onSelect).toHaveBeenCalledTimes(0) expect(onSelect).toHaveBeenCalledTimes(0)
@@ -148,7 +144,7 @@ test("multiple select", async () => {
test("optional onSelect + unknown value", async () => { test("optional onSelect + unknown value", async () => {
const tester = new ReacordTester() const tester = new ReacordTester()
tester.reply().render(<Select placeholder="select" />) tester.createInteractionReply().render(<Select placeholder="select" />)
await tester.findSelectByPlaceholder("select").select("something") await tester.findSelectByPlaceholder("select").select("something")
await tester.assertMessages([ await tester.assertMessages([
{ {

View File

@@ -11,6 +11,7 @@ import type {
ChannelInfo, ChannelInfo,
GuildInfo, GuildInfo,
MessageInfo, MessageInfo,
ReplyInfo,
UserInfo, UserInfo,
} from "../library/core/component-event" } from "../library/core/component-event"
import type { ButtonClickEvent } from "../library/core/components/button" import type { ButtonClickEvent } from "../library/core/components/button"
@@ -42,26 +43,26 @@ export class ReacordTester extends Reacord {
return [...this.messageContainer] return [...this.messageContainer]
} }
override send(initialContent?: ReactNode): ReacordInstance { public createChannelMessage(): ReacordInstance {
return this.createInstance( return this.createInstance(
new ChannelMessageRenderer(new TestChannel(this.messageContainer)), 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( return this.createInstance(
new InteractionReplyRenderer( new InteractionReplyRenderer(
new TestCommandInteraction(this.messageContainer), new TestCommandInteraction(this.messageContainer),
), ),
initialContent,
) )
} }
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
return this.reply(initialContent)
}
assertMessages(expected: MessageSample[]) { assertMessages(expected: MessageSample[]) {
return waitFor(() => { return waitFor(() => {
expect(this.sampleMessages()).toEqual(expected) expect(this.sampleMessages()).toEqual(expected)
@@ -69,7 +70,7 @@ export class ReacordTester extends Reacord {
} }
async assertRender(content: ReactNode, expected: MessageSample[]) { async assertRender(content: ReactNode, expected: MessageSample[]) {
const instance = this.reply() const instance = this.createInteractionReply()
instance.render(content) instance.render(content)
await this.assertMessages(expected) await this.assertMessages(expected)
instance.destroy() instance.destroy()
@@ -254,11 +255,13 @@ class TestComponentEvent {
guild: GuildInfo = {} as GuildInfo // todo guild: GuildInfo = {} as GuildInfo // todo
reply(content?: ReactNode): ReacordInstance { reply(content?: ReactNode): ReacordInstance {
return this.tester.reply(content) return this.tester.createInteractionReply().render(content)
} }
ephemeralReply(content?: ReactNode): ReacordInstance { ephemeralReply(content?: ReactNode): ReacordInstance {
return this.tester.ephemeralReply(content) return this.tester
.createInteractionReply({ ephemeral: true })
.render(content)
} }
} }

View File

@@ -49,7 +49,9 @@ describe("useInstance", () => {
} }
const tester = new ReacordTester() const tester = new ReacordTester()
const instance = tester.send(<TestComponent name="parent" />) const instance = tester
.createChannelMessage()
.render(<TestComponent name="parent" />)
await tester.assertMessages([messageOutput("parent")]) await tester.assertMessages([messageOutput("parent")])
expect(instanceFromHook).toBe(instance) expect(instanceFromHook).toBe(instance)

File diff suppressed because one or more lines are too long

View File

@@ -3,5 +3,5 @@ export type Props = astroHTML.JSX.AnchorHTMLAttributes
--- ---
<a rel="noopener noreferrer" target="_blank" {...Astro.props}> <a rel="noopener noreferrer" target="_blank" {...Astro.props}>
<slot /> <slot />
</a> </a>

View File

@@ -7,32 +7,32 @@ const guides = await getCollection("guides")
--- ---
<Layout> <Layout>
<div class="isolate"> <div class="isolate">
<header <header
class="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex" class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
> >
<div class="container"> <div class="container">
<MainNavigation /> <MainNavigation />
</div> </div>
</header> </header>
<main class="container mt-8 flex items-start gap-4"> <main class="container mt-8 flex items-start gap-4">
<nav class="w-48 sticky top-24 hidden md:block"> <nav class="sticky top-24 hidden w-48 md:block">
<h2 class="text-2xl">Guides</h2> <h2 class="text-2xl">Guides</h2>
<ul class="mt-3 flex flex-col gap-2 items-start"> <ul class="mt-3 flex flex-col items-start gap-2">
{ {
guides.map((guide) => ( guides.map((guide) => (
<li> <li>
<a class="link" href={`/guides/${guide.slug}`}> <a class="link" href={`/guides/${guide.slug}`}>
{guide.data.title} {guide.data.title}
</a> </a>
</li> </li>
)) ))
} }
</ul> </ul>
</nav> </nav>
<section class="prose prose-invert pb-8 flex-1 min-w-0"> <section class="prose prose-invert min-w-0 flex-1 pb-8">
<slot /> <slot />
</section> </section>
</main> </main>
</div> </div>
</Layout> </Layout>

View File

@@ -7,7 +7,7 @@ import faviconUrl from "~/assets/favicon.png"
import "~/styles/tailwind.css" import "~/styles/tailwind.css"
--- ---
<!DOCTYPE html> <!doctype html>
<html lang="en" class="bg-slate-900 text-slate-100"> <html lang="en" class="bg-slate-900 text-slate-100">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@@ -1,30 +1,30 @@
<details class="md:hidden relative" data-menu> <details class="relative md:hidden" data-menu>
<summary <summary
class="list-none p-2 -m-2 cursor-pointer hover:text-emerald-500 transition" class="-m-2 cursor-pointer list-none p-2 transition hover:text-emerald-500"
> >
<slot name="button" /> <slot name="button" />
</summary> </summary>
<div <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" 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 /> <slot />
</div> </div>
</details> </details>
<script> <script>
for (const menu of document.querySelectorAll<HTMLDetailsElement>( for (const menu of document.querySelectorAll<HTMLDetailsElement>(
"[data-menu]", "[data-menu]",
)) { )) {
window.addEventListener("click", (event) => { window.addEventListener("click", (event) => {
if (!menu.contains(event.target as Node)) { if (!menu.contains(event.target as Node)) {
menu.open = false menu.open = false
} }
}) })
menu.addEventListener("keydown", (event) => { menu.addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape") {
menu.open = false menu.open = false
menu.querySelector("summary")!.focus() menu.querySelector("summary")!.focus()
} }
}) })
} }
</script> </script>

View File

@@ -9,14 +9,13 @@ slug: sending-messages
You can send messages via Reacord to a channel like so. You can send messages via Reacord to a channel like so.
```jsx ```jsx
const channelId = "abc123deadbeef"
client.on("ready", () => { 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. 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", () => { 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}!</> const Hello = ({ subject }) => <>Hello, {subject}!</>
client.on("ready", () => { client.on("ready", () => {
const instance = reacord.send(channel) const instance = reacord.createChannelMessage(channel)
instance.render(<Hello subject="World" />) instance.render(<Hello subject="World" />)
instance.render(<Hello subject="Moon" />) 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 ## Cleaning Up Instances
If you no longer want to use the instance, you can clean it up in a few ways: 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. This section also applies to other kinds of application commands, such as context menu commands.
</aside> </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 ```jsx
import { Client } from "discord.js" import { Client } from "discord.js"
@@ -94,8 +110,8 @@ client.on("ready", () => {
client.on("interactionCreate", (interaction) => { client.on("interactionCreate", (interaction) => {
if (interaction.isCommand() && interaction.commandName === "ping") { if (interaction.isCommand() && interaction.commandName === "ping") {
// Use the reply() function instead of send // Use the createInteractionReply() function instead of createChannelMessage
reacord.reply(interaction, <>pong!</>) reacord.createInteractionReply(interaction).render(<>pong!</>)
} }
}) })
@@ -134,33 +150,55 @@ handleCommands(client, [
name: "ping", name: "ping",
description: "pong!", description: "pong!",
run: (interaction) => { run: (interaction) => {
reacord.reply(interaction, <>pong!</>) reacord.createInteractionReply(interaction).render(<>pong!</>)
}, },
}, },
{ {
name: "hi", name: "hi",
description: "say hi", description: "say hi",
run: (interaction) => { 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, [ handleCommands(client, [
{ {
name: "pong", name: "pong",
description: "pong, but in secret", description: "pong, but in secret",
run: (interaction) => { 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!</>)
},
},
])
```

View File

@@ -24,7 +24,9 @@ function FancyMessage({ title, description }) {
``` ```
```jsx ```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: 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 ```jsx
reacord.send( reacord.createChannelMessage(channel).render(
channelId,
<FancyMessage> <FancyMessage>
<FancyDetails title="Hello" description="World" /> <FancyDetails title="Hello" description="World" />
</FancyMessage>, </FancyMessage>,

View File

@@ -35,7 +35,9 @@ function TheButton() {
const publicReply = event.reply(`${name} clicked the button. wow`) const publicReply = event.reply(`${name} clicked the button. wow`)
setTimeout(() => publicReply.destroy(), 3000) 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 privateReply.deactivate() // we don't need to listen to updates on this
} }

View File

@@ -36,8 +36,7 @@ export function FruitSelect({ onConfirm }) {
``` ```
```jsx ```jsx
const instance = reacord.send( const instance = reacord.createChannelMessage(channel).render(
channelId,
<FruitSelect <FruitSelect
onConfirm={(value) => { onConfirm={(value) => {
instance.render(`you chose ${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. 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 }) { export function FruitSelect({ onConfirm }) {
const [values, setValues] = useState([]) const [values, setValues] = useState([])

View File

@@ -22,5 +22,5 @@ function SelfDestruct() {
) )
} }
reacord.send(channelId, <SelfDestruct />) reacord.createChannelMessage(channel).render(<SelfDestruct />)
``` ```