Merge pull request #40 from domin-mnd/main
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" />)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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!</>)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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([])
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ function SelfDestruct() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reacord.send(channelId, <SelfDestruct />)
|
reacord.createChannelMessage(channel).render(<SelfDestruct />)
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user