Compare commits
27 Commits
reacord@0.
...
reacord@0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00fbc0631 | ||
|
|
a713f17a5c | ||
|
|
44795cd7cc | ||
|
|
17978a5252 | ||
|
|
95fb342183 | ||
|
|
0772ca4502 | ||
|
|
11153dfe0f | ||
|
|
fb0a997855 | ||
|
|
da1c62f2f0 | ||
|
|
cdc22b7916 | ||
|
|
7fee69c8ae | ||
|
|
c2e5dc04dd | ||
|
|
390da4cab6 | ||
|
|
def0c46f13 | ||
|
|
8b6e283810 | ||
|
|
13fcf7ddc9 | ||
|
|
ce12351a24 | ||
|
|
73bb098774 | ||
|
|
4ee4d4ab91 | ||
|
|
f998a0e09a | ||
|
|
453192cc96 | ||
|
|
d387f669ab | ||
|
|
9aec87ae9f | ||
|
|
65d1d68bb0 | ||
|
|
dfb7562c97 | ||
|
|
9e2be6c2e0 | ||
|
|
d078995516 |
@@ -4,12 +4,9 @@
|
||||
"scripts": {
|
||||
"lint": "run-s --continue-on-error lint:*",
|
||||
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
||||
"lint:prettier": "prettier . --write --cache --list-different",
|
||||
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
||||
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
||||
"astro-sync": "pnpm --filter website exec astro sync",
|
||||
"format": "run-s --continue-on-error format:*",
|
||||
"format:eslint": "eslint . --report-unused-disable-directives --fix",
|
||||
"format:prettier": "prettier --cache --write . \"**/*.astro\"",
|
||||
"test": "vitest",
|
||||
"build": "pnpm -r run build",
|
||||
"build:website": "pnpm --filter website... run build",
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# reacord
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 11153df: breaking: more descriptive component event types
|
||||
- fb0a997: add new descriptive adapter methods
|
||||
|
||||
The reacord instance names have been updated, and the old names are now deprecated.
|
||||
|
||||
- `send` -> `createChannelMessage`
|
||||
- `reply` -> `createInteractionReply`
|
||||
|
||||
These new methods also accept discord JS options. Usage example:
|
||||
|
||||
```ts
|
||||
// can accept either a channel object or a channel ID
|
||||
reacord.createChannelMessage(channel)
|
||||
reacord.createChannelMessage(channel, {
|
||||
tts: true,
|
||||
})
|
||||
reacord.createChannelMessage(channel, {
|
||||
reply: {
|
||||
messageReference: "123456789012345678",
|
||||
failIfNotExists: false,
|
||||
},
|
||||
})
|
||||
|
||||
reacord.createInteractionReply(interaction)
|
||||
reacord.createInteractionReply(interaction, {
|
||||
ephemeral: true,
|
||||
})
|
||||
```
|
||||
|
||||
These new methods replace the old ones, which are now deprecated.
|
||||
|
||||
## 0.5.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,41 +9,52 @@ export interface ComponentEvent {
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/channel#message-object
|
||||
*/
|
||||
message: MessageInfo
|
||||
message: ComponentEventMessage
|
||||
|
||||
/**
|
||||
* The channel that this event occurred in.
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/channel#channel-object
|
||||
*/
|
||||
channel: ChannelInfo
|
||||
channel: ComponentEventChannel
|
||||
|
||||
/**
|
||||
* The user that triggered this event.
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/user#user-object
|
||||
*/
|
||||
user: UserInfo
|
||||
user: ComponentEventUser
|
||||
|
||||
/**
|
||||
* The guild that this event occurred in.
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/guild#guild-object
|
||||
*/
|
||||
guild?: GuildInfo
|
||||
guild?: ComponentEventGuild
|
||||
|
||||
/** Create a new reply to this event. */
|
||||
reply(content?: ReactNode): ReacordInstance
|
||||
reply(
|
||||
content?: ReactNode,
|
||||
options?: ComponentEventReplyOptions,
|
||||
): 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 ChannelInfo {
|
||||
export interface ComponentEventReplyOptions {
|
||||
ephemeral?: boolean
|
||||
tts?: boolean
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface ComponentEventChannel {
|
||||
id: string
|
||||
name?: string
|
||||
topic?: string
|
||||
@@ -55,11 +66,11 @@ export interface ChannelInfo {
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface MessageInfo {
|
||||
export interface ComponentEventMessage {
|
||||
id: string
|
||||
channelId: string
|
||||
authorId: string
|
||||
member?: GuildMemberInfo
|
||||
member?: ComponentEventGuildMember
|
||||
content: string
|
||||
timestamp: string
|
||||
editedTimestamp?: string
|
||||
@@ -70,14 +81,14 @@ export interface MessageInfo {
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface GuildInfo {
|
||||
export interface ComponentEventGuild {
|
||||
id: string
|
||||
name: string
|
||||
member: GuildMemberInfo
|
||||
member: ComponentEventGuildMember
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface GuildMemberInfo {
|
||||
export interface ComponentEventGuildMember {
|
||||
id: string
|
||||
nick?: string
|
||||
displayName: string
|
||||
@@ -92,7 +103,7 @@ export interface GuildMemberInfo {
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface UserInfo {
|
||||
export interface ComponentEventUser {
|
||||
id: string
|
||||
username: string
|
||||
discriminator: 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
|
||||
|
||||
@@ -14,11 +14,12 @@ import type {
|
||||
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
|
||||
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
|
||||
import type {
|
||||
ChannelInfo,
|
||||
GuildInfo,
|
||||
GuildMemberInfo,
|
||||
MessageInfo,
|
||||
UserInfo,
|
||||
ComponentEventChannel,
|
||||
ComponentEventGuild,
|
||||
ComponentEventGuildMember,
|
||||
ComponentEventMessage,
|
||||
ComponentEventReplyOptions,
|
||||
ComponentEventUser,
|
||||
} from "./component-event"
|
||||
import type { ReacordInstance } from "./instance"
|
||||
import type { ReacordConfig } from "./reacord"
|
||||
@@ -48,14 +49,49 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* 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 {@link Discord.MessageCreateOptions}
|
||||
*/
|
||||
public createChannelMessage(
|
||||
target: Discord.ChannelResolvable,
|
||||
options: Discord.MessageCreateOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createChannelMessageRenderer(target, 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
|
||||
* @see {@link Discord.InteractionReplyOptions}
|
||||
*/
|
||||
public createInteractionReply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
options: Discord.InteractionReplyOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction, options),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a channel.
|
||||
*
|
||||
* @deprecated Use reacord.createChannelMessage() instead.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override send(
|
||||
channelId: string,
|
||||
public send(
|
||||
channel: Discord.ChannelResolvable,
|
||||
initialContent?: React.ReactNode,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createChannelRenderer(channelId),
|
||||
this.createChannelMessageRenderer(channel, {}),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
@@ -63,14 +99,15 @@ 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,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, {}),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
@@ -78,31 +115,49 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* Sends an ephemeral message as a reply to a command interaction.
|
||||
*
|
||||
* @deprecated Use reacord.createInteractionReply(interaction, { ephemeral:
|
||||
* true })
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override ephemeralReply(
|
||||
public ephemeralReply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
initialContent?: React.ReactNode,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, {
|
||||
ephemeral: true,
|
||||
}),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
|
||||
private createChannelRenderer(channelId: string) {
|
||||
private createChannelMessageRenderer(
|
||||
channelResolvable: Discord.ChannelResolvable,
|
||||
messageCreateOptions?: Discord.MessageCreateOptions,
|
||||
) {
|
||||
return new ChannelMessageRenderer({
|
||||
send: async (options) => {
|
||||
const channel =
|
||||
this.client.channels.cache.get(channelId) ??
|
||||
(await this.client.channels.fetch(channelId)) ??
|
||||
raise(`Channel ${channelId} not found`)
|
||||
|
||||
if (!channel.isTextBased()) {
|
||||
raise(`Channel ${channelId} is not a text channel`)
|
||||
send: async (messageOptions) => {
|
||||
let channel = this.client.channels.resolve(channelResolvable)
|
||||
if (!channel && typeof channelResolvable === "string") {
|
||||
channel = await this.client.channels.fetch(channelResolvable)
|
||||
}
|
||||
|
||||
const message = await channel.send(getDiscordMessageOptions(options))
|
||||
if (!channel) {
|
||||
const id =
|
||||
typeof channelResolvable === "string"
|
||||
? channelResolvable
|
||||
: channelResolvable.id
|
||||
raise(`Channel ${id} not found`)
|
||||
}
|
||||
|
||||
if (!channel.isTextBased()) {
|
||||
raise(`Channel ${channel.id} must be a text channel`)
|
||||
}
|
||||
|
||||
const message = await channel.send({
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...messageCreateOptions,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
})
|
||||
@@ -112,20 +167,22 @@ export class ReacordDiscordJs extends Reacord {
|
||||
interaction:
|
||||
| Discord.CommandInteraction
|
||||
| Discord.MessageComponentInteraction,
|
||||
interactionReplyOptions: Discord.InteractionReplyOptions,
|
||||
) {
|
||||
return new InteractionReplyRenderer({
|
||||
type: "command",
|
||||
id: interaction.id,
|
||||
reply: async (options) => {
|
||||
interactionId: interaction.id,
|
||||
reply: async (messageOptions) => {
|
||||
const message = await interaction.reply({
|
||||
...getDiscordMessageOptions(options),
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...interactionReplyOptions,
|
||||
fetchReply: true,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
followUp: async (options) => {
|
||||
followUp: async (messageOptions) => {
|
||||
const message = await interaction.followUp({
|
||||
...getDiscordMessageOptions(options),
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...interactionReplyOptions,
|
||||
fetchReply: true,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
@@ -133,36 +190,11 @@ export class ReacordDiscordJs extends Reacord {
|
||||
})
|
||||
}
|
||||
|
||||
private createEphemeralInteractionReplyRenderer(
|
||||
interaction:
|
||||
| Discord.CommandInteraction
|
||||
| Discord.MessageComponentInteraction,
|
||||
) {
|
||||
return new InteractionReplyRenderer({
|
||||
type: "command",
|
||||
id: interaction.id,
|
||||
reply: async (options) => {
|
||||
await interaction.reply({
|
||||
...getDiscordMessageOptions(options),
|
||||
ephemeral: true,
|
||||
})
|
||||
return createEphemeralReacordMessage()
|
||||
},
|
||||
followUp: async (options) => {
|
||||
await interaction.followUp({
|
||||
...getDiscordMessageOptions(options),
|
||||
ephemeral: true,
|
||||
})
|
||||
return createEphemeralReacordMessage()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private createReacordComponentInteraction(
|
||||
interaction: Discord.MessageComponentInteraction,
|
||||
): ComponentInteraction {
|
||||
// todo please dear god clean this up
|
||||
const channel: ChannelInfo = interaction.channel
|
||||
const channel: ComponentEventChannel = interaction.channel
|
||||
? {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.channel, [
|
||||
@@ -178,7 +210,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
: raise("Non-channel interactions are not supported")
|
||||
|
||||
const message: MessageInfo =
|
||||
const message: ComponentEventMessage =
|
||||
interaction.message instanceof Discord.Message
|
||||
? {
|
||||
...pick(interaction.message, [
|
||||
@@ -201,7 +233,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
: raise("Message not found")
|
||||
|
||||
const member: GuildMemberInfo | undefined =
|
||||
const member: ComponentEventGuildMember | undefined =
|
||||
interaction.member instanceof Discord.GuildMember
|
||||
? {
|
||||
...pruneNullishValues(
|
||||
@@ -226,14 +258,14 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
: undefined
|
||||
|
||||
const guild: GuildInfo | undefined = interaction.guild
|
||||
const guild: ComponentEventGuild | undefined = interaction.guild
|
||||
? {
|
||||
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
||||
member: member ?? raise("unexpected: member is undefined"),
|
||||
}
|
||||
: undefined
|
||||
|
||||
const user: UserInfo = {
|
||||
const user: ComponentEventUser = {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
||||
),
|
||||
@@ -275,15 +307,18 @@ export class ReacordDiscordJs extends Reacord {
|
||||
user,
|
||||
guild,
|
||||
|
||||
reply: (content?: ReactNode) =>
|
||||
reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
|
||||
this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, options ?? {}),
|
||||
content,
|
||||
),
|
||||
|
||||
/** @deprecated Use event.reply(content, { ephemeral: true }) */
|
||||
ephemeralReply: (content: ReactNode) =>
|
||||
this.createInstance(
|
||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, {
|
||||
ephemeral: true,
|
||||
}),
|
||||
content,
|
||||
),
|
||||
},
|
||||
@@ -322,19 +357,6 @@ function createReacordMessage(message: Discord.Message): Message {
|
||||
}
|
||||
}
|
||||
|
||||
function createEphemeralReacordMessage(): Message {
|
||||
return {
|
||||
edit: () => {
|
||||
console.warn("Ephemeral messages can't be edited")
|
||||
return Promise.resolve()
|
||||
},
|
||||
delete: () => {
|
||||
console.warn("Ephemeral messages can't be deleted")
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
||||
const styleMap = {
|
||||
primary: Discord.ButtonStyle.Primary,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interaction } from "../interaction"
|
||||
import type { Message, MessageOptions } from "../message"
|
||||
import { Renderer } from "./renderer"
|
||||
|
||||
@@ -6,17 +5,23 @@ import { Renderer } from "./renderer"
|
||||
// so we know whether to call reply() or followUp()
|
||||
const repliedInteractionIds = new Set<string>()
|
||||
|
||||
export type InteractionReplyRendererImplementation = {
|
||||
interactionId: string
|
||||
reply: (options: MessageOptions) => Promise<Message>
|
||||
followUp: (options: MessageOptions) => Promise<Message>
|
||||
}
|
||||
|
||||
export class InteractionReplyRenderer extends Renderer {
|
||||
constructor(private interaction: Interaction) {
|
||||
constructor(private implementation: InteractionReplyRendererImplementation) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||
if (repliedInteractionIds.has(this.interaction.id)) {
|
||||
return this.interaction.followUp(options)
|
||||
if (repliedInteractionIds.has(this.implementation.interactionId)) {
|
||||
return this.implementation.followUp(options)
|
||||
}
|
||||
|
||||
repliedInteractionIds.add(this.interaction.id)
|
||||
return this.interaction.reply(options)
|
||||
repliedInteractionIds.add(this.implementation.interactionId)
|
||||
return this.implementation.reply(options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "reacord",
|
||||
"type": "module",
|
||||
"description": "Create interactive Discord messages using React.",
|
||||
"version": "0.5.5",
|
||||
"version": "0.6.0",
|
||||
"homepage": "https://reacord.mapleleaf.dev",
|
||||
"repository": "https://github.com/itsMapleLeaf/reacord.git",
|
||||
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap --dts --dts-resolve",
|
||||
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve",
|
||||
"build-watch": "pnpm build -- --watch",
|
||||
"test": "vitest --coverage --no-watch",
|
||||
"test-dev": "vitest",
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -8,10 +8,11 @@ import { setTimeout } from "node:timers/promises"
|
||||
import type { ReactNode } from "react"
|
||||
import { expect } from "vitest"
|
||||
import type {
|
||||
ChannelInfo,
|
||||
GuildInfo,
|
||||
MessageInfo,
|
||||
UserInfo,
|
||||
ComponentEventChannel,
|
||||
ComponentEventGuild,
|
||||
ComponentEventMessage,
|
||||
ComponentEventReplyOptions,
|
||||
ComponentEventUser,
|
||||
} from "../library/core/component-event"
|
||||
import type { ButtonClickEvent } from "../library/core/components/button"
|
||||
import type { SelectChangeEvent } from "../library/core/components/select"
|
||||
@@ -21,12 +22,14 @@ import type { Channel } from "../library/internal/channel"
|
||||
import { Container } from "../library/internal/container"
|
||||
import type {
|
||||
ButtonInteraction,
|
||||
CommandInteraction,
|
||||
SelectInteraction,
|
||||
} from "../library/internal/interaction"
|
||||
import type { Message, MessageOptions } from "../library/internal/message"
|
||||
import { ChannelMessageRenderer } from "../library/internal/renderers/channel-message-renderer"
|
||||
import { InteractionReplyRenderer } from "../library/internal/renderers/interaction-reply-renderer"
|
||||
import {
|
||||
InteractionReplyRenderer,
|
||||
type InteractionReplyRendererImplementation,
|
||||
} from "../library/internal/renderers/interaction-reply-renderer"
|
||||
|
||||
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
|
||||
|
||||
@@ -42,26 +45,28 @@ 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?: ComponentEventReplyOptions,
|
||||
): 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 +74,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()
|
||||
@@ -171,9 +176,8 @@ class TestMessage implements Message {
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandInteraction implements CommandInteraction {
|
||||
readonly type = "command"
|
||||
readonly id = "test-command-interaction"
|
||||
class TestCommandInteraction implements InteractionReplyRendererImplementation {
|
||||
readonly interactionId = "test-command-interaction"
|
||||
readonly channelId = "test-channel-id"
|
||||
|
||||
constructor(private messageContainer: Container<TestMessage>) {}
|
||||
@@ -248,17 +252,19 @@ class TestSelectInteraction
|
||||
class TestComponentEvent {
|
||||
constructor(private tester: ReacordTester) {}
|
||||
|
||||
message: MessageInfo = {} as MessageInfo // todo
|
||||
channel: ChannelInfo = {} as ChannelInfo // todo
|
||||
user: UserInfo = {} as UserInfo // todo
|
||||
guild: GuildInfo = {} as GuildInfo // todo
|
||||
message: ComponentEventMessage = {} as ComponentEventMessage // todo
|
||||
channel: ComponentEventChannel = {} as ComponentEventChannel // todo
|
||||
user: ComponentEventUser = {} as ComponentEventUser // todo
|
||||
guild: ComponentEventGuild = {} as ComponentEventGuild // 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)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# website
|
||||
|
||||
## 0.4.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [11153df]
|
||||
- Updated dependencies [fb0a997]
|
||||
- reacord@0.6.0
|
||||
|
||||
## 0.4.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "website",
|
||||
"version": "0.4.6",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
|
||||
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>
|
||||
|
||||
@@ -6,7 +6,7 @@ slug: getting-started
|
||||
|
||||
# Getting Started
|
||||
|
||||
These guides assume some familiarity with JavaScript, [React](https://reactjs.org), [Discord.js](https://discord.js.org) and the [Discord API](https://discord.dev). Keep these pages as reference if you need it.
|
||||
These guides assume some familiarity with [JavaScript](https://developer.mozilla.org/en-US/docs/Web/javascript), [React](https://reactjs.org), [Discord.js](https://discord.js.org) and the [Discord API](https://discord.dev). Keep these pages as reference if you need it.
|
||||
|
||||
## Setup from template
|
||||
|
||||
@@ -47,6 +47,13 @@ await client.login(process.env.BOT_TOKEN)
|
||||
To use JSX in your code, run it with [tsx](https://npm.im/tsx):
|
||||
|
||||
```bash
|
||||
npm install tsx
|
||||
tsx main.tsx
|
||||
npm install -D tsx
|
||||
npx tsx main.tsx
|
||||
```
|
||||
|
||||
For production, I recommend compiling it with [tsup](https://npm.im/tsup):
|
||||
|
||||
```bash
|
||||
npm install -D tsup
|
||||
npx tsup src/main.tsx --target node20
|
||||
```
|
||||
|
||||
@@ -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,8 @@ function Uptime() {
|
||||
}
|
||||
|
||||
client.on("ready", () => {
|
||||
reacord.send(channelId, <Uptime />)
|
||||
const instance = reacord.createChannelMessage(channel)
|
||||
instance.render(<Uptime />)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -46,12 +46,26 @@ 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" />)
|
||||
})
|
||||
```
|
||||
|
||||
You can specify various options for the message:
|
||||
|
||||
```jsx
|
||||
const instance = reacord.createChannelMessage(channel, {
|
||||
tts: true,
|
||||
reply: {
|
||||
messageReference: someMessage.id,
|
||||
},
|
||||
flags: [MessageFlags.SuppressNotifications],
|
||||
})
|
||||
```
|
||||
|
||||
See the [Discord.js docs](https://discord.js.org/#/docs/discord.js/main/typedef/MessageCreateOptions) for all of the available options.
|
||||
|
||||
## Cleaning Up Instances
|
||||
|
||||
If you no longer want to use the instance, you can clean it up in a few ways:
|
||||
@@ -75,7 +89,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()`. Here's an example:
|
||||
|
||||
```jsx
|
||||
import { Client } from "discord.js"
|
||||
@@ -94,8 +108,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,14 +148,14 @@ 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</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -149,18 +163,36 @@ handleCommands(client, [
|
||||
|
||||
## Ephemeral Command Replies
|
||||
|
||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.ephemeralReply()` function.
|
||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.createInteractionReply()` function and provide `ephemeral` option.
|
||||
|
||||
```tsx
|
||||
```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