35 Commits

Author SHA1 Message Date
itsMapleLeaf
9594542869 remove vercel config 2023-10-29 14:01:04 -05:00
itsMapleLeaf
17978a5252 node 16 is EOL 2023-10-28 14:47:01 -05:00
itsMapleLeaf
95fb342183 remove format and fix lint:prettier 2023-10-28 14:46:25 -05:00
itsMapleLeaf
0772ca4502 fix test command interaction 2023-10-28 14:45:03 -05:00
itsMapleLeaf
11153dfe0f breaking: more descriptive component event types 2023-10-28 14:39:16 -05:00
itsMapleLeaf
fb0a997855 changeset 2023-10-28 14:34:13 -05:00
itsMapleLeaf
da1c62f2f0 public interface tweaks and such 2023-10-28 14:34:09 -05:00
Darius
cdc22b7916 Merge pull request #40 from domin-mnd/main 2023-10-28 13:04:33 -05:00
Domin-MND
7fee69c8ae fix select-menu guide 2023-10-27 16:09:18 +03:00
Domin-MND
c2e5dc04dd fix api guides 2023-10-27 16:06:00 +03:00
Domin-MND
390da4cab6 remove initial content for create methods 2023-10-24 19:58:48 +03:00
Domin-MND
def0c46f13 fix monorepo formatting 2023-10-23 23:25:44 +03:00
Domin-MND
8b6e283810 update guides 2023-10-23 23:22:25 +03:00
Domin-MND
13fcf7ddc9 match test adapter syntax 2023-10-23 22:25:06 +03:00
Domin-MND
ce12351a24 fix formatting 2023-10-23 22:08:08 +03:00
Domin-MND
73bb098774 add options for component event 2023-10-23 22:05:05 +03:00
Domin-MND
4ee4d4ab91 add options for component event 2023-10-23 22:02:33 +03:00
Domin-MND
f998a0e09a fix djs manual test 2023-10-23 12:24:24 +03:00
Domin-MND
453192cc96 cleanup 2023-10-23 11:51:59 +03:00
Domin-MND
d387f669ab more descriptive djs adapter methods 2023-10-21 11:16:58 +03:00
Darius
9aec87ae9f Merge pull request #39 from domin-mnd/main 2023-10-19 13:05:01 -05:00
Domin-MND
65d1d68bb0 fix id raising 2023-10-19 16:37:51 +03:00
Domin-MND
dfb7562c97 use reply renderer for ephermalReply 2023-10-18 21:48:38 +03:00
Domin-MND
9e2be6c2e0 add opts argument support 2023-10-18 21:39:17 +03:00
Domin-MND
d078995516 deprecate ephemeralReply in adapters 2023-10-18 20:59:14 +03:00
Darius
82b3575f2d Merge pull request #37 from itsMapleLeaf/changeset-release/main 2023-10-10 10:53:03 -05:00
github-actions[bot]
82b811c98b Version Packages 2023-10-10 15:51:06 +00:00
itsMapleLeaf
3a786310b1 upgrades 2023-10-10 10:50:15 -05:00
itsMapleLeaf
ced48a3ecb distribute .d.ts files 2023-10-10 10:47:31 -05:00
itsMapleLeaf
37b75a99e2 use type:module in helpers 2023-10-10 10:45:50 -05:00
itsMapleLeaf
f2f215d6b9 fix banner in readme 2023-09-28 12:47:39 -05:00
Darius
1f67e7c263 Merge pull request #35 from itsMapleLeaf/changeset-release/main 2023-09-28 12:33:08 -05:00
github-actions[bot]
d4f1bb4d4b Version Packages 2023-09-28 17:23:24 +00:00
Darius
47c9b75940 Merge pull request #34 from itsMapleLeaf/fix-type-definitions 2023-09-28 12:22:52 -05:00
itsMapleLeaf
41c87e3dcc fix typedefs 2023-09-28 12:20:58 -05:00
32 changed files with 1394 additions and 1013 deletions

View File

@@ -0,0 +1,5 @@
---
"reacord": minor
---
breaking: more descriptive component event types

View File

@@ -0,0 +1,33 @@
---
"reacord": minor
---
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.

View File

@@ -1,5 +1,5 @@
<center> <center>
<img src="./packages/website/app/assets/banner.png" alt="Reacord: Create interactive Discord messages using React"> <img src="packages/website/src/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
</center> </center>
## Installation ∙ [![npm](https://img.shields.io/npm/v/reacord?color=blue&style=flat-square)](https://www.npmjs.com/package/reacord) ## Installation ∙ [![npm](https://img.shields.io/npm/v/reacord?color=blue&style=flat-square)](https://www.npmjs.com/package/reacord)

View File

@@ -4,12 +4,9 @@
"scripts": { "scripts": {
"lint": "run-s --continue-on-error lint:*", "lint": "run-s --continue-on-error lint:*",
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives", "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", "lint:types": "tsc -b & pnpm -r --parallel run typecheck",
"astro-sync": "pnpm --filter website exec astro sync", "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", "test": "vitest",
"build": "pnpm -r run build", "build": "pnpm -r run build",
"build:website": "pnpm --filter website... run build", "build:website": "pnpm --filter website... run build",
@@ -20,13 +17,13 @@
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.26.2", "@changesets/cli": "^2.26.2",
"@itsmapleleaf/configs": "github:itsMapleLeaf/configs", "@itsmapleleaf/configs": "github:itsMapleLeaf/configs",
"eslint": "^8.50.0", "eslint": "^8.51.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"react": "^18.2.0", "react": "^18.2.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vitest": "^0.34.5" "vitest": "^0.34.6"
}, },
"prettier": "@itsmapleleaf/configs/prettier", "prettier": "@itsmapleleaf/configs/prettier",
"eslintConfig": { "eslintConfig": {

View File

@@ -1,5 +1,6 @@
{ {
"name": "@reacord/helpers", "name": "@reacord/helpers",
"type": "module",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -8,7 +9,7 @@
"dependencies": { "dependencies": {
"@types/lodash-es": "^4.17.9", "@types/lodash-es": "^4.17.9",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"type-fest": "^4.3.2", "type-fest": "^4.4.0",
"vitest": "^0.34.5" "vitest": "^0.34.6"
} }
} }

View File

@@ -1,5 +1,23 @@
# reacord # reacord
## 0.5.5
### Patch Changes
- ced48a3: distribute d.ts files again instead of the source
distributing the source causes typecheck errors when the modules it imports from (in this case, `@reacord/helpers`) don't exist in the end users' projects, so we'll just distribute d.ts files instead like normal. failed experiment :(
## 0.5.4
### Patch Changes
- 41c87e3: fix type definitions
`"types"` wasn't updated, oops!
technically the typedefs were already correctly defined via `"exports"`, but this may not be picked up depending on the tsconfig, so I'll ensure both are used for compatibility purposes. but this might be worth a note in the docs; pretty much every modern TS Node project should be using a tsconfig that doesn't require setting `"types"`
## 0.5.3 ## 0.5.3
### Patch Changes ### Patch Changes

View File

@@ -9,41 +9,52 @@ export interface ComponentEvent {
* *
* @see https://discord.com/developers/docs/resources/channel#message-object * @see https://discord.com/developers/docs/resources/channel#message-object
*/ */
message: MessageInfo message: ComponentEventMessage
/** /**
* The channel that this event occurred in. * The channel that this event occurred in.
* *
* @see https://discord.com/developers/docs/resources/channel#channel-object * @see https://discord.com/developers/docs/resources/channel#channel-object
*/ */
channel: ChannelInfo channel: ComponentEventChannel
/** /**
* The user that triggered this event. * The user that triggered this event.
* *
* @see https://discord.com/developers/docs/resources/user#user-object * @see https://discord.com/developers/docs/resources/user#user-object
*/ */
user: UserInfo user: ComponentEventUser
/** /**
* The guild that this event occurred in. * The guild that this event occurred in.
* *
* @see https://discord.com/developers/docs/resources/guild#guild-object * @see https://discord.com/developers/docs/resources/guild#guild-object
*/ */
guild?: GuildInfo guild?: ComponentEventGuild
/** Create a new reply to this event. */ /** 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 * 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 */ /** @category Component Event */
export interface ChannelInfo { export interface ComponentEventReplyOptions {
ephemeral?: boolean
tts?: boolean
}
/** @category Component Event */
export interface ComponentEventChannel {
id: string id: string
name?: string name?: string
topic?: string topic?: string
@@ -55,11 +66,11 @@ export interface ChannelInfo {
} }
/** @category Component Event */ /** @category Component Event */
export interface MessageInfo { export interface ComponentEventMessage {
id: string id: string
channelId: string channelId: string
authorId: string authorId: string
member?: GuildMemberInfo member?: ComponentEventGuildMember
content: string content: string
timestamp: string timestamp: string
editedTimestamp?: string editedTimestamp?: string
@@ -70,14 +81,14 @@ export interface MessageInfo {
} }
/** @category Component Event */ /** @category Component Event */
export interface GuildInfo { export interface ComponentEventGuild {
id: string id: string
name: string name: string
member: GuildMemberInfo member: ComponentEventGuildMember
} }
/** @category Component Event */ /** @category Component Event */
export interface GuildMemberInfo { export interface ComponentEventGuildMember {
id: string id: string
nick?: string nick?: string
displayName: string displayName: string
@@ -92,7 +103,7 @@ export interface GuildMemberInfo {
} }
/** @category Component Event */ /** @category Component Event */
export interface UserInfo { export interface ComponentEventUser {
id: string id: string
username: string username: string
discriminator: string discriminator: 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

@@ -14,11 +14,12 @@ import type {
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer" import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer" import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
import type { import type {
ChannelInfo, ComponentEventChannel,
GuildInfo, ComponentEventGuild,
GuildMemberInfo, ComponentEventGuildMember,
MessageInfo, ComponentEventMessage,
UserInfo, ComponentEventReplyOptions,
ComponentEventUser,
} 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"
@@ -48,14 +49,49 @@ export class ReacordDiscordJs extends Reacord {
/** /**
* Sends a message to a channel. * 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 * @see https://reacord.mapleleaf.dev/guides/sending-messages
*/ */
override send( public send(
channelId: string, channel: Discord.ChannelResolvable,
initialContent?: React.ReactNode, initialContent?: React.ReactNode,
): ReacordInstance { ): ReacordInstance {
return this.createInstance( return this.createInstance(
this.createChannelRenderer(channelId), this.createChannelMessageRenderer(channel, {}),
initialContent, initialContent,
) )
} }
@@ -63,14 +99,15 @@ 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,
): ReacordInstance { ): ReacordInstance {
return this.createInstance( return this.createInstance(
this.createInteractionReplyRenderer(interaction), this.createInteractionReplyRenderer(interaction, {}),
initialContent, initialContent,
) )
} }
@@ -78,31 +115,49 @@ 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.createInteractionReply(interaction, { 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,
): ReacordInstance { ): ReacordInstance {
return this.createInstance( return this.createInstance(
this.createEphemeralInteractionReplyRenderer(interaction), this.createInteractionReplyRenderer(interaction, {
ephemeral: true,
}),
initialContent, initialContent,
) )
} }
private createChannelRenderer(channelId: string) { private createChannelMessageRenderer(
channelResolvable: Discord.ChannelResolvable,
messageCreateOptions?: Discord.MessageCreateOptions,
) {
return new ChannelMessageRenderer({ return new ChannelMessageRenderer({
send: async (options) => { send: async (messageOptions) => {
const channel = let channel = this.client.channels.resolve(channelResolvable)
this.client.channels.cache.get(channelId) ?? if (!channel && typeof channelResolvable === "string") {
(await this.client.channels.fetch(channelId)) ?? channel = await this.client.channels.fetch(channelResolvable)
raise(`Channel ${channelId} not found`)
if (!channel.isTextBased()) {
raise(`Channel ${channelId} is not a text channel`)
} }
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) return createReacordMessage(message)
}, },
}) })
@@ -112,20 +167,22 @@ export class ReacordDiscordJs extends Reacord {
interaction: interaction:
| Discord.CommandInteraction | Discord.CommandInteraction
| Discord.MessageComponentInteraction, | Discord.MessageComponentInteraction,
interactionReplyOptions: Discord.InteractionReplyOptions,
) { ) {
return new InteractionReplyRenderer({ return new InteractionReplyRenderer({
type: "command", interactionId: interaction.id,
id: interaction.id, reply: async (messageOptions) => {
reply: async (options) => {
const message = await interaction.reply({ const message = await interaction.reply({
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(messageOptions),
...interactionReplyOptions,
fetchReply: true, fetchReply: true,
}) })
return createReacordMessage(message) return createReacordMessage(message)
}, },
followUp: async (options) => { followUp: async (messageOptions) => {
const message = await interaction.followUp({ const message = await interaction.followUp({
...getDiscordMessageOptions(options), ...getDiscordMessageOptions(messageOptions),
...interactionReplyOptions,
fetchReply: true, fetchReply: true,
}) })
return createReacordMessage(message) 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( private createReacordComponentInteraction(
interaction: Discord.MessageComponentInteraction, interaction: Discord.MessageComponentInteraction,
): ComponentInteraction { ): ComponentInteraction {
// todo please dear god clean this up // todo please dear god clean this up
const channel: ChannelInfo = interaction.channel const channel: ComponentEventChannel = interaction.channel
? { ? {
...pruneNullishValues( ...pruneNullishValues(
pick(interaction.channel, [ pick(interaction.channel, [
@@ -178,7 +210,7 @@ export class ReacordDiscordJs extends Reacord {
} }
: raise("Non-channel interactions are not supported") : raise("Non-channel interactions are not supported")
const message: MessageInfo = const message: ComponentEventMessage =
interaction.message instanceof Discord.Message interaction.message instanceof Discord.Message
? { ? {
...pick(interaction.message, [ ...pick(interaction.message, [
@@ -201,7 +233,7 @@ export class ReacordDiscordJs extends Reacord {
} }
: raise("Message not found") : raise("Message not found")
const member: GuildMemberInfo | undefined = const member: ComponentEventGuildMember | undefined =
interaction.member instanceof Discord.GuildMember interaction.member instanceof Discord.GuildMember
? { ? {
...pruneNullishValues( ...pruneNullishValues(
@@ -226,14 +258,14 @@ export class ReacordDiscordJs extends Reacord {
} }
: undefined : undefined
const guild: GuildInfo | undefined = interaction.guild const guild: ComponentEventGuild | undefined = interaction.guild
? { ? {
...pruneNullishValues(pick(interaction.guild, ["id", "name"])), ...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
member: member ?? raise("unexpected: member is undefined"), member: member ?? raise("unexpected: member is undefined"),
} }
: undefined : undefined
const user: UserInfo = { const user: ComponentEventUser = {
...pruneNullishValues( ...pruneNullishValues(
pick(interaction.user, ["id", "username", "discriminator", "tag"]), pick(interaction.user, ["id", "username", "discriminator", "tag"]),
), ),
@@ -275,15 +307,18 @@ export class ReacordDiscordJs extends Reacord {
user, user,
guild, guild,
reply: (content?: ReactNode) => reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
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.createEphemeralInteractionReplyRenderer(interaction), this.createInteractionReplyRenderer(interaction, {
ephemeral: true,
}),
content, 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"]) { function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
const styleMap = { const styleMap = {
primary: Discord.ButtonStyle.Primary, primary: Discord.ButtonStyle.Primary,

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

@@ -1,4 +1,3 @@
import type { Interaction } from "../interaction"
import type { Message, MessageOptions } from "../message" import type { Message, MessageOptions } from "../message"
import { Renderer } from "./renderer" import { Renderer } from "./renderer"
@@ -6,17 +5,23 @@ import { Renderer } from "./renderer"
// so we know whether to call reply() or followUp() // so we know whether to call reply() or followUp()
const repliedInteractionIds = new Set<string>() 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 { export class InteractionReplyRenderer extends Renderer {
constructor(private interaction: Interaction) { constructor(private implementation: InteractionReplyRendererImplementation) {
super() super()
} }
protected createMessage(options: MessageOptions): Promise<Message> { protected createMessage(options: MessageOptions): Promise<Message> {
if (repliedInteractionIds.has(this.interaction.id)) { if (repliedInteractionIds.has(this.implementation.interactionId)) {
return this.interaction.followUp(options) return this.implementation.followUp(options)
} }
repliedInteractionIds.add(this.interaction.id) repliedInteractionIds.add(this.implementation.interactionId)
return this.interaction.reply(options) return this.implementation.reply(options)
} }
} }

View File

@@ -2,8 +2,7 @@
"name": "reacord", "name": "reacord",
"type": "module", "type": "module",
"description": "Create interactive Discord messages using React.", "description": "Create interactive Discord messages using React.",
"version": "0.5.3", "version": "0.5.5",
"types": "./dist/main.d.ts",
"homepage": "https://reacord.mapleleaf.dev", "homepage": "https://reacord.mapleleaf.dev",
"repository": "https://github.com/itsMapleLeaf/reacord.git", "repository": "https://github.com/itsMapleLeaf/reacord.git",
"changelog": "https://github.com/itsMapleLeaf/reacord/releases", "changelog": "https://github.com/itsMapleLeaf/reacord/releases",
@@ -20,16 +19,16 @@
"reacord" "reacord"
], ],
"files": [ "files": [
"library",
"dist", "dist",
"README.md", "README.md",
"LICENSE" "LICENSE"
], ],
"types": "./dist/main.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./dist/main.js", "import": "./dist/main.js",
"require": "./dist/main.cjs", "require": "./dist/main.cjs",
"types": "./library/main.ts" "types": "./dist/main.d.ts"
}, },
"./package.json": { "./package.json": {
"import": "./package.json", "import": "./package.json",
@@ -37,7 +36,7 @@
} }
}, },
"scripts": { "scripts": {
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap", "build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve",
"build-watch": "pnpm build -- --watch", "build-watch": "pnpm build -- --watch",
"test": "vitest --coverage --no-watch", "test": "vitest --coverage --no-watch",
"test-dev": "vitest", "test-dev": "vitest",
@@ -45,8 +44,8 @@
"typecheck": "tsc -b" "typecheck": "tsc -b"
}, },
"dependencies": { "dependencies": {
"@types/node": "^20.7.0", "@types/node": "^20.8.4",
"@types/react": "^18.2.23", "@types/react": "^18.2.27",
"@types/react-reconciler": "^0.28.5", "@types/react-reconciler": "^0.28.5",
"react-reconciler": "^0.29.0", "react-reconciler": "^0.29.0",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
@@ -74,7 +73,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"tsup": "^7.2.0", "tsup": "^7.2.0",
"tsx": "^3.13.0", "tsx": "^3.13.0",
"type-fest": "^4.3.2" "type-fest": "^4.4.0"
}, },
"release-it": { "release-it": {
"git": { "git": {

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

@@ -8,10 +8,11 @@ import { setTimeout } from "node:timers/promises"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import { expect } from "vitest" import { expect } from "vitest"
import type { import type {
ChannelInfo, ComponentEventChannel,
GuildInfo, ComponentEventGuild,
MessageInfo, ComponentEventMessage,
UserInfo, ComponentEventReplyOptions,
ComponentEventUser,
} 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"
import type { SelectChangeEvent } from "../library/core/components/select" 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 { Container } from "../library/internal/container"
import type { import type {
ButtonInteraction, ButtonInteraction,
CommandInteraction,
SelectInteraction, SelectInteraction,
} from "../library/internal/interaction" } from "../library/internal/interaction"
import type { Message, MessageOptions } from "../library/internal/message" import type { Message, MessageOptions } from "../library/internal/message"
import { ChannelMessageRenderer } from "../library/internal/renderers/channel-message-renderer" 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] export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
@@ -42,26 +45,28 @@ 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?: ComponentEventReplyOptions,
): 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 +74,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()
@@ -171,9 +176,8 @@ class TestMessage implements Message {
} }
} }
class TestCommandInteraction implements CommandInteraction { class TestCommandInteraction implements InteractionReplyRendererImplementation {
readonly type = "command" readonly interactionId = "test-command-interaction"
readonly id = "test-command-interaction"
readonly channelId = "test-channel-id" readonly channelId = "test-channel-id"
constructor(private messageContainer: Container<TestMessage>) {} constructor(private messageContainer: Container<TestMessage>) {}
@@ -248,17 +252,19 @@ class TestSelectInteraction
class TestComponentEvent { class TestComponentEvent {
constructor(private tester: ReacordTester) {} constructor(private tester: ReacordTester) {}
message: MessageInfo = {} as MessageInfo // todo message: ComponentEventMessage = {} as ComponentEventMessage // todo
channel: ChannelInfo = {} as ChannelInfo // todo channel: ComponentEventChannel = {} as ComponentEventChannel // todo
user: UserInfo = {} as UserInfo // todo user: ComponentEventUser = {} as ComponentEventUser // todo
guild: GuildInfo = {} as GuildInfo // todo guild: ComponentEventGuild = {} as ComponentEventGuild // 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)

View File

@@ -1,5 +1,19 @@
# website # website
## 0.4.6
### Patch Changes
- Updated dependencies [ced48a3]
- reacord@0.5.5
## 0.4.5
### Patch Changes
- Updated dependencies [41c87e3]
- reacord@0.5.4
## 0.4.4 ## 0.4.4
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "website", "name": "website",
"version": "0.4.4", "version": "0.4.6",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
@@ -14,13 +14,13 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/prefetch": "^0.3.0", "@astrojs/prefetch": "^0.3.0",
"@astrojs/react": "^2.2.2", "@astrojs/react": "^2.3.2",
"@fontsource/jetbrains-mono": "^4.5.12", "@fontsource/jetbrains-mono": "^4.5.12",
"@fontsource/rubik": "^4.5.14", "@fontsource/rubik": "^4.5.14",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@reacord/helpers": "workspace:^", "@reacord/helpers": "workspace:^",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"astro": "^2.10.9", "astro": "^2.10.15",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"reacord": "workspace:*", "reacord": "workspace:*",
"react": "^18.2.0", "react": "^18.2.0",
@@ -30,12 +30,12 @@
"devDependencies": { "devDependencies": {
"@astrojs/tailwind": "^4.0.0", "@astrojs/tailwind": "^4.0.0",
"@total-typescript/ts-reset": "^0.5.1", "@total-typescript/ts-reset": "^0.5.1",
"@types/node": "^20.7.0", "@types/node": "^20.8.4",
"@types/react": "^18.2.23", "@types/react": "^18.2.27",
"@types/react-dom": "^18.2.8", "@types/react-dom": "^18.2.12",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typedoc": "^0.25.1", "typedoc": "^0.25.2",
"wait-on": "^7.0.1" "wait-on": "^7.0.1"
} }
} }

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

@@ -6,7 +6,7 @@ slug: getting-started
# 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 ## 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): To use JSX in your code, run it with [tsx](https://npm.im/tsx):
```bash ```bash
npm install tsx npm install -D tsx
tsx main.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
``` ```

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,8 @@ function Uptime() {
} }
client.on("ready", () => { 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}!</> 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" />)
}) })
``` ```
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 ## 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 +89,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()`. Here's an example:
```jsx ```jsx
import { Client } from "discord.js" import { Client } from "discord.js"
@@ -94,8 +108,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,14 +148,14 @@ 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</>)
}, },
}, },
]) ])
@@ -149,18 +163,36 @@ handleCommands(client, [
## Ephemeral Command Replies ## 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, [ 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 />)
``` ```

1735
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
{
"github": {
"silent": true
}
}