Compare commits
35 Commits
reacord@0.
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9594542869 | ||
|
|
17978a5252 | ||
|
|
95fb342183 | ||
|
|
0772ca4502 | ||
|
|
11153dfe0f | ||
|
|
fb0a997855 | ||
|
|
da1c62f2f0 | ||
|
|
cdc22b7916 | ||
|
|
7fee69c8ae | ||
|
|
c2e5dc04dd | ||
|
|
390da4cab6 | ||
|
|
def0c46f13 | ||
|
|
8b6e283810 | ||
|
|
13fcf7ddc9 | ||
|
|
ce12351a24 | ||
|
|
73bb098774 | ||
|
|
4ee4d4ab91 | ||
|
|
f998a0e09a | ||
|
|
453192cc96 | ||
|
|
d387f669ab | ||
|
|
9aec87ae9f | ||
|
|
65d1d68bb0 | ||
|
|
dfb7562c97 | ||
|
|
9e2be6c2e0 | ||
|
|
d078995516 | ||
|
|
82b3575f2d | ||
|
|
82b811c98b | ||
|
|
3a786310b1 | ||
|
|
ced48a3ecb | ||
|
|
37b75a99e2 | ||
|
|
f2f215d6b9 | ||
|
|
1f67e7c263 | ||
|
|
d4f1bb4d4b | ||
|
|
47c9b75940 | ||
|
|
41c87e3dcc |
5
.changeset/five-wolves-destroy.md
Normal file
5
.changeset/five-wolves-destroy.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"reacord": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: more descriptive component event types
|
||||||
33
.changeset/many-pets-melt.md
Normal file
33
.changeset/many-pets-melt.md
Normal 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.
|
||||||
@@ -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 ∙ [](https://www.npmjs.com/package/reacord)
|
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const createTest = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createTest("basic", (channel) => {
|
await createTest("basic", (channel) => {
|
||||||
reacord.send(channel.id, "Hello, world!")
|
reacord.createChannelMessage(channel).render("Hello, world!")
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("counter", (channel) => {
|
await createTest("counter", (channel) => {
|
||||||
@@ -73,7 +73,7 @@ await createTest("counter", (channel) => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
reacord.send(channel.id, <Counter />)
|
reacord.createChannelMessage(channel).render(<Counter />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("select", (channel) => {
|
await createTest("select", (channel) => {
|
||||||
@@ -102,8 +102,7 @@ await createTest("select", (channel) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = reacord.send(
|
const instance = reacord.createChannelMessage(channel).render(
|
||||||
channel.id,
|
|
||||||
<FruitSelect
|
<FruitSelect
|
||||||
onConfirm={(value) => {
|
onConfirm={(value) => {
|
||||||
instance.render(`you chose ${value}`)
|
instance.render(`you chose ${value}`)
|
||||||
@@ -114,8 +113,7 @@ await createTest("select", (channel) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await createTest("ephemeral button", (channel) => {
|
await createTest("ephemeral button", (channel) => {
|
||||||
reacord.send(
|
reacord.createChannelMessage(channel).render(
|
||||||
channel.id,
|
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="public clic"
|
label="public clic"
|
||||||
@@ -125,7 +123,7 @@ await createTest("ephemeral button", (channel) => {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="clic"
|
label="clic"
|
||||||
onClick={(event) => event.ephemeralReply("you clic")}
|
onClick={(event) => event.reply("you clic", { ephemeral: true })}
|
||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
)
|
)
|
||||||
@@ -136,9 +134,11 @@ await createTest("delete this", (channel) => {
|
|||||||
const instance = useInstance()
|
const instance = useInstance()
|
||||||
return <Button label="delete this" onClick={() => instance.destroy()} />
|
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||||
}
|
}
|
||||||
reacord.send(channel.id, <DeleteThis />)
|
reacord.createChannelMessage(channel).render(<DeleteThis />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("link", (channel) => {
|
await createTest("link", (channel) => {
|
||||||
reacord.send(channel.id, <Link label="hi" url="https://mapleleaf.dev" />)
|
reacord
|
||||||
|
.createChannelMessage(channel)
|
||||||
|
.render(<Link label="hi" url="https://mapleleaf.dev" />)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { test } from "vitest"
|
|||||||
test("rendering behavior", async () => {
|
test("rendering behavior", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = tester.reply()
|
const reply = tester
|
||||||
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
.createInteractionReply()
|
||||||
|
.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||||
|
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
|
|||||||
test("delete", async () => {
|
test("delete", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = tester.reply()
|
const reply = tester.createInteractionReply().render(
|
||||||
reply.render(
|
|
||||||
<>
|
<>
|
||||||
some text
|
some text
|
||||||
<Embed>some embed</Embed>
|
<Embed>some embed</Embed>
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ test("single select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
@@ -119,9 +117,7 @@ test("multiple select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
@@ -148,7 +144,7 @@ test("multiple select", async () => {
|
|||||||
|
|
||||||
test("optional onSelect + unknown value", async () => {
|
test("optional onSelect + unknown value", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
tester.reply().render(<Select placeholder="select" />)
|
tester.createInteractionReply().render(<Select placeholder="select" />)
|
||||||
await tester.findSelectByPlaceholder("select").select("something")
|
await tester.findSelectByPlaceholder("select").select("something")
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -3,5 +3,5 @@ export type Props = astroHTML.JSX.AnchorHTMLAttributes
|
|||||||
---
|
---
|
||||||
|
|
||||||
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
|
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -7,32 +7,32 @@ const guides = await getCollection("guides")
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div class="isolate">
|
<div class="isolate">
|
||||||
<header
|
<header
|
||||||
class="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex"
|
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
|
||||||
>
|
>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<MainNavigation />
|
<MainNavigation />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="container mt-8 flex items-start gap-4">
|
<main class="container mt-8 flex items-start gap-4">
|
||||||
<nav class="w-48 sticky top-24 hidden md:block">
|
<nav class="sticky top-24 hidden w-48 md:block">
|
||||||
<h2 class="text-2xl">Guides</h2>
|
<h2 class="text-2xl">Guides</h2>
|
||||||
<ul class="mt-3 flex flex-col gap-2 items-start">
|
<ul class="mt-3 flex flex-col items-start gap-2">
|
||||||
{
|
{
|
||||||
guides.map((guide) => (
|
guides.map((guide) => (
|
||||||
<li>
|
<li>
|
||||||
<a class="link" href={`/guides/${guide.slug}`}>
|
<a class="link" href={`/guides/${guide.slug}`}>
|
||||||
{guide.data.title}
|
{guide.data.title}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="prose prose-invert pb-8 flex-1 min-w-0">
|
<section class="prose prose-invert min-w-0 flex-1 pb-8">
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import faviconUrl from "~/assets/favicon.png"
|
|||||||
import "~/styles/tailwind.css"
|
import "~/styles/tailwind.css"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" class="bg-slate-900 text-slate-100">
|
<html lang="en" class="bg-slate-900 text-slate-100">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
<details class="md:hidden relative" data-menu>
|
<details class="relative md:hidden" data-menu>
|
||||||
<summary
|
<summary
|
||||||
class="list-none p-2 -m-2 cursor-pointer hover:text-emerald-500 transition"
|
class="-m-2 cursor-pointer list-none p-2 transition hover:text-emerald-500"
|
||||||
>
|
>
|
||||||
<slot name="button" />
|
<slot name="button" />
|
||||||
</summary>
|
</summary>
|
||||||
<div
|
<div
|
||||||
class="w-48 max-h-[calc(100vh-5rem)] bg-slate-800 shadow rounded-lg overflow-x-hidden overflow-y-auto top-[calc(100%+8px)] right-0 absolute z-10"
|
class="absolute right-0 top-[calc(100%+8px)] z-10 max-h-[calc(100vh-5rem)] w-48 overflow-y-auto overflow-x-hidden rounded-lg bg-slate-800 shadow"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
|
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
|
||||||
"[data-menu]",
|
"[data-menu]",
|
||||||
)) {
|
)) {
|
||||||
window.addEventListener("click", (event) => {
|
window.addEventListener("click", (event) => {
|
||||||
if (!menu.contains(event.target as Node)) {
|
if (!menu.contains(event.target as Node)) {
|
||||||
menu.open = false
|
menu.open = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
menu.addEventListener("keydown", (event) => {
|
menu.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
menu.open = false
|
menu.open = false
|
||||||
menu.querySelector("summary")!.focus()
|
menu.querySelector("summary")!.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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!</>)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ function FancyMessage({ title, description }) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
reacord.send(channelId, <FancyMessage title="Hello" description="World" />)
|
reacord
|
||||||
|
.createChannelMessage(channel)
|
||||||
|
.render(<FancyMessage title="Hello" description="World" />)
|
||||||
```
|
```
|
||||||
|
|
||||||
Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition:
|
Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition:
|
||||||
@@ -52,8 +54,7 @@ function FancyMessage({ children }) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
reacord.send(
|
reacord.createChannelMessage(channel).render(
|
||||||
channelId,
|
|
||||||
<FancyMessage>
|
<FancyMessage>
|
||||||
<FancyDetails title="Hello" description="World" />
|
<FancyDetails title="Hello" description="World" />
|
||||||
</FancyMessage>,
|
</FancyMessage>,
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ function TheButton() {
|
|||||||
const publicReply = event.reply(`${name} clicked the button. wow`)
|
const publicReply = event.reply(`${name} clicked the button. wow`)
|
||||||
setTimeout(() => publicReply.destroy(), 3000)
|
setTimeout(() => publicReply.destroy(), 3000)
|
||||||
|
|
||||||
const privateReply = event.ephemeralReply("good job, you clicked it")
|
const privateReply = event.reply("good job, you clicked it", {
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
privateReply.deactivate() // we don't need to listen to updates on this
|
privateReply.deactivate() // we don't need to listen to updates on this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export function FruitSelect({ onConfirm }) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const instance = reacord.send(
|
const instance = reacord.createChannelMessage(channel).render(
|
||||||
channelId,
|
|
||||||
<FruitSelect
|
<FruitSelect
|
||||||
onConfirm={(value) => {
|
onConfirm={(value) => {
|
||||||
instance.render(`you chose ${value}`)
|
instance.render(`you chose ${value}`)
|
||||||
@@ -49,7 +48,7 @@ const instance = reacord.send(
|
|||||||
|
|
||||||
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
|
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
|
||||||
|
|
||||||
```tsx
|
```jsx
|
||||||
export function FruitSelect({ onConfirm }) {
|
export function FruitSelect({ onConfirm }) {
|
||||||
const [values, setValues] = useState([])
|
const [values, setValues] = useState([])
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ function SelfDestruct() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reacord.send(channelId, <SelfDestruct />)
|
reacord.createChannelMessage(channel).render(<SelfDestruct />)
|
||||||
```
|
```
|
||||||
|
|||||||
1735
pnpm-lock.yaml
generated
1735
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"github": {
|
|
||||||
"silent": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user