Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 061608323f | |||
| d1611d8f64 | |||
|
|
b641885112 | ||
|
|
2a8ee7885d | ||
|
|
6c71073d10 | ||
|
|
5674e3c1b6 | ||
|
|
a41c825cdd | ||
|
|
a00fbc0631 | ||
|
|
a713f17a5c | ||
|
|
44795cd7cc | ||
|
|
17978a5252 | ||
|
|
95fb342183 | ||
|
|
0772ca4502 | ||
|
|
11153dfe0f | ||
|
|
fb0a997855 | ||
|
|
da1c62f2f0 | ||
|
|
cdc22b7916 | ||
|
|
7fee69c8ae | ||
|
|
c2e5dc04dd | ||
|
|
390da4cab6 | ||
|
|
def0c46f13 | ||
|
|
8b6e283810 | ||
|
|
13fcf7ddc9 | ||
|
|
ce12351a24 | ||
|
|
73bb098774 | ||
|
|
4ee4d4ab91 | ||
|
|
f998a0e09a | ||
|
|
453192cc96 | ||
|
|
d387f669ab | ||
|
|
9aec87ae9f | ||
|
|
65d1d68bb0 | ||
|
|
dfb7562c97 | ||
|
|
9e2be6c2e0 | ||
|
|
d078995516 | ||
|
|
82b3575f2d | ||
|
|
82b811c98b | ||
|
|
3a786310b1 | ||
|
|
ced48a3ecb | ||
|
|
37b75a99e2 | ||
|
|
f2f215d6b9 | ||
|
|
1f67e7c263 | ||
|
|
d4f1bb4d4b | ||
|
|
47c9b75940 | ||
|
|
41c87e3dcc | ||
|
|
b210670b2a | ||
|
|
2b9110bf2c | ||
|
|
5d4dde4e0c | ||
|
|
31baa23076 | ||
|
|
d76f316bb7 | ||
|
|
47b0645a90 | ||
|
|
0bab505994 | ||
|
|
104b175931 | ||
|
|
156cf90919 | ||
|
|
b463ce3cf4 | ||
|
|
576dd2e35e | ||
|
|
0d4294ee8c | ||
|
|
25fcc53d91 | ||
|
|
34bc293df5 | ||
|
|
b7b237f2f5 | ||
|
|
e2c3de4fae | ||
|
|
ffe0a86a33 | ||
|
|
6ce9241080 | ||
|
|
5d96d517df | ||
|
|
2c706f6791 | ||
|
|
2abb61493e | ||
|
|
3db1013b74 | ||
|
|
3e2c6ba5d6 | ||
|
|
0172534d13 | ||
|
|
1a49423beb | ||
|
|
3824859352 | ||
|
|
0dad3c9ecd | ||
|
|
eea1a7ee9d | ||
|
|
e9e5a1617b | ||
|
|
7ac1a9cdce | ||
|
|
33841a0c84 |
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json",
|
"$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json",
|
||||||
"changelog": "@changesets/cli/changelog",
|
"changelog": "@changesets/cli/changelog",
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": []
|
"ignore": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
extends: [require.resolve("@itsmapleleaf/configs/eslint")],
|
|
||||||
ignorePatterns: [
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/.cache/**",
|
|
||||||
"**/build/**",
|
|
||||||
"**/dist/**",
|
|
||||||
"**/coverage/**",
|
|
||||||
"**/public/**",
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
project: require.resolve("./tsconfig.base.json"),
|
|
||||||
extraFileExtensions: [".astro"],
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["packages/website/cypress/**"],
|
|
||||||
parserOptions: {
|
|
||||||
project: require.resolve("./packages/website/cypress/tsconfig.json"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["*.astro"],
|
|
||||||
parser: "astro-eslint-parser",
|
|
||||||
parserOptions: {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"react/no-unknown-property": "off",
|
|
||||||
"react/jsx-key": "off",
|
|
||||||
"react/jsx-no-undef": "off",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
53
.github/workflows/main.yml
vendored
53
.github/workflows/main.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: main
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
env:
|
|
||||||
TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }}
|
|
||||||
TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }}
|
|
||||||
TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run-commands:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
command:
|
|
||||||
# if these run in the same process, it dies,
|
|
||||||
# so we test them separate
|
|
||||||
- name: test reacord
|
|
||||||
run: pnpm -C packages/reacord test
|
|
||||||
# - name: test website
|
|
||||||
# run: pnpm -C packages/website test
|
|
||||||
- name: build
|
|
||||||
run: pnpm --recursive run build
|
|
||||||
- name: lint
|
|
||||||
run: pnpm run lint
|
|
||||||
- name: typecheck
|
|
||||||
run: pnpm --recursive run typecheck
|
|
||||||
name: ${{ matrix.command.name }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: setup pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 7.13.4
|
|
||||||
|
|
||||||
- name: setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: ${{ matrix.command.run }}
|
|
||||||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -13,22 +13,15 @@ jobs:
|
|||||||
name: release
|
name: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v3
|
- uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
- name: setup pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
with:
|
||||||
version: 7.13.4
|
version: 8
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
- name: setup node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
- name: install deps
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: changesets release
|
- name: changesets release
|
||||||
id: changesets
|
id: changesets
|
||||||
|
|||||||
44
.github/workflows/tests.yml
vendored
Normal file
44
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }}
|
||||||
|
TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }}
|
||||||
|
TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: ${{ matrix.script }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
script:
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm run ${{ matrix.script }}
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
if: always()
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,8 +5,7 @@ coverage
|
|||||||
.env
|
.env
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
|
|
||||||
build
|
build
|
||||||
.cache
|
.cache
|
||||||
|
.vercel
|
||||||
.vercel
|
*.tsbuildinfo
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
coverage
|
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
build
|
/packages/website/public/api
|
||||||
.cache
|
.astro
|
||||||
packages/website/public/api
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
const base = require("@itsmapleleaf/configs/prettier")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...base,
|
|
||||||
plugins: [require.resolve("prettier-plugin-astro")],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: "*.astro",
|
|
||||||
options: {
|
|
||||||
parser: "astro",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
30
README.md
30
README.md
@@ -1,42 +1,30 @@
|
|||||||
<center>
|
|
||||||
<img src="./packages/website/app/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
|
|
||||||
</center>
|
|
||||||
|
|
||||||
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
||||||
|
|
||||||
```console
|
```console
|
||||||
# npm
|
# bun
|
||||||
npm install reacord react discord.js
|
bun add reacord react discord.js
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn add reacord react discord.js
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm add reacord react discord.js
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Get Started
|
|
||||||
|
|
||||||
[Visit the docs to get started.](https://reacord.mapleleaf.dev/guides/getting-started)
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
```tsx
|
```tsx
|
||||||
import * as React from "react"
|
import { useState } from "react"
|
||||||
import { Embed, Button } from "reacord"
|
import { Embed, Button } from "reacord"
|
||||||
|
|
||||||
function Counter() {
|
function Counter() {
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Embed title="Counter">
|
<Embed title="Counter">
|
||||||
This button has been clicked {count} times.
|
This button has been clicked {count} times.
|
||||||
</Embed>
|
</Embed>
|
||||||
<Button onClick={() => setCount(count + 1)}>
|
<Button
|
||||||
+1
|
label="+1"
|
||||||
</Button>
|
onClick={() => setCount(count + 1)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
49
package.json
49
package.json
@@ -1,26 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "reacord-monorepo",
|
"name": "reacord-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext js,ts,tsx .",
|
"lint": "run-s --continue-on-error lint:*",
|
||||||
"lint-fix": "pnpm lint -- --fix",
|
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
||||||
"format": "prettier --write .",
|
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
||||||
"build": "pnpm -r run build",
|
"lint:types": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
|
||||||
"start": "pnpm -C packages/website run start",
|
"test": "vitest",
|
||||||
"release": "pnpm -r run build && changeset publish"
|
"build": "bun run --cwd packages/reacord build",
|
||||||
|
"release": "bun run build && changeset publish"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.25.0",
|
"@changesets/cli": "^2.26.2",
|
||||||
"@itsmapleleaf/configs": "^1.1.7",
|
"@itsmapleleaf/configs": "github:itsMapleLeaf/configs",
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"eslint": "^8.51.0",
|
||||||
"@types/eslint": "^8.4.6",
|
"npm-run-all": "^4.1.5",
|
||||||
"astro-eslint-parser": "^0.12.0",
|
"prettier": "^3.0.3",
|
||||||
"eslint": "^8.36.0",
|
"react": "^18.2.0",
|
||||||
"prettier": "^2.7.1",
|
"typescript": "^5.2.2",
|
||||||
"prettier-plugin-astro": "^0.8.0",
|
"vitest": "^0.34.6"
|
||||||
"typescript": "^4.8.4"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"prettier": "@itsmapleleaf/configs/prettier",
|
||||||
"esbuild": "latest"
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"./node_modules/@itsmapleleaf/configs/eslint.config.cjs",
|
||||||
|
"./node_modules/@itsmapleleaf/configs/eslint.config.react.cjs"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/require-await": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
|
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
|
||||||
import type {
|
import type {
|
||||||
CamelCasedPropertiesDeep,
|
CamelCasedPropertiesDeep,
|
||||||
SnakeCasedPropertiesDeep,
|
SnakeCasedPropertiesDeep,
|
||||||
} from "type-fest"
|
} from "type-fest"
|
||||||
import { expect, test } from "vitest"
|
import { expect, test } from "vitest"
|
||||||
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
|
|
||||||
|
|
||||||
test("camelCaseDeep", () => {
|
test("camelCaseDeep", () => {
|
||||||
const input = {
|
const input = {
|
||||||
some_prop: {
|
some_prop: {
|
||||||
some_deep_prop: "some_deep_value",
|
some_deep_prop: "some_deep_value",
|
||||||
},
|
},
|
||||||
someOtherProp: "someOtherValue",
|
someOtherProp: "someOtherValue",
|
||||||
}
|
}
|
||||||
|
|
||||||
const expected: CamelCasedPropertiesDeep<typeof input> = {
|
const expected: CamelCasedPropertiesDeep<typeof input> = {
|
||||||
someProp: {
|
someProp: {
|
||||||
someDeepProp: "some_deep_value",
|
someDeepProp: "some_deep_value",
|
||||||
},
|
},
|
||||||
someOtherProp: "someOtherValue",
|
someOtherProp: "someOtherValue",
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(camelCaseDeep(input)).toEqual(expected)
|
expect(camelCaseDeep(input)).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("snakeCaseDeep", () => {
|
test("snakeCaseDeep", () => {
|
||||||
const input = {
|
const input = {
|
||||||
someProp: {
|
someProp: {
|
||||||
someDeepProp: "someDeepValue",
|
someDeepProp: "someDeepValue",
|
||||||
},
|
},
|
||||||
some_other_prop: "someOtherValue",
|
some_other_prop: "someOtherValue",
|
||||||
}
|
}
|
||||||
|
|
||||||
const expected: SnakeCasedPropertiesDeep<typeof input> = {
|
const expected: SnakeCasedPropertiesDeep<typeof input> = {
|
||||||
some_prop: {
|
some_prop: {
|
||||||
some_deep_prop: "someDeepValue",
|
some_deep_prop: "someDeepValue",
|
||||||
},
|
},
|
||||||
some_other_prop: "someOtherValue",
|
some_other_prop: "someOtherValue",
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(snakeCaseDeep(input)).toEqual(expected)
|
expect(snakeCaseDeep(input)).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
import { camelCase, isObject, snakeCase } from "lodash-es"
|
import { camelCase, isObject, snakeCase } from "lodash-es"
|
||||||
import type {
|
import type {
|
||||||
CamelCasedPropertiesDeep,
|
CamelCasedPropertiesDeep,
|
||||||
SnakeCasedPropertiesDeep,
|
SnakeCasedPropertiesDeep,
|
||||||
|
UnknownRecord,
|
||||||
} from "type-fest"
|
} from "type-fest"
|
||||||
|
|
||||||
function convertKeyCaseDeep<Input, Output>(
|
function convertKeyCaseDeep<Input, Output>(
|
||||||
input: Input,
|
input: Input,
|
||||||
convertKey: (key: string) => string,
|
convertKey: (key: string) => string,
|
||||||
): Output {
|
): Output {
|
||||||
if (!isObject(input)) {
|
if (!isObject(input)) {
|
||||||
return input as unknown as Output
|
return input as unknown as Output
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
return input.map((item) =>
|
return input.map((item) =>
|
||||||
convertKeyCaseDeep(item, convertKey),
|
convertKeyCaseDeep(item, convertKey),
|
||||||
) as unknown as Output
|
) as unknown as Output
|
||||||
}
|
}
|
||||||
|
|
||||||
const output: any = {}
|
const output = {} as UnknownRecord
|
||||||
for (const [key, value] of Object.entries(input)) {
|
for (const [key, value] of Object.entries(input)) {
|
||||||
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
|
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
|
||||||
}
|
}
|
||||||
return output
|
return output as Output
|
||||||
}
|
}
|
||||||
|
|
||||||
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {
|
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {
|
||||||
return convertKeyCaseDeep(input, camelCase)
|
return convertKeyCaseDeep(input, camelCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snakeCaseDeep<T>(input: T): SnakeCasedPropertiesDeep<T> {
|
export function snakeCaseDeep<T>(input: T): SnakeCasedPropertiesDeep<T> {
|
||||||
return convertKeyCaseDeep(input, snakeCase)
|
return convertKeyCaseDeep(input, snakeCase)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { raise } from "./raise.js"
|
import { raise } from "./raise.js"
|
||||||
|
|
||||||
export function getEnvironmentValue(name: string) {
|
export function getEnvironmentValue(name: string) {
|
||||||
return process.env[name] ?? raise(`Missing environment variable: ${name}`)
|
return process.env[name] ?? raise(`Missing environment variable: ${name}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/** For narrowing instance types with array.filter */
|
||||||
* for narrowing instance types with array.filter
|
|
||||||
*/
|
|
||||||
export const isInstanceOf =
|
export const isInstanceOf =
|
||||||
<T>(Constructor: new (...args: any[]) => T) =>
|
<Instance, Args extends unknown[]>(
|
||||||
(value: unknown): value is T =>
|
constructor: new (...args: Args) => Instance,
|
||||||
value instanceof Constructor
|
) =>
|
||||||
|
(value: unknown): value is Instance =>
|
||||||
|
value instanceof constructor
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
export function isObject<T>(
|
export function isObject(value: unknown): value is object {
|
||||||
value: T,
|
return typeof value === "object" && value !== null
|
||||||
): value is Exclude<T, Primitive | AnyFunction> {
|
|
||||||
return typeof value === "object" && value !== null
|
|
||||||
}
|
}
|
||||||
type Primitive = string | number | boolean | undefined | null
|
|
||||||
type AnyFunction = (...args: any[]) => any
|
|
||||||
|
|||||||
7
packages/helpers/json.ts
Normal file
7
packages/helpers/json.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function safeJsonStringify(value: unknown): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
} catch {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export function last<T>(array: T[]): T | undefined {
|
export function last<T>(array: T[]): T | undefined {
|
||||||
return array[array.length - 1]
|
return array[array.length - 1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { inspect } from "node:util"
|
import { inspect } from "node:util"
|
||||||
|
|
||||||
export function logPretty(value: unknown) {
|
export function logPretty(value: unknown) {
|
||||||
console.info(
|
console.info(
|
||||||
inspect(value, {
|
inspect(value, {
|
||||||
// depth: Number.POSITIVE_INFINITY,
|
// depth: Number.POSITIVE_INFINITY,
|
||||||
depth: 10,
|
depth: 10,
|
||||||
colors: true,
|
colors: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/helpers/omit.test.ts
Normal file
7
packages/helpers/omit.test.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { expect, test } from "vitest"
|
||||||
|
import { omit } from "./omit.ts"
|
||||||
|
|
||||||
|
test("omit", () => {
|
||||||
|
const subject = { a: 1, b: true }
|
||||||
|
expect(omit(subject, ["a"])).toStrictEqual({ b: true })
|
||||||
|
})
|
||||||
3
packages/helpers/omit.test.types.ts
Normal file
3
packages/helpers/omit.test.types.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { omit } from "./omit.ts"
|
||||||
|
|
||||||
|
omit({ a: 1, b: true }, ["a"]) satisfies { b: boolean }
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
export function omit<Subject extends object, Key extends PropertyKey>(
|
export function omit<Subject extends object, Key extends PropertyKey>(
|
||||||
subject: Subject,
|
subject: Subject,
|
||||||
keys: Key[],
|
keys: Key[],
|
||||||
// hack: using a conditional type preserves union types
|
) {
|
||||||
): Subject extends any ? Omit<Subject, Key> : never {
|
const keySet = new Set<PropertyKey>(keys)
|
||||||
const result: any = {}
|
return Object.fromEntries(
|
||||||
for (const key in subject) {
|
Object.entries(subject).filter(([key]) => !keySet.has(key)),
|
||||||
if (!keys.includes(key as unknown as Key)) {
|
// hack: conditional type preserves unions
|
||||||
result[key] = subject[key]
|
) as Subject extends unknown ? Omit<Subject, Key> : never
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@reacord/helpers",
|
"name": "@reacord/helpers",
|
||||||
|
"type": "module",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc -b"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"type-fest": "^2.17.0",
|
"type-fest": "^4.4.0",
|
||||||
"vitest": "^0.18.1"
|
"vitest": "^0.34.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import type { LoosePick, UnknownRecord } from "./types"
|
import type { LoosePick } from "./types"
|
||||||
|
|
||||||
export function pick<T, K extends keyof T | PropertyKey>(
|
export function pick<T extends object, K extends keyof T | PropertyKey>(
|
||||||
object: T,
|
object: T,
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): LoosePick<T, K> {
|
) {
|
||||||
const result: any = {}
|
const keySet = new Set<PropertyKey>(keys)
|
||||||
for (const key of keys) {
|
return Object.fromEntries(
|
||||||
const value = (object as UnknownRecord)[key]
|
Object.entries(object).filter(([key]) => keySet.has(key)),
|
||||||
if (value !== undefined) {
|
) as LoosePick<T, K>
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,32 @@ import type { PruneNullishValues } from "./prune-nullish-values"
|
|||||||
import { pruneNullishValues } from "./prune-nullish-values"
|
import { pruneNullishValues } from "./prune-nullish-values"
|
||||||
|
|
||||||
test("pruneNullishValues", () => {
|
test("pruneNullishValues", () => {
|
||||||
type InputType = {
|
interface InputType {
|
||||||
a: string
|
a: string
|
||||||
b: string | null | undefined
|
b: string | null | undefined
|
||||||
c?: string
|
c?: string
|
||||||
d: {
|
d: {
|
||||||
a: string
|
a: string
|
||||||
b: string | undefined
|
b: string | undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const input: InputType = {
|
const input: InputType = {
|
||||||
a: "a",
|
a: "a",
|
||||||
// eslint-disable-next-line unicorn/no-null
|
b: null,
|
||||||
b: null,
|
c: undefined,
|
||||||
c: undefined,
|
d: {
|
||||||
d: {
|
a: "a",
|
||||||
a: "a",
|
b: undefined,
|
||||||
b: undefined,
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const output: PruneNullishValues<InputType> = {
|
const output: PruneNullishValues<InputType> = {
|
||||||
a: "a",
|
a: "a",
|
||||||
d: {
|
d: {
|
||||||
a: "a",
|
a: "a",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(pruneNullishValues(input)).toEqual(output)
|
expect(pruneNullishValues(input)).toEqual(output)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,42 +1,46 @@
|
|||||||
import { isObject } from "./is-object"
|
import { isObject } from "./is-object"
|
||||||
|
|
||||||
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
|
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
|
||||||
if (Array.isArray(input)) {
|
if (!isObject(input)) {
|
||||||
return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any
|
return input as PruneNullishValues<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isObject(input)) {
|
if (Array.isArray(input)) {
|
||||||
return input as any
|
return input
|
||||||
}
|
.filter(Boolean)
|
||||||
|
.map(
|
||||||
|
(item) => pruneNullishValues(item) as unknown,
|
||||||
|
) as PruneNullishValues<T>
|
||||||
|
}
|
||||||
|
|
||||||
const result: any = {}
|
const result: Record<string, unknown> = {}
|
||||||
for (const [key, value] of Object.entries(input as any)) {
|
for (const [key, value] of Object.entries(input)) {
|
||||||
if (value != undefined) {
|
if (value != undefined) {
|
||||||
result[key] = pruneNullishValues(value)
|
result[key] = pruneNullishValues(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result as PruneNullishValues<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PruneNullishValues<Input> = Input extends object
|
export type PruneNullishValues<Input> = Input extends object
|
||||||
? OptionalKeys<
|
? OptionalKeys<
|
||||||
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
|
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
|
||||||
KeysWithNullishValues<Input>
|
KeysWithNullishValues<Input>
|
||||||
>
|
>
|
||||||
: Input
|
: Input
|
||||||
|
|
||||||
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
|
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
|
||||||
[Key in Keys]?: Input[Key]
|
[Key in Keys]?: Input[Key]
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeysWithNullishValues<Input> = NonNullable<
|
type KeysWithNullishValues<Input> = NonNullable<
|
||||||
Values<{
|
Values<{
|
||||||
[Key in keyof Input]: null extends Input[Key]
|
[Key in keyof Input]: null extends Input[Key]
|
||||||
? Key
|
? Key
|
||||||
: undefined extends Input[Key]
|
: undefined extends Input[Key]
|
||||||
? Key
|
? Key
|
||||||
: never
|
: never
|
||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
|
|
||||||
type Values<Input> = Input[keyof Input]
|
type Values<Input> = Input[keyof Input]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { toError } from "./to-error.js"
|
import { toError } from "./to-error.js"
|
||||||
|
|
||||||
export function raise(error: unknown): never {
|
export function raise(error: unknown): never {
|
||||||
throw toError(error)
|
throw toError(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { setTimeout } from "node:timers/promises"
|
|
||||||
import { toError } from "./to-error.js"
|
import { toError } from "./to-error.js"
|
||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
|
||||||
export async function rejectAfter(
|
export async function rejectAfter(
|
||||||
timeMs: number,
|
timeMs: number,
|
||||||
error: unknown = `rejected after ${timeMs}ms`,
|
error: unknown = `rejected after ${timeMs}ms`,
|
||||||
): Promise<never> {
|
): Promise<never> {
|
||||||
await setTimeout(timeMs)
|
await setTimeout(timeMs)
|
||||||
throw toError(error)
|
throw toError(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ const maxTime = 500
|
|||||||
const waitPeriod = 50
|
const waitPeriod = 50
|
||||||
|
|
||||||
export async function retryWithTimeout<T>(
|
export async function retryWithTimeout<T>(
|
||||||
callback: () => Promise<T> | T,
|
callback: () => Promise<T> | T,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
return await callback()
|
return await callback()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (Date.now() - startTime > maxTime) {
|
if (Date.now() - startTime > maxTime) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
await setTimeout(waitPeriod)
|
await setTimeout(waitPeriod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function toError(value: unknown) {
|
export function toError(value: unknown) {
|
||||||
return value instanceof Error ? value : new Error(String(value))
|
return value instanceof Error ? value : new Error(String(value))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** A typesafe version of toUpperCase */
|
/** A typesafe version of toUpperCase */
|
||||||
export function toUpper<S extends string>(string: S) {
|
export function toUpper<S extends string>(string: S) {
|
||||||
return string.toUpperCase() as Uppercase<S>
|
return string.toUpperCase() as Uppercase<S>
|
||||||
}
|
}
|
||||||
|
|||||||
3
packages/helpers/tsconfig.json
Normal file
3
packages/helpers/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json"
|
||||||
|
}
|
||||||
4
packages/helpers/types.test.types.ts
Normal file
4
packages/helpers/types.test.types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { type LooseOmit, type LoosePick, typeEquals } from "./types.ts"
|
||||||
|
|
||||||
|
typeEquals<LoosePick<{ a: 1; b: 2 }, "a">, { a: 1 }>(true)
|
||||||
|
typeEquals<LooseOmit<{ a: 1; b: 2 }, "a">, { b: 2 }>(true)
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
export type MaybePromise<T> = T | Promise<T>
|
import { raise } from "./raise.ts"
|
||||||
|
|
||||||
|
export type MaybePromise<T> = T | PromiseLike<T>
|
||||||
|
|
||||||
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
|
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
|
||||||
? Value
|
? Value
|
||||||
: Type[keyof Type]
|
: Type[keyof Type]
|
||||||
|
|
||||||
export type UnknownRecord = Record<PropertyKey, unknown>
|
export type LoosePick<Shape, Keys extends PropertyKey> = Simplify<{
|
||||||
|
[Key in Extract<Keys, keyof Shape>]: Shape[Key]
|
||||||
|
}>
|
||||||
|
|
||||||
export type LoosePick<Shape, Keys extends PropertyKey> = {
|
export type LooseOmit<Shape, Keys extends PropertyKey> = Simplify<{
|
||||||
[Key in Keys]: Shape extends Record<Key, infer Value> ? Value : never
|
[Key in Exclude<keyof Shape, Keys>]: Shape[Key]
|
||||||
}
|
}>
|
||||||
|
|
||||||
|
export type Simplify<T> = { [Key in keyof T]: T[Key] } & NonNullable<unknown>
|
||||||
|
|
||||||
|
export const typeEquals = <A, B>(
|
||||||
|
_result: A extends B ? (B extends A ? true : false) : false,
|
||||||
|
) => raise("typeEquals() should not be called at runtime")
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { setTimeout } from "node:timers/promises"
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
import type { MaybePromise } from "./types.ts"
|
||||||
|
|
||||||
const maxTime = 1000
|
const maxTime = 1000
|
||||||
|
|
||||||
export async function waitFor<Result>(
|
export async function waitFor<Result>(
|
||||||
predicate: () => Result,
|
predicate: () => MaybePromise<Result>,
|
||||||
): Promise<Awaited<Result>> {
|
): Promise<Awaited<Result>> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
let lastError: unknown
|
let lastError: unknown
|
||||||
|
|
||||||
while (Date.now() - startTime < maxTime) {
|
while (Date.now() - startTime < maxTime) {
|
||||||
try {
|
try {
|
||||||
return await predicate()
|
return await predicate()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error
|
lastError = error
|
||||||
await setTimeout(50)
|
await setTimeout(50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError ?? new Error("Timeout")
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
|
throw lastError ?? new Error("Timeout")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { inspect } from "node:util"
|
import { inspect } from "node:util"
|
||||||
|
|
||||||
export function withLoggedMethodCalls<T extends object>(value: T) {
|
export function withLoggedMethodCalls<T extends object>(value: T) {
|
||||||
return new Proxy(value as Record<string | symbol, unknown>, {
|
return new Proxy(value as Record<string | symbol, unknown>, {
|
||||||
get(target, property) {
|
get(target, property) {
|
||||||
const value = target[property]
|
const value = target[property]
|
||||||
if (typeof value !== "function") {
|
if (typeof value !== "function") {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return (...values: any[]) => {
|
return (...values: unknown[]) => {
|
||||||
console.info(
|
console.info(
|
||||||
`${String(property)}(${values
|
`${String(property)}(${values
|
||||||
.map((value) =>
|
.map((value) =>
|
||||||
typeof value === "object" && value !== null
|
typeof value === "object" && value !== null
|
||||||
? value.constructor.name
|
? value.constructor.name
|
||||||
: inspect(value, { colors: true }),
|
: inspect(value, { colors: true }),
|
||||||
)
|
)
|
||||||
.join(", ")})`,
|
.join(", ")})`,
|
||||||
)
|
)
|
||||||
return value.apply(target, values)
|
return value.apply(target, values) as unknown
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}) as T
|
}) as T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,67 @@
|
|||||||
# reacord
|
# reacord
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 11153df: breaking: more descriptive component event types
|
||||||
|
- fb0a997: add new descriptive adapter methods
|
||||||
|
|
||||||
|
The reacord instance names have been updated, and the old names are now deprecated.
|
||||||
|
|
||||||
|
- `send` -> `createChannelMessage`
|
||||||
|
- `reply` -> `createInteractionReply`
|
||||||
|
|
||||||
|
These new methods also accept discord JS options. Usage example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// can accept either a channel object or a channel ID
|
||||||
|
reacord.createChannelMessage(channel)
|
||||||
|
reacord.createChannelMessage(channel, {
|
||||||
|
tts: true,
|
||||||
|
})
|
||||||
|
reacord.createChannelMessage(channel, {
|
||||||
|
reply: {
|
||||||
|
messageReference: "123456789012345678",
|
||||||
|
failIfNotExists: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
reacord.createInteractionReply(interaction)
|
||||||
|
reacord.createInteractionReply(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
These new methods replace the old ones, which are now deprecated.
|
||||||
|
|
||||||
|
## 0.5.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 104b175: ensure message is edited from arbitrary component updates
|
||||||
|
- 156cf90: fix interaction handling
|
||||||
|
- 0bab505: fix DJS deprecation warning on isStringSelectMenu
|
||||||
|
- d76f316: ensure action rows handle child interactions
|
||||||
|
|
||||||
## 0.5.2
|
## 0.5.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
1
packages/reacord/env.d.ts
vendored
Normal file
1
packages/reacord/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@total-typescript/ts-reset" />
|
||||||
@@ -1,113 +1,113 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import type { ReacordInstance } from "./instance"
|
import type { ReacordInstance } from "./instance"
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEvent {
|
||||||
*/
|
/**
|
||||||
export type ComponentEvent = {
|
* The message associated with this event. For example: with a button click,
|
||||||
/**
|
* this is the message that the button is on.
|
||||||
* The message associated with this event.
|
*
|
||||||
* For example: with a button click,
|
* @see https://discord.com/developers/docs/resources/channel#message-object
|
||||||
* this is the message that the button is on.
|
*/
|
||||||
* @see https://discord.com/developers/docs/resources/channel#message-object
|
message: ComponentEventMessage
|
||||||
*/
|
|
||||||
message: MessageInfo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,
|
||||||
reply(content?: ReactNode): ReacordInstance
|
options?: ComponentEventReplyOptions,
|
||||||
|
): ReacordInstance
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an ephemeral reply to this event,
|
* Create an ephemeral reply to this event, shown only to the user who
|
||||||
* shown only to the user who triggered it.
|
* triggered it.
|
||||||
*/
|
*
|
||||||
ephemeralReply(content?: ReactNode): ReacordInstance
|
* @deprecated Use event.reply(content, { ephemeral: true })
|
||||||
|
*/
|
||||||
|
ephemeralReply(content?: ReactNode): ReacordInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventReplyOptions {
|
||||||
*/
|
ephemeral?: boolean
|
||||||
export type ChannelInfo = {
|
tts?: boolean
|
||||||
id: string
|
|
||||||
name?: string
|
|
||||||
topic?: string
|
|
||||||
nsfw?: boolean
|
|
||||||
lastMessageId?: string
|
|
||||||
ownerId?: string
|
|
||||||
parentId?: string
|
|
||||||
rateLimitPerUser?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventChannel {
|
||||||
*/
|
id: string
|
||||||
export type MessageInfo = {
|
name?: string
|
||||||
id: string
|
topic?: string
|
||||||
channelId: string
|
nsfw?: boolean
|
||||||
authorId: UserInfo
|
lastMessageId?: string
|
||||||
member?: GuildMemberInfo
|
ownerId?: string
|
||||||
content: string
|
parentId?: string
|
||||||
timestamp: string
|
rateLimitPerUser?: number
|
||||||
editedTimestamp?: string
|
|
||||||
tts: boolean
|
|
||||||
mentionEveryone: boolean
|
|
||||||
/** The IDs of mentioned users */
|
|
||||||
mentions: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventMessage {
|
||||||
*/
|
id: string
|
||||||
export type GuildInfo = {
|
channelId: string
|
||||||
id: string
|
authorId: string
|
||||||
name: string
|
member?: ComponentEventGuildMember
|
||||||
member: GuildMemberInfo
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
editedTimestamp?: string
|
||||||
|
tts: boolean
|
||||||
|
mentionEveryone: boolean
|
||||||
|
/** The IDs of mentioned users */
|
||||||
|
mentions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventGuild {
|
||||||
*/
|
id: string
|
||||||
export type GuildMemberInfo = {
|
name: string
|
||||||
id: string
|
member: ComponentEventGuildMember
|
||||||
nick?: string
|
|
||||||
displayName: string
|
|
||||||
avatarUrl?: string
|
|
||||||
displayAvatarUrl: string
|
|
||||||
roles: string[]
|
|
||||||
color: number
|
|
||||||
joinedAt?: string
|
|
||||||
premiumSince?: string
|
|
||||||
pending?: boolean
|
|
||||||
communicationDisabledUntil?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventGuildMember {
|
||||||
*/
|
id: string
|
||||||
export type UserInfo = {
|
nick?: string
|
||||||
id: string
|
displayName: string
|
||||||
username: string
|
avatarUrl?: string
|
||||||
discriminator: string
|
displayAvatarUrl: string
|
||||||
tag: string
|
roles: string[]
|
||||||
avatarUrl: string
|
color: number
|
||||||
accentColor?: number
|
joinedAt?: string
|
||||||
|
premiumSince?: string
|
||||||
|
pending?: boolean
|
||||||
|
communicationDisabledUntil?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @category Component Event */
|
||||||
|
export interface ComponentEventUser {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
discriminator: string
|
||||||
|
tag: string
|
||||||
|
avatarUrl: string | null
|
||||||
|
accentColor?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import type { MessageOptions } from "../../internal/message"
|
import type { MessageOptions } from "../../internal/message"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
|
import type { ComponentInteraction } from "../../internal/interaction.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for an action row
|
* Props for an action row
|
||||||
|
*
|
||||||
* @category Action Row
|
* @category Action Row
|
||||||
*/
|
*/
|
||||||
export type ActionRowProps = {
|
export interface ActionRowProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An action row is a top-level container for message components.
|
* An action row is a top-level container for message components.
|
||||||
*
|
*
|
||||||
* You don't need to use this; Reacord automatically creates action rows for you.
|
* You don't need to use this; Reacord automatically creates action rows for
|
||||||
* But this can be useful if you want a specific layout.
|
* you. But this can be useful if you want a specific layout.
|
||||||
*
|
*
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* // put buttons on two separate rows
|
* // put buttons on two separate rows
|
||||||
@@ -30,18 +31,26 @@ export type ActionRowProps = {
|
|||||||
* @see https://discord.com/developers/docs/interactions/message-components#action-rows
|
* @see https://discord.com/developers/docs/interactions/message-components#action-rows
|
||||||
*/
|
*/
|
||||||
export function ActionRow(props: ActionRowProps) {
|
export function ActionRow(props: ActionRowProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new ActionRowNode(props)}>
|
<ReacordElement props={props} createNode={() => new ActionRowNode(props)}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionRowNode extends Node<{}> {
|
class ActionRowNode extends Node<ActionRowProps> {
|
||||||
override modifyMessageOptions(options: MessageOptions): void {
|
override modifyMessageOptions(options: MessageOptions): void {
|
||||||
options.actionRows.push([])
|
options.actionRows.push([])
|
||||||
for (const child of this.children) {
|
for (const child of this.children) {
|
||||||
child.modifyMessageOptions(options)
|
child.modifyMessageOptions(options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
|
for (const child of this.children) {
|
||||||
|
if (child.handleComponentInteraction(interaction)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,23 @@ import type { ReactNode } from "react"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Common props between button-like components
|
* Common props between button-like components
|
||||||
|
*
|
||||||
* @category Button
|
* @category Button
|
||||||
*/
|
*/
|
||||||
export type ButtonSharedProps = {
|
export interface ButtonSharedProps {
|
||||||
/** The text on the button. Rich formatting (markdown) is not supported here. */
|
/** The text on the button. Rich formatting (markdown) is not supported here. */
|
||||||
label?: ReactNode
|
label?: ReactNode
|
||||||
|
|
||||||
/** When true, the button will be slightly faded, and cannot be clicked. */
|
/** When true, the button will be slightly faded, and cannot be clicked. */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an emoji to the left of the text.
|
* Renders an emoji to the left of the text. Has to be a literal emoji
|
||||||
* Has to be a literal emoji character (e.g. 🍍),
|
* character (e.g. 🍍), or an emoji code, like
|
||||||
* or an emoji code, like `<:plus_one:778531744860602388>`.
|
* `<:plus_one:778531744860602388>`.
|
||||||
*
|
*
|
||||||
* To get an emoji code, type your emoji in Discord chat
|
* To get an emoji code, type your emoji in Discord chat with a backslash `\`
|
||||||
* with a backslash `\` in front.
|
* in front. The bot has to be in the emoji's guild to use it.
|
||||||
* The bot has to be in the emoji's guild to use it.
|
*/
|
||||||
*/
|
emoji?: string
|
||||||
emoji?: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import type { ComponentInteraction } from "../../internal/interaction"
|
import type { ComponentInteraction } from "../../internal/interaction"
|
||||||
import type { MessageOptions } from "../../internal/message"
|
import type { MessageOptions } from "../../internal/message"
|
||||||
@@ -8,70 +7,63 @@ import { Node } from "../../internal/node.js"
|
|||||||
import type { ComponentEvent } from "../component-event"
|
import type { ComponentEvent } from "../component-event"
|
||||||
import type { ButtonSharedProps } from "./button-shared-props"
|
import type { ButtonSharedProps } from "./button-shared-props"
|
||||||
|
|
||||||
/**
|
/** @category Button */
|
||||||
* @category Button
|
|
||||||
*/
|
|
||||||
export type ButtonProps = ButtonSharedProps & {
|
export type ButtonProps = ButtonSharedProps & {
|
||||||
/**
|
/**
|
||||||
* The style determines the color of the button and signals intent.
|
* The style determines the color of the button and signals intent.
|
||||||
* @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles
|
*
|
||||||
*/
|
* @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles
|
||||||
style?: "primary" | "secondary" | "success" | "danger"
|
*/
|
||||||
|
style?: "primary" | "secondary" | "success" | "danger"
|
||||||
|
|
||||||
/**
|
/** Happens when a user clicks the button. */
|
||||||
* Happens when a user clicks the button.
|
onClick: (event: ButtonClickEvent) => void
|
||||||
*/
|
|
||||||
onClick: (event: ButtonClickEvent) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Button */
|
||||||
* @category Button
|
|
||||||
*/
|
|
||||||
export type ButtonClickEvent = ComponentEvent
|
export type ButtonClickEvent = ComponentEvent
|
||||||
|
|
||||||
/**
|
/** @category Button */
|
||||||
* @category Button
|
|
||||||
*/
|
|
||||||
export function Button(props: ButtonProps) {
|
export function Button(props: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new ButtonNode(props)}>
|
<ReacordElement props={props} createNode={() => new ButtonNode(props)}>
|
||||||
<ReacordElement props={{}} createNode={() => new ButtonLabelNode({})}>
|
<ReacordElement props={{}} createNode={() => new ButtonLabelNode({})}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButtonNode extends Node<ButtonProps> {
|
class ButtonNode extends Node<ButtonProps> {
|
||||||
private customId = randomUUID()
|
private customId = randomUUID()
|
||||||
|
|
||||||
// this has text children, but buttons themselves shouldn't yield text
|
// this has text children, but buttons themselves shouldn't yield text
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
||||||
override get text() {
|
override get text() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
override modifyMessageOptions(options: MessageOptions): void {
|
override modifyMessageOptions(options: MessageOptions): void {
|
||||||
getNextActionRow(options).push({
|
getNextActionRow(options).push({
|
||||||
type: "button",
|
type: "button",
|
||||||
customId: this.customId,
|
customId: this.customId,
|
||||||
style: this.props.style ?? "secondary",
|
style: this.props.style ?? "secondary",
|
||||||
disabled: this.props.disabled,
|
disabled: this.props.disabled,
|
||||||
emoji: this.props.emoji,
|
emoji: this.props.emoji,
|
||||||
label: this.children.findType(ButtonLabelNode)?.text,
|
label: this.children.findType(ButtonLabelNode)?.text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override handleComponentInteraction(interaction: ComponentInteraction) {
|
override handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
if (
|
if (
|
||||||
interaction.type === "button" &&
|
interaction.type === "button" &&
|
||||||
interaction.customId === this.customId
|
interaction.customId === this.customId
|
||||||
) {
|
) {
|
||||||
this.props.onClick(interaction.event)
|
this.props.onClick(interaction.event)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButtonLabelNode extends Node<{}> {}
|
class ButtonLabelNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,41 +1,36 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
import { EmbedChildNode } from "./embed-child.js"
|
import { EmbedChildNode } from "./embed-child.js"
|
||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
export interface EmbedAuthorProps {
|
||||||
*/
|
name?: ReactNode
|
||||||
export type EmbedAuthorProps = {
|
children?: ReactNode
|
||||||
name?: ReactNode
|
url?: string
|
||||||
children?: ReactNode
|
iconUrl?: string
|
||||||
url?: string
|
|
||||||
iconUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedAuthor(props: EmbedAuthorProps) {
|
export function EmbedAuthor(props: EmbedAuthorProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
|
<ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
|
||||||
<ReacordElement props={{}} createNode={() => new AuthorTextNode({})}>
|
<ReacordElement props={{}} createNode={() => new AuthorTextNode({})}>
|
||||||
{props.name ?? props.children}
|
{props.name ?? props.children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
|
class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
|
||||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||||
options.author = {
|
options.author = {
|
||||||
name: this.children.findType(AuthorTextNode)?.text ?? "",
|
name: this.children.findType(AuthorTextNode)?.text ?? "",
|
||||||
url: this.props.url,
|
url: this.props.url,
|
||||||
icon_url: this.props.iconUrl,
|
icon_url: this.props.iconUrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorTextNode extends Node<{}> {}
|
class AuthorTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { Node } from "../../internal/node.js"
|
|||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
export abstract class EmbedChildNode<Props> extends Node<Props> {
|
export abstract class EmbedChildNode<Props> extends Node<Props> {
|
||||||
abstract modifyEmbedOptions(options: EmbedOptions): void
|
abstract modifyEmbedOptions(options: EmbedOptions): void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,41 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
import { EmbedChildNode } from "./embed-child.js"
|
import { EmbedChildNode } from "./embed-child.js"
|
||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
export interface EmbedFieldProps {
|
||||||
*/
|
name: ReactNode
|
||||||
export type EmbedFieldProps = {
|
value?: ReactNode
|
||||||
name: ReactNode
|
inline?: boolean
|
||||||
value?: ReactNode
|
children?: ReactNode
|
||||||
inline?: boolean
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedField(props: EmbedFieldProps) {
|
export function EmbedField(props: EmbedFieldProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
|
<ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
|
||||||
<ReacordElement props={{}} createNode={() => new FieldNameNode({})}>
|
<ReacordElement props={{}} createNode={() => new FieldNameNode({})}>
|
||||||
{props.name}
|
{props.name}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
<ReacordElement props={{}} createNode={() => new FieldValueNode({})}>
|
<ReacordElement props={{}} createNode={() => new FieldValueNode({})}>
|
||||||
{props.value || props.children}
|
{props.value ?? props.children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
|
class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
|
||||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||||
options.fields ??= []
|
options.fields ??= []
|
||||||
options.fields.push({
|
options.fields.push({
|
||||||
name: this.children.findType(FieldNameNode)?.text ?? "",
|
name: this.children.findType(FieldNameNode)?.text ?? "",
|
||||||
value: this.children.findType(FieldValueNode)?.text ?? "",
|
value: this.children.findType(FieldValueNode)?.text ?? "",
|
||||||
inline: this.props.inline,
|
inline: this.props.inline,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FieldNameNode extends Node<{}> {}
|
class FieldNameNode extends Node<Record<string, never>> {}
|
||||||
class FieldValueNode extends Node<{}> {}
|
class FieldValueNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,45 +1,40 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
import { EmbedChildNode } from "./embed-child.js"
|
import { EmbedChildNode } from "./embed-child.js"
|
||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
export interface EmbedFooterProps {
|
||||||
*/
|
text?: ReactNode
|
||||||
export type EmbedFooterProps = {
|
children?: ReactNode
|
||||||
text?: ReactNode
|
iconUrl?: string
|
||||||
children?: ReactNode
|
timestamp?: string | number | Date
|
||||||
iconUrl?: string
|
|
||||||
timestamp?: string | number | Date
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) {
|
export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
|
<ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
|
||||||
<ReacordElement props={{}} createNode={() => new FooterTextNode({})}>
|
<ReacordElement props={{}} createNode={() => new FooterTextNode({})}>
|
||||||
{text ?? children}
|
{text ?? children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedFooterNode extends EmbedChildNode<
|
class EmbedFooterNode extends EmbedChildNode<
|
||||||
Omit<EmbedFooterProps, "text" | "children">
|
Omit<EmbedFooterProps, "text" | "children">
|
||||||
> {
|
> {
|
||||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||||
options.footer = {
|
options.footer = {
|
||||||
text: this.children.findType(FooterTextNode)?.text ?? "",
|
text: this.children.findType(FooterTextNode)?.text ?? "",
|
||||||
icon_url: this.props.iconUrl,
|
icon_url: this.props.iconUrl,
|
||||||
}
|
}
|
||||||
options.timestamp = this.props.timestamp
|
options.timestamp = this.props.timestamp
|
||||||
? new Date(this.props.timestamp).toISOString()
|
? new Date(this.props.timestamp).toISOString()
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FooterTextNode extends Node<{}> {}
|
class FooterTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import { EmbedChildNode } from "./embed-child.js"
|
import { EmbedChildNode } from "./embed-child.js"
|
||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
export interface EmbedImageProps {
|
||||||
*/
|
url: string
|
||||||
export type EmbedImageProps = {
|
|
||||||
url: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedImage(props: EmbedImageProps) {
|
export function EmbedImage(props: EmbedImageProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement
|
<ReacordElement
|
||||||
props={props}
|
props={props}
|
||||||
createNode={() => new EmbedImageNode(props)}
|
createNode={() => new EmbedImageNode(props)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedImageNode extends EmbedChildNode<EmbedImageProps> {
|
class EmbedImageNode extends EmbedChildNode<EmbedImageProps> {
|
||||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||||
options.image = { url: this.props.url }
|
options.image = { url: this.props.url }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
|
|
||||||
import type { EmbedProps } from "./embed"
|
import type { EmbedProps } from "./embed"
|
||||||
|
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
|
||||||
|
|
||||||
export type EmbedOptions = SnakeCasedPropertiesDeep<
|
export type EmbedOptions = SnakeCasedPropertiesDeep<
|
||||||
Except<EmbedProps, "timestamp" | "children"> & {
|
Except<EmbedProps, "timestamp" | "children"> & {
|
||||||
timestamp?: string
|
timestamp?: string
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import { EmbedChildNode } from "./embed-child.js"
|
import { EmbedChildNode } from "./embed-child.js"
|
||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
export interface EmbedThumbnailProps {
|
||||||
*/
|
url: string
|
||||||
export type EmbedThumbnailProps = {
|
|
||||||
url: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedThumbnail(props: EmbedThumbnailProps) {
|
export function EmbedThumbnail(props: EmbedThumbnailProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement
|
<ReacordElement
|
||||||
props={props}
|
props={props}
|
||||||
createNode={() => new EmbedThumbnailNode(props)}
|
createNode={() => new EmbedThumbnailNode(props)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedThumbnailNode extends EmbedChildNode<EmbedThumbnailProps> {
|
class EmbedThumbnailNode extends EmbedChildNode<EmbedThumbnailProps> {
|
||||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||||
options.thumbnail = { url: this.props.url }
|
options.thumbnail = { url: this.props.url }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,31 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
import { EmbedChildNode } from "./embed-child.js"
|
import { EmbedChildNode } from "./embed-child.js"
|
||||||
import type { EmbedOptions } from "./embed-options"
|
import type { EmbedOptions } from "./embed-options"
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
export interface EmbedTitleProps {
|
||||||
*/
|
children: ReactNode
|
||||||
export type EmbedTitleProps = {
|
url?: string
|
||||||
children: ReactNode
|
|
||||||
url?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedTitle({ children, ...props }: EmbedTitleProps) {
|
export function EmbedTitle({ children, ...props }: EmbedTitleProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
|
<ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
|
||||||
<ReacordElement props={{}} createNode={() => new TitleTextNode({})}>
|
<ReacordElement props={{}} createNode={() => new TitleTextNode({})}>
|
||||||
{children}
|
{children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
|
class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
|
||||||
override modifyEmbedOptions(options: EmbedOptions): void {
|
override modifyEmbedOptions(options: EmbedOptions): void {
|
||||||
options.title = this.children.findType(TitleTextNode)?.text ?? ""
|
options.title = this.children.findType(TitleTextNode)?.text ?? ""
|
||||||
options.url = this.props.url
|
options.url = this.props.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TitleTextNode extends Node<{}> {}
|
class TitleTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { snakeCaseDeep } from "@reacord/helpers/convert-object-property-case"
|
import { snakeCaseDeep } from "@reacord/helpers/convert-object-property-case"
|
||||||
import { omit } from "@reacord/helpers/omit"
|
import { omit } from "@reacord/helpers/omit"
|
||||||
import React from "react"
|
import type React from "react"
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import type { MessageOptions } from "../../internal/message"
|
import type { MessageOptions } from "../../internal/message"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
@@ -12,19 +12,19 @@ import type { EmbedOptions } from "./embed-options"
|
|||||||
* @category Embed
|
* @category Embed
|
||||||
* @see https://discord.com/developers/docs/resources/channel#embed-object
|
* @see https://discord.com/developers/docs/resources/channel#embed-object
|
||||||
*/
|
*/
|
||||||
export type EmbedProps = {
|
export interface EmbedProps {
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
url?: string
|
url?: string
|
||||||
color?: number
|
color?: number
|
||||||
fields?: Array<{ name: string; value: string; inline?: boolean }>
|
fields?: Array<{ name: string; value: string; inline?: boolean }>
|
||||||
author?: { name: string; url?: string; iconUrl?: string }
|
author?: { name: string; url?: string; iconUrl?: string }
|
||||||
thumbnail?: { url: string }
|
thumbnail?: { url: string }
|
||||||
image?: { url: string }
|
image?: { url: string }
|
||||||
video?: { url: string }
|
video?: { url: string }
|
||||||
footer?: { text: string; iconUrl?: string }
|
footer?: { text: string; iconUrl?: string }
|
||||||
timestamp?: string | number | Date
|
timestamp?: string | number | Date
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,31 +32,31 @@ export type EmbedProps = {
|
|||||||
* @see https://discord.com/developers/docs/resources/channel#embed-object
|
* @see https://discord.com/developers/docs/resources/channel#embed-object
|
||||||
*/
|
*/
|
||||||
export function Embed(props: EmbedProps) {
|
export function Embed(props: EmbedProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new EmbedNode(props)}>
|
<ReacordElement props={props} createNode={() => new EmbedNode(props)}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmbedNode extends Node<EmbedProps> {
|
class EmbedNode extends Node<EmbedProps> {
|
||||||
override modifyMessageOptions(options: MessageOptions): void {
|
override modifyMessageOptions(options: MessageOptions): void {
|
||||||
const embed: EmbedOptions = {
|
const embed: EmbedOptions = {
|
||||||
...snakeCaseDeep(omit(this.props, ["children", "timestamp"])),
|
...snakeCaseDeep(omit(this.props, ["children", "timestamp"])),
|
||||||
timestamp: this.props.timestamp
|
timestamp: this.props.timestamp
|
||||||
? new Date(this.props.timestamp).toISOString()
|
? new Date(this.props.timestamp).toISOString()
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of this.children) {
|
for (const child of this.children) {
|
||||||
if (child instanceof EmbedChildNode) {
|
if (child instanceof EmbedChildNode) {
|
||||||
child.modifyEmbedOptions(embed)
|
child.modifyEmbedOptions(embed)
|
||||||
}
|
}
|
||||||
if (child instanceof TextNode) {
|
if (child instanceof TextNode) {
|
||||||
embed.description = (embed.description || "") + child.props
|
embed.description = (embed.description ?? "") + child.props
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options.embeds.push(embed)
|
options.embeds.push(embed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import type { MessageOptions } from "../../internal/message"
|
import type { MessageOptions } from "../../internal/message"
|
||||||
import { getNextActionRow } from "../../internal/message"
|
import { getNextActionRow } from "../../internal/message"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
import type { ButtonSharedProps } from "./button-shared-props"
|
import type { ButtonSharedProps } from "./button-shared-props"
|
||||||
|
|
||||||
/**
|
/** @category Link */
|
||||||
* @category Link
|
|
||||||
*/
|
|
||||||
export type LinkProps = ButtonSharedProps & {
|
export type LinkProps = ButtonSharedProps & {
|
||||||
/** The URL the link should lead to */
|
/** The URL the link should lead to */
|
||||||
url: string
|
url: string
|
||||||
/** The link text */
|
/** The link text */
|
||||||
children?: string
|
children?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Link */
|
||||||
* @category Link
|
|
||||||
*/
|
|
||||||
export function Link({ label, children, ...props }: LinkProps) {
|
export function Link({ label, children, ...props }: LinkProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new LinkNode(props)}>
|
<ReacordElement props={props} createNode={() => new LinkNode(props)}>
|
||||||
<ReacordElement props={{}} createNode={() => new LinkTextNode({})}>
|
<ReacordElement props={{}} createNode={() => new LinkTextNode({})}>
|
||||||
{label || children}
|
{label ?? children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
|
class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
|
||||||
override modifyMessageOptions(options: MessageOptions): void {
|
override modifyMessageOptions(options: MessageOptions): void {
|
||||||
getNextActionRow(options).push({
|
getNextActionRow(options).push({
|
||||||
type: "link",
|
type: "link",
|
||||||
disabled: this.props.disabled,
|
disabled: this.props.disabled,
|
||||||
emoji: this.props.emoji,
|
emoji: this.props.emoji,
|
||||||
label: this.children.findType(LinkTextNode)?.text,
|
label: this.children.findType(LinkTextNode)?.text,
|
||||||
url: this.props.url,
|
url: this.props.url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkTextNode extends Node<{}> {}
|
class LinkTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { Node } from "../../internal/node"
|
|||||||
import type { OptionProps } from "./option"
|
import type { OptionProps } from "./option"
|
||||||
|
|
||||||
export class OptionNode extends Node<
|
export class OptionNode extends Node<
|
||||||
Omit<OptionProps, "children" | "label" | "description">
|
Omit<OptionProps, "children" | "label" | "description">
|
||||||
> {
|
> {
|
||||||
get options(): MessageSelectOptionOptions {
|
get options(): MessageSelectOptionOptions {
|
||||||
return {
|
return {
|
||||||
label: this.children.findType(OptionLabelNode)?.text ?? this.props.value,
|
label: this.children.findType(OptionLabelNode)?.text ?? this.props.value,
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
description: this.children.findType(OptionDescriptionNode)?.text,
|
description: this.children.findType(OptionDescriptionNode)?.text,
|
||||||
emoji: this.props.emoji,
|
emoji: this.props.emoji,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OptionLabelNode extends Node<{}> {}
|
export class OptionLabelNode extends Node<Record<string, never>> {}
|
||||||
export class OptionDescriptionNode extends Node<{}> {}
|
export class OptionDescriptionNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,62 +1,56 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element"
|
import { ReacordElement } from "../../internal/element"
|
||||||
import {
|
import {
|
||||||
OptionDescriptionNode,
|
OptionDescriptionNode,
|
||||||
OptionLabelNode,
|
OptionLabelNode,
|
||||||
OptionNode,
|
OptionNode,
|
||||||
} from "./option-node"
|
} from "./option-node"
|
||||||
|
|
||||||
/**
|
/** @category Select */
|
||||||
* @category Select
|
export interface OptionProps {
|
||||||
*/
|
/** The internal value of this option */
|
||||||
export type OptionProps = {
|
value: string
|
||||||
/** The internal value of this option */
|
/** The text shown to the user. This takes priority over `children` */
|
||||||
value: string
|
label?: ReactNode
|
||||||
/** The text shown to the user. This takes priority over `children` */
|
/** The text shown to the user */
|
||||||
label?: ReactNode
|
children?: ReactNode
|
||||||
/** The text shown to the user */
|
/** Description for the option, shown to the user */
|
||||||
children?: ReactNode
|
description?: ReactNode
|
||||||
/** Description for the option, shown to the user */
|
|
||||||
description?: ReactNode
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an emoji to the left of the text.
|
* Renders an emoji to the left of the text.
|
||||||
*
|
*
|
||||||
* Has to be a literal emoji character (e.g. 🍍),
|
* Has to be a literal emoji character (e.g. 🍍), or an emoji code, like
|
||||||
* or an emoji code, like `<:plus_one:778531744860602388>`.
|
* `<:plus_one:778531744860602388>`.
|
||||||
*
|
*
|
||||||
* To get an emoji code, type your emoji in Discord chat
|
* To get an emoji code, type your emoji in Discord chat with a backslash `\`
|
||||||
* with a backslash `\` in front.
|
* in front. The bot has to be in the emoji's guild to use it.
|
||||||
* The bot has to be in the emoji's guild to use it.
|
*/
|
||||||
*/
|
emoji?: string
|
||||||
emoji?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Select */
|
||||||
* @category Select
|
|
||||||
*/
|
|
||||||
export function Option({
|
export function Option({
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
description,
|
description,
|
||||||
...props
|
...props
|
||||||
}: OptionProps) {
|
}: OptionProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new OptionNode(props)}>
|
<ReacordElement props={props} createNode={() => new OptionNode(props)}>
|
||||||
{(label !== undefined || children !== undefined) && (
|
{(label !== undefined || children !== undefined) && (
|
||||||
<ReacordElement props={{}} createNode={() => new OptionLabelNode({})}>
|
<ReacordElement props={{}} createNode={() => new OptionLabelNode({})}>
|
||||||
{label || children}
|
{label ?? children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)}
|
)}
|
||||||
{description !== undefined && (
|
{description !== undefined && (
|
||||||
<ReacordElement
|
<ReacordElement
|
||||||
props={{}}
|
props={{}}
|
||||||
createNode={() => new OptionDescriptionNode({})}
|
createNode={() => new OptionDescriptionNode({})}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)}
|
)}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,153 +1,173 @@
|
|||||||
import { isInstanceOf } from "@reacord/helpers/is-instance-of"
|
import { isInstanceOf } from "@reacord/helpers/is-instance-of"
|
||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { ReacordElement } from "../../internal/element.js"
|
import { ReacordElement } from "../../internal/element.js"
|
||||||
import type { ComponentInteraction } from "../../internal/interaction"
|
import type { ComponentInteraction } from "../../internal/interaction"
|
||||||
import type {
|
import type {
|
||||||
ActionRow,
|
ActionRow,
|
||||||
ActionRowItem,
|
ActionRowItem,
|
||||||
MessageOptions,
|
MessageOptions,
|
||||||
} from "../../internal/message"
|
} from "../../internal/message"
|
||||||
import { Node } from "../../internal/node.js"
|
import { Node } from "../../internal/node.js"
|
||||||
import type { ComponentEvent } from "../component-event"
|
import type { ComponentEvent } from "../component-event"
|
||||||
import { OptionNode } from "./option-node"
|
import { OptionNode } from "./option-node"
|
||||||
|
import { omit } from "@reacord/helpers/omit.js"
|
||||||
|
|
||||||
/**
|
export type SelectMenuType =
|
||||||
* @category Select
|
| "string"
|
||||||
*/
|
| "user"
|
||||||
export type SelectProps = {
|
| "role"
|
||||||
children?: ReactNode
|
| "mentionable"
|
||||||
/** Sets the currently selected value */
|
| "channel"
|
||||||
value?: string
|
|
||||||
|
|
||||||
/** Sets the currently selected values, for use with `multiple` */
|
/** @category Select */
|
||||||
values?: string[]
|
export interface SelectProps {
|
||||||
|
children?: ReactNode
|
||||||
|
/** Sets the currently selected value */
|
||||||
|
value?: string
|
||||||
|
|
||||||
/** The text shown when no value is selected */
|
/** Sets the currently selected values, for use with `multiple` */
|
||||||
placeholder?: string
|
values?: string[]
|
||||||
|
|
||||||
/** Set to true to allow multiple selected values */
|
/** The text shown when no value is selected */
|
||||||
multiple?: boolean
|
placeholder?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* With `multiple`, the minimum number of values that can be selected.
|
* The kind of select menu to render.
|
||||||
* When `multiple` is false or not defined, this is always 1.
|
*
|
||||||
*
|
* Defaults to `string`.
|
||||||
* This only limits the number of values that can be received by the user.
|
*/
|
||||||
* This does not limit the number of values that can be displayed by you.
|
menuType?: SelectMenuType
|
||||||
*/
|
|
||||||
minValues?: number
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* With `multiple`, the maximum number of values that can be selected.
|
* Limit the channel types shown in a channel select menu.
|
||||||
* When `multiple` is false or not defined, this is always 1.
|
*
|
||||||
*
|
* This is only used when `menuType` is `channel`.
|
||||||
* This only limits the number of values that can be received by the user.
|
*/
|
||||||
* This does not limit the number of values that can be displayed by you.
|
channelTypes?: number[]
|
||||||
*/
|
|
||||||
maxValues?: number
|
|
||||||
|
|
||||||
/** When true, the select will be slightly faded, and cannot be interacted with. */
|
/** Set to true to allow multiple selected values */
|
||||||
disabled?: boolean
|
multiple?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user inputs a selection.
|
* With `multiple`, the minimum number of values that can be selected. When
|
||||||
* Receives the entire select change event,
|
* `multiple` is false or not defined, this is always 1.
|
||||||
* which can be used to create new replies, etc.
|
*
|
||||||
*/
|
* This only limits the number of values that can be received by the user.
|
||||||
onChange?: (event: SelectChangeEvent) => void
|
* This does not limit the number of values that can be displayed by you.
|
||||||
|
*/
|
||||||
|
minValues?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience shorthand for `onChange`, which receives the first selected value.
|
* With `multiple`, the maximum number of values that can be selected. When
|
||||||
*/
|
* `multiple` is false or not defined, this is always 1.
|
||||||
onChangeValue?: (value: string, event: SelectChangeEvent) => void
|
*
|
||||||
|
* This only limits the number of values that can be received by the user.
|
||||||
|
* This does not limit the number of values that can be displayed by you.
|
||||||
|
*/
|
||||||
|
maxValues?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience shorthand for `onChange`, which receives all selected values.
|
* When true, the select will be slightly faded, and cannot be interacted
|
||||||
*/
|
* with.
|
||||||
onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void
|
*/
|
||||||
|
disabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user inputs a selection. Receives the entire select change
|
||||||
|
* event, which can be used to create new replies, etc.
|
||||||
|
*/
|
||||||
|
onChange?: (event: SelectChangeEvent) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience shorthand for `onChange`, which receives the first selected
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
onChangeValue?: (value: string, event: SelectChangeEvent) => void
|
||||||
|
|
||||||
|
/** Convenience shorthand for `onChange`, which receives all selected values. */
|
||||||
|
onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Select */
|
||||||
* @category Select
|
|
||||||
*/
|
|
||||||
export type SelectChangeEvent = ComponentEvent & {
|
export type SelectChangeEvent = ComponentEvent & {
|
||||||
values: string[]
|
values: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See [the select menu guide](/guides/select-menu) for a usage example.
|
* See [the select menu guide](/guides/select-menu) for a usage example.
|
||||||
|
*
|
||||||
* @category Select
|
* @category Select
|
||||||
*/
|
*/
|
||||||
export function Select(props: SelectProps) {
|
export function Select(props: SelectProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement props={props} createNode={() => new SelectNode(props)}>
|
<ReacordElement props={props} createNode={() => new SelectNode(props)}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ReacordElement>
|
</ReacordElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SelectNode extends Node<SelectProps> {
|
class SelectNode extends Node<SelectProps> {
|
||||||
readonly customId = randomUUID()
|
readonly customId = randomUUID()
|
||||||
|
|
||||||
override modifyMessageOptions(message: MessageOptions): void {
|
override modifyMessageOptions(message: MessageOptions): void {
|
||||||
const actionRow: ActionRow = []
|
const actionRow: ActionRow = []
|
||||||
message.actionRows.push(actionRow)
|
message.actionRows.push(actionRow)
|
||||||
|
|
||||||
const options = [...this.children]
|
const options = [...this.children]
|
||||||
.filter(isInstanceOf(OptionNode))
|
.filter(isInstanceOf(OptionNode))
|
||||||
.map((node) => node.options)
|
.map((node) => node.options)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
multiple,
|
multiple,
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
minValues = 0,
|
minValues = 0,
|
||||||
maxValues = 25,
|
maxValues = 25,
|
||||||
children,
|
...props
|
||||||
onChange,
|
} = omit(this.props, [
|
||||||
onChangeValue,
|
"children",
|
||||||
onChangeMultiple,
|
"onChange",
|
||||||
...props
|
"onChangeValue",
|
||||||
} = this.props
|
"onChangeMultiple",
|
||||||
|
])
|
||||||
|
|
||||||
const item: ActionRowItem = {
|
const item: ActionRowItem = {
|
||||||
...props,
|
...props,
|
||||||
type: "select",
|
type: "select",
|
||||||
customId: this.customId,
|
customId: this.customId,
|
||||||
options,
|
options,
|
||||||
values: [],
|
values: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
item.minValues = minValues
|
item.minValues = minValues
|
||||||
item.maxValues = maxValues
|
item.maxValues = maxValues
|
||||||
if (values) item.values = values
|
if (values) item.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!multiple && value != undefined) {
|
if (!multiple && value != undefined) {
|
||||||
item.values = [value]
|
item.values = [value]
|
||||||
}
|
}
|
||||||
|
|
||||||
actionRow.push(item)
|
actionRow.push(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override handleComponentInteraction(
|
override handleComponentInteraction(
|
||||||
interaction: ComponentInteraction,
|
interaction: ComponentInteraction,
|
||||||
): boolean {
|
): boolean {
|
||||||
const isSelectInteraction =
|
const isSelectInteraction =
|
||||||
interaction.type === "select" &&
|
interaction.type === "select" &&
|
||||||
interaction.customId === this.customId &&
|
interaction.customId === this.customId &&
|
||||||
!this.props.disabled
|
!this.props.disabled
|
||||||
|
|
||||||
if (!isSelectInteraction) return false
|
if (!isSelectInteraction) return false
|
||||||
|
|
||||||
this.props.onChange?.(interaction.event)
|
this.props.onChange?.(interaction.event)
|
||||||
this.props.onChangeMultiple?.(interaction.event.values, interaction.event)
|
this.props.onChangeMultiple?.(interaction.event.values, interaction.event)
|
||||||
if (interaction.event.values[0]) {
|
if (interaction.event.values[0]) {
|
||||||
this.props.onChangeValue?.(interaction.event.values[0], interaction.event)
|
this.props.onChangeValue?.(interaction.event.values[0], interaction.event)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { raise } from "@reacord/helpers/raise"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import type { ReacordInstance } from "./instance"
|
import type { MessageStore } from "../internal/message-store.js"
|
||||||
|
|
||||||
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||||
|
const MessageContext = React.createContext<MessageStore | undefined>(undefined)
|
||||||
|
|
||||||
export const InstanceProvider = Context.Provider
|
export const InstanceProvider = Context.Provider
|
||||||
|
export const MessageProvider = MessageContext.Provider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the associated instance for the current component.
|
* Get the associated instance for the current component.
|
||||||
@@ -13,8 +16,32 @@ export const InstanceProvider = Context.Provider
|
|||||||
* @see https://reacord.mapleleaf.dev/guides/use-instance
|
* @see https://reacord.mapleleaf.dev/guides/use-instance
|
||||||
*/
|
*/
|
||||||
export function useInstance(): ReacordInstance {
|
export function useInstance(): ReacordInstance {
|
||||||
return (
|
return (
|
||||||
React.useContext(Context) ??
|
React.useContext(Context) ??
|
||||||
raise("Could not find instance, was this component rendered via Reacord?")
|
raise("Could not find instance, was this component rendered via Reacord?")
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message that the current component is rendered into.
|
||||||
|
*
|
||||||
|
* @category Core
|
||||||
|
*/
|
||||||
|
export function useMessage() {
|
||||||
|
const store =
|
||||||
|
React.useContext(MessageContext) ??
|
||||||
|
raise("Could not find message store, was this component rendered via Reacord?")
|
||||||
|
|
||||||
|
const getSnapshot = React.useCallback(() => store.getSnapshot(), [store])
|
||||||
|
|
||||||
|
if (React.useSyncExternalStore) {
|
||||||
|
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState(getSnapshot)
|
||||||
|
React.useEffect(() => store.subscribe(() => setValue(getSnapshot())), [
|
||||||
|
store,
|
||||||
|
getSnapshot,
|
||||||
|
])
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import type { ReactNode } from "react"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an interactive message, which can later be replaced or deleted.
|
* Represents an interactive message, which can later be replaced or deleted.
|
||||||
|
*
|
||||||
* @category Core
|
* @category Core
|
||||||
*/
|
*/
|
||||||
export type 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as destroy, but keeps the message and disables the components on it.
|
* Same as destroy, but keeps the message and disables the components on it.
|
||||||
* This prevents it from listening to user interactions.
|
* This prevents it from listening to user interactions.
|
||||||
*/
|
*/
|
||||||
deactivate: () => void
|
deactivate: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
import { safeJsonStringify } from "@reacord/helpers/json"
|
||||||
import { pick } from "@reacord/helpers/pick"
|
import { pick } from "@reacord/helpers/pick"
|
||||||
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
|
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
|
||||||
import { raise } from "@reacord/helpers/raise"
|
import { raise } from "@reacord/helpers/raise"
|
||||||
@@ -7,18 +7,19 @@ import type { ReactNode } from "react"
|
|||||||
import type { Except } from "type-fest"
|
import type { Except } from "type-fest"
|
||||||
import type { ComponentInteraction } from "../internal/interaction"
|
import type { ComponentInteraction } from "../internal/interaction"
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
MessageButtonOptions,
|
MessageButtonOptions,
|
||||||
MessageOptions,
|
MessageOptions,
|
||||||
} from "../internal/message"
|
} from "../internal/message"
|
||||||
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"
|
||||||
@@ -26,364 +27,450 @@ import { Reacord } from "./reacord"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The Reacord adapter for Discord.js.
|
* The Reacord adapter for Discord.js.
|
||||||
|
*
|
||||||
* @category Core
|
* @category Core
|
||||||
*/
|
*/
|
||||||
export class ReacordDiscordJs extends Reacord {
|
export class ReacordDiscordJs extends Reacord {
|
||||||
constructor(private client: Discord.Client, config: ReacordConfig = {}) {
|
constructor(
|
||||||
super(config)
|
private client: Discord.Client,
|
||||||
|
config: ReacordConfig = {},
|
||||||
|
) {
|
||||||
|
super(config)
|
||||||
|
|
||||||
client.on("interactionCreate", (interaction) => {
|
client.on("interactionCreate", (interaction) => {
|
||||||
if (interaction.isButton() || interaction.isSelectMenu()) {
|
if (interaction.isButton() || interaction.isAnySelectMenu()) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
this.createReacordComponentInteraction(interaction),
|
this.createReacordComponentInteraction(interaction),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to a channel.
|
* Sends a message to a channel.
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
*
|
||||||
*/
|
* @param target Discord channel object.
|
||||||
override send(
|
* @param [options] Options for the channel message
|
||||||
channelId: string,
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
initialContent?: React.ReactNode,
|
* @see {@link Discord.MessageCreateOptions}
|
||||||
): ReacordInstance {
|
*/
|
||||||
return this.createInstance(
|
public createChannelMessage(
|
||||||
this.createChannelRenderer(channelId),
|
target: Discord.ChannelResolvable,
|
||||||
initialContent,
|
options: Discord.MessageCreateOptions = {},
|
||||||
)
|
): ReacordInstance {
|
||||||
}
|
return this.createInstance(
|
||||||
|
this.createChannelMessageRenderer(target, options),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message as a reply to a command interaction.
|
* Replies to a command interaction by sending a message.
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
*
|
||||||
*/
|
* @param interaction Discord command interaction object.
|
||||||
override reply(
|
* @param [options] Custom options for the interaction reply method.
|
||||||
interaction: Discord.CommandInteraction,
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
initialContent?: React.ReactNode,
|
* @see {@link Discord.InteractionReplyOptions}
|
||||||
): ReacordInstance {
|
*/
|
||||||
return this.createInstance(
|
public createInteractionReply(
|
||||||
this.createInteractionReplyRenderer(interaction),
|
interaction: Discord.CommandInteraction,
|
||||||
initialContent,
|
options: Discord.InteractionReplyOptions = {},
|
||||||
)
|
): ReacordInstance {
|
||||||
}
|
return this.createInstance(
|
||||||
|
this.createInteractionReplyRenderer(interaction, options),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an ephemeral message as a reply to a command interaction.
|
* Sends a message to a channel.
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
*
|
||||||
*/
|
* @deprecated Use reacord.createChannelMessage() instead.
|
||||||
override ephemeralReply(
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
interaction: Discord.CommandInteraction,
|
*/
|
||||||
initialContent?: React.ReactNode,
|
public send(
|
||||||
): ReacordInstance {
|
channel: Discord.ChannelResolvable,
|
||||||
return this.createInstance(
|
initialContent?: React.ReactNode,
|
||||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
): ReacordInstance {
|
||||||
initialContent,
|
return this.createInstance(
|
||||||
)
|
this.createChannelMessageRenderer(channel, {}),
|
||||||
}
|
initialContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private createChannelRenderer(channelId: string) {
|
/**
|
||||||
return new ChannelMessageRenderer({
|
* Sends a message as a reply to a command interaction.
|
||||||
send: async (options) => {
|
*
|
||||||
const channel =
|
* @deprecated Use reacord.createInteractionReply() instead.
|
||||||
this.client.channels.cache.get(channelId) ??
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
(await this.client.channels.fetch(channelId)) ??
|
*/
|
||||||
raise(`Channel ${channelId} not found`)
|
public reply(
|
||||||
|
interaction: Discord.CommandInteraction,
|
||||||
|
initialContent?: React.ReactNode,
|
||||||
|
): ReacordInstance {
|
||||||
|
return this.createInstance(
|
||||||
|
this.createInteractionReplyRenderer(interaction, {}),
|
||||||
|
initialContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!channel.isTextBased()) {
|
/**
|
||||||
raise(`Channel ${channelId} is not a text channel`)
|
* 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
|
||||||
|
*/
|
||||||
|
public ephemeralReply(
|
||||||
|
interaction: Discord.CommandInteraction,
|
||||||
|
initialContent?: React.ReactNode,
|
||||||
|
): ReacordInstance {
|
||||||
|
return this.createInstance(
|
||||||
|
this.createInteractionReplyRenderer(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
|
initialContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const message = await channel.send(getDiscordMessageOptions(options))
|
private createChannelMessageRenderer(
|
||||||
return createReacordMessage(message)
|
channelResolvable: Discord.ChannelResolvable,
|
||||||
},
|
messageCreateOptions?: Discord.MessageCreateOptions,
|
||||||
})
|
) {
|
||||||
}
|
return new ChannelMessageRenderer({
|
||||||
|
send: async (messageOptions) => {
|
||||||
|
let channel = this.client.channels.resolve(channelResolvable)
|
||||||
|
if (!channel && typeof channelResolvable === "string") {
|
||||||
|
channel = await this.client.channels.fetch(channelResolvable)
|
||||||
|
}
|
||||||
|
|
||||||
private createInteractionReplyRenderer(
|
if (!channel) {
|
||||||
interaction:
|
const id =
|
||||||
| Discord.CommandInteraction
|
typeof channelResolvable === "string"
|
||||||
| Discord.MessageComponentInteraction,
|
? channelResolvable
|
||||||
) {
|
: channelResolvable.id
|
||||||
return new InteractionReplyRenderer({
|
raise(`Channel ${id} not found`)
|
||||||
type: "command",
|
}
|
||||||
id: interaction.id,
|
|
||||||
reply: async (options) => {
|
|
||||||
const message = await interaction.reply({
|
|
||||||
...getDiscordMessageOptions(options),
|
|
||||||
fetchReply: true,
|
|
||||||
})
|
|
||||||
return createReacordMessage(message as Discord.Message)
|
|
||||||
},
|
|
||||||
followUp: async (options) => {
|
|
||||||
const message = await interaction.followUp({
|
|
||||||
...getDiscordMessageOptions(options),
|
|
||||||
fetchReply: true,
|
|
||||||
})
|
|
||||||
return createReacordMessage(message as Discord.Message)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private createEphemeralInteractionReplyRenderer(
|
if (!channel.isTextBased()) {
|
||||||
interaction:
|
raise(`Channel ${channel.id} must be a text channel`)
|
||||||
| 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(
|
const textChannel = channel as Discord.TextBasedChannel &
|
||||||
interaction: Discord.MessageComponentInteraction,
|
Discord.PartialTextBasedChannelFields
|
||||||
): ComponentInteraction {
|
const message = await textChannel.send({
|
||||||
// todo please dear god clean this up
|
...getDiscordMessageOptions(messageOptions),
|
||||||
const channel: ChannelInfo = interaction.channel
|
...messageCreateOptions,
|
||||||
? {
|
})
|
||||||
...pruneNullishValues(
|
return createReacordMessage(message)
|
||||||
pick(interaction.channel, [
|
},
|
||||||
"topic",
|
})
|
||||||
"nsfw",
|
}
|
||||||
"lastMessageId",
|
|
||||||
"ownerId",
|
|
||||||
"parentId",
|
|
||||||
"rateLimitPerUser",
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
id: interaction.channelId,
|
|
||||||
}
|
|
||||||
: raise("Non-channel interactions are not supported")
|
|
||||||
|
|
||||||
const message: MessageInfo =
|
private createInteractionReplyRenderer(
|
||||||
interaction.message instanceof Discord.Message
|
interaction:
|
||||||
? {
|
| Discord.CommandInteraction
|
||||||
...pick(interaction.message, [
|
| Discord.MessageComponentInteraction,
|
||||||
"id",
|
interactionReplyOptions: Discord.InteractionReplyOptions,
|
||||||
"channelId",
|
) {
|
||||||
"authorId",
|
return new InteractionReplyRenderer({
|
||||||
"content",
|
interactionId: interaction.id,
|
||||||
"tts",
|
reply: async (messageOptions) => {
|
||||||
"mentionEveryone",
|
const message = await interaction.reply({
|
||||||
]),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
timestamp: new Date(
|
...interactionReplyOptions,
|
||||||
interaction.message.createdTimestamp,
|
fetchReply: true,
|
||||||
).toISOString(),
|
})
|
||||||
editedTimestamp: interaction.message.editedTimestamp
|
return createReacordMessage(message)
|
||||||
? new Date(interaction.message.editedTimestamp).toISOString()
|
},
|
||||||
: undefined,
|
followUp: async (messageOptions) => {
|
||||||
mentions: interaction.message.mentions.users.map((u) => u.id),
|
const message = await interaction.followUp({
|
||||||
}
|
...getDiscordMessageOptions(messageOptions),
|
||||||
: raise("Message not found")
|
...interactionReplyOptions,
|
||||||
|
fetchReply: true,
|
||||||
|
})
|
||||||
|
return createReacordMessage(message)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const member: GuildMemberInfo | undefined =
|
private createReacordComponentInteraction(
|
||||||
interaction.member instanceof Discord.GuildMember
|
interaction: Discord.MessageComponentInteraction,
|
||||||
? {
|
): ComponentInteraction {
|
||||||
...pruneNullishValues(
|
// todo please dear god clean this up
|
||||||
pick(interaction.member, [
|
const channel: ComponentEventChannel = interaction.channel
|
||||||
"id",
|
? {
|
||||||
"nick",
|
...pruneNullishValues(
|
||||||
"displayName",
|
pick(interaction.channel, [
|
||||||
"avatarUrl",
|
"topic",
|
||||||
"displayAvatarUrl",
|
"nsfw",
|
||||||
"color",
|
"lastMessageId",
|
||||||
"pending",
|
"ownerId",
|
||||||
]),
|
"parentId",
|
||||||
),
|
"rateLimitPerUser",
|
||||||
displayName: interaction.member.displayName,
|
]),
|
||||||
roles: interaction.member.roles.cache.map((role) => role.id),
|
),
|
||||||
joinedAt: interaction.member.joinedAt?.toISOString(),
|
id: interaction.channelId,
|
||||||
premiumSince: interaction.member.premiumSince?.toISOString(),
|
}
|
||||||
communicationDisabledUntil:
|
: raise("Non-channel interactions are not supported")
|
||||||
interaction.member.communicationDisabledUntil?.toISOString(),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const guild: GuildInfo | undefined = interaction.guild
|
const message: ComponentEventMessage =
|
||||||
? {
|
interaction.message instanceof Discord.Message
|
||||||
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
? createComponentEventMessage(interaction.message)
|
||||||
member: member ?? raise("unexpected: member is undefined"),
|
: raise("Message not found")
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const user: UserInfo = {
|
const member: ComponentEventGuildMember | undefined =
|
||||||
...pruneNullishValues(
|
interaction.member instanceof Discord.GuildMember
|
||||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
? {
|
||||||
),
|
...pruneNullishValues(
|
||||||
avatarUrl: interaction.user.avatarURL()!,
|
pick(interaction.member, ["nick", "avatarUrl", "pending"]),
|
||||||
accentColor: interaction.user.accentColor ?? undefined,
|
),
|
||||||
}
|
id: interaction.member.id,
|
||||||
|
displayName: interaction.member.displayName,
|
||||||
|
roles: interaction.member.roles.cache.map((role) => role.id),
|
||||||
|
joinedAt: interaction.member.joinedAt?.toISOString(),
|
||||||
|
premiumSince: interaction.member.premiumSince?.toISOString(),
|
||||||
|
communicationDisabledUntil:
|
||||||
|
interaction.member.communicationDisabledUntil?.toISOString(),
|
||||||
|
color: interaction.member.displayColor,
|
||||||
|
displayAvatarUrl: interaction.member.displayAvatarURL(),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
const baseProps: Except<ComponentInteraction, "type"> = {
|
const guild: ComponentEventGuild | undefined = interaction.guild
|
||||||
id: interaction.id,
|
? {
|
||||||
customId: interaction.customId,
|
id: interaction.guild.id,
|
||||||
update: async (options: MessageOptions) => {
|
name: interaction.guild.name,
|
||||||
await interaction.update(getDiscordMessageOptions(options))
|
member: member ?? raise("unexpected: member is undefined"),
|
||||||
},
|
}
|
||||||
deferUpdate: async () => {
|
: undefined
|
||||||
if (interaction.replied || interaction.deferred) return
|
|
||||||
await interaction.deferUpdate()
|
|
||||||
},
|
|
||||||
reply: async (options) => {
|
|
||||||
const message = await interaction.reply({
|
|
||||||
...getDiscordMessageOptions(options),
|
|
||||||
fetchReply: true,
|
|
||||||
})
|
|
||||||
return createReacordMessage(message as Discord.Message)
|
|
||||||
},
|
|
||||||
followUp: async (options) => {
|
|
||||||
const message = await interaction.followUp({
|
|
||||||
...getDiscordMessageOptions(options),
|
|
||||||
fetchReply: true,
|
|
||||||
})
|
|
||||||
return createReacordMessage(message as Discord.Message)
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
channel,
|
|
||||||
message,
|
|
||||||
user,
|
|
||||||
guild,
|
|
||||||
|
|
||||||
reply: (content?: ReactNode) =>
|
const user: ComponentEventUser = {
|
||||||
this.createInstance(
|
id: interaction.user.id,
|
||||||
this.createInteractionReplyRenderer(interaction),
|
username: interaction.user.username,
|
||||||
content,
|
discriminator: interaction.user.discriminator,
|
||||||
),
|
tag: interaction.user.tag,
|
||||||
|
avatarUrl: interaction.user.avatarURL(),
|
||||||
|
accentColor: interaction.user.accentColor ?? undefined,
|
||||||
|
}
|
||||||
|
|
||||||
ephemeralReply: (content: ReactNode) =>
|
const baseProps: Except<ComponentInteraction, "type"> = {
|
||||||
this.createInstance(
|
id: interaction.id,
|
||||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
customId: interaction.customId,
|
||||||
content,
|
update: async (options: MessageOptions) => {
|
||||||
),
|
if (interaction.deferred || interaction.replied) {
|
||||||
},
|
await interaction.message.edit(getDiscordMessageOptions(options))
|
||||||
}
|
} else {
|
||||||
|
await interaction.update(getDiscordMessageOptions(options))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deferUpdate: async () => {
|
||||||
|
if (interaction.replied || interaction.deferred) return
|
||||||
|
await interaction.deferUpdate()
|
||||||
|
},
|
||||||
|
reply: async (options) => {
|
||||||
|
const message = await interaction.reply({
|
||||||
|
...getDiscordMessageOptions(options),
|
||||||
|
fetchReply: true,
|
||||||
|
})
|
||||||
|
return createReacordMessage(message)
|
||||||
|
},
|
||||||
|
followUp: async (options) => {
|
||||||
|
const message = await interaction.followUp({
|
||||||
|
...getDiscordMessageOptions(options),
|
||||||
|
fetchReply: true,
|
||||||
|
})
|
||||||
|
return createReacordMessage(message)
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
channel,
|
||||||
|
message,
|
||||||
|
user,
|
||||||
|
guild,
|
||||||
|
|
||||||
if (interaction.isButton()) {
|
reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
|
||||||
return {
|
this.createInstance(
|
||||||
...baseProps,
|
this.createInteractionReplyRenderer(interaction, options ?? {}),
|
||||||
type: "button",
|
content,
|
||||||
}
|
),
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.isSelectMenu()) {
|
/** @deprecated Use event.reply(content, { ephemeral: true }) */
|
||||||
return {
|
ephemeralReply: (content: ReactNode) =>
|
||||||
...baseProps,
|
this.createInstance(
|
||||||
type: "select",
|
this.createInteractionReplyRenderer(interaction, {
|
||||||
event: {
|
ephemeral: true,
|
||||||
...baseProps.event,
|
}),
|
||||||
values: interaction.values,
|
content,
|
||||||
},
|
),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
raise(`Unsupported component interaction type: ${interaction.type}`)
|
if (interaction.isButton()) {
|
||||||
}
|
return {
|
||||||
|
...baseProps,
|
||||||
|
type: "button",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.isAnySelectMenu()) {
|
||||||
|
return {
|
||||||
|
...baseProps,
|
||||||
|
type: "select",
|
||||||
|
event: {
|
||||||
|
...baseProps.event,
|
||||||
|
values: interaction.values,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raise(`Unsupported component interaction type: ${interaction.type}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReacordMessage(message: Discord.Message): Message {
|
function createReacordMessage(message: Discord.Message): Message {
|
||||||
return {
|
return {
|
||||||
edit: async (options) => {
|
data: createComponentEventMessage(message),
|
||||||
await message.edit(getDiscordMessageOptions(options))
|
edit: async (options) => {
|
||||||
},
|
await message.edit(getDiscordMessageOptions(options))
|
||||||
delete: async () => {
|
},
|
||||||
await message.delete()
|
delete: async () => {
|
||||||
},
|
await message.delete()
|
||||||
}
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEphemeralReacordMessage(): Message {
|
function createComponentEventMessage(
|
||||||
return {
|
message: Discord.Message,
|
||||||
edit: () => {
|
): ComponentEventMessage {
|
||||||
console.warn("Ephemeral messages can't be edited")
|
return {
|
||||||
return Promise.resolve()
|
...pick(message, [
|
||||||
},
|
"id",
|
||||||
delete: () => {
|
"channelId",
|
||||||
console.warn("Ephemeral messages can't be deleted")
|
"authorId",
|
||||||
return Promise.resolve()
|
"content",
|
||||||
},
|
"tts",
|
||||||
}
|
"mentionEveryone",
|
||||||
|
]),
|
||||||
|
timestamp: new Date(message.createdTimestamp).toISOString(),
|
||||||
|
editedTimestamp: message.editedTimestamp
|
||||||
|
? new Date(message.editedTimestamp).toISOString()
|
||||||
|
: undefined,
|
||||||
|
mentions: message.mentions.users.map((u) => u.id),
|
||||||
|
authorId: message.author.id,
|
||||||
|
mentionEveryone: message.mentions.everyone,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
||||||
const styleMap = {
|
const styleMap = {
|
||||||
primary: Discord.ButtonStyle.Primary,
|
primary: Discord.ButtonStyle.Primary,
|
||||||
secondary: Discord.ButtonStyle.Secondary,
|
secondary: Discord.ButtonStyle.Secondary,
|
||||||
success: Discord.ButtonStyle.Success,
|
success: Discord.ButtonStyle.Success,
|
||||||
danger: Discord.ButtonStyle.Danger,
|
danger: Discord.ButtonStyle.Danger,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
return styleMap[style ?? "secondary"]
|
return styleMap[style ?? "secondary"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this could be a part of the core library,
|
// TODO: this could be a part of the core library,
|
||||||
// and also handle some edge cases, e.g. empty messages
|
// and also handle some edge cases, e.g. empty messages
|
||||||
function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
||||||
const options = {
|
const options = {
|
||||||
// eslint-disable-next-line unicorn/no-null
|
content: reacordOptions.content || undefined,
|
||||||
content: reacordOptions.content || null,
|
embeds: reacordOptions.embeds,
|
||||||
embeds: reacordOptions.embeds,
|
components: reacordOptions.actionRows.map((row) => ({
|
||||||
components: reacordOptions.actionRows.map((row) => ({
|
type: Discord.ComponentType.ActionRow,
|
||||||
type: Discord.ComponentType.ActionRow,
|
components: row.map(
|
||||||
components: row.map(
|
(component): Discord.MessageActionRowComponentData => {
|
||||||
(component): Discord.MessageActionRowComponentData => {
|
if (component.type === "button") {
|
||||||
if (component.type === "button") {
|
return {
|
||||||
return {
|
type: Discord.ComponentType.Button,
|
||||||
type: Discord.ComponentType.Button,
|
customId: component.customId,
|
||||||
customId: component.customId,
|
label: component.label ?? "",
|
||||||
label: component.label ?? "",
|
style: convertButtonStyleToEnum(component.style),
|
||||||
style: convertButtonStyleToEnum(component.style),
|
disabled: component.disabled,
|
||||||
disabled: component.disabled,
|
emoji: component.emoji,
|
||||||
emoji: component.emoji,
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (component.type === "link") {
|
if (component.type === "link") {
|
||||||
return {
|
return {
|
||||||
type: Discord.ComponentType.Button,
|
type: Discord.ComponentType.Button,
|
||||||
url: component.url,
|
url: component.url,
|
||||||
label: component.label ?? "",
|
label: component.label ?? "",
|
||||||
style: Discord.ButtonStyle.Link,
|
style: Discord.ButtonStyle.Link,
|
||||||
disabled: component.disabled,
|
disabled: component.disabled,
|
||||||
emoji: component.emoji,
|
emoji: component.emoji,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component.type === "select") {
|
// future proofing
|
||||||
return {
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
...component,
|
if (component.type === "select") {
|
||||||
type: Discord.ComponentType.SelectMenu,
|
const {
|
||||||
options: component.options.map((option) => ({
|
menuType,
|
||||||
...option,
|
values,
|
||||||
default: component.values?.includes(option.value),
|
options: selectOptions,
|
||||||
})),
|
channelTypes,
|
||||||
}
|
multiple,
|
||||||
}
|
...rest
|
||||||
|
} = component
|
||||||
|
|
||||||
raise(`Unsupported component type: ${(component as any).type}`)
|
if (menuType === "string" || menuType == undefined) {
|
||||||
},
|
return {
|
||||||
),
|
...rest,
|
||||||
})),
|
type: Discord.ComponentType.StringSelect,
|
||||||
}
|
options: selectOptions.map((option) => ({
|
||||||
|
...option,
|
||||||
|
default: values?.includes(option.value),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.content && !options.embeds?.length) {
|
if (menuType === "user") {
|
||||||
options.content = "_ _"
|
return {
|
||||||
}
|
...rest,
|
||||||
|
type: Discord.ComponentType.UserSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return options
|
if (menuType === "role") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.RoleSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "mentionable") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.MentionableSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "channel") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.ChannelSelect,
|
||||||
|
channelTypes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raise(
|
||||||
|
`Unsupported select menu type: ${menuType ?? "string"}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
component satisfies never
|
||||||
|
throw new Error(
|
||||||
|
`Invalid component type ${safeJsonStringify(component)}}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.content && !options.embeds.length) {
|
||||||
|
options.content = "_ _"
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,86 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
import type { ComponentInteraction } from "../internal/interaction.js"
|
||||||
import type { ComponentInteraction } from "../internal/interaction"
|
|
||||||
import { reconciler } from "../internal/reconciler.js"
|
import { reconciler } from "../internal/reconciler.js"
|
||||||
import type { Renderer } from "../internal/renderers/renderer"
|
import type { Renderer } from "../internal/renderers/renderer.js"
|
||||||
import type { ReacordInstance } from "./instance"
|
import { InstanceProvider, MessageProvider } from "./instance-context.js"
|
||||||
import { InstanceProvider } from "./instance-context"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
|
||||||
/**
|
/** @category Core */
|
||||||
* @category Core
|
export interface ReacordConfig {
|
||||||
*/
|
/**
|
||||||
export type ReacordConfig = {
|
* The max number of active instances. When this limit is exceeded, the oldest
|
||||||
/**
|
* instances will be disabled.
|
||||||
* The max number of active instances.
|
*/
|
||||||
* When this limit is exceeded, the oldest instances will be disabled.
|
maxInstances?: number
|
||||||
*/
|
|
||||||
maxInstances?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main Reacord class that other Reacord adapters should extend.
|
* The main Reacord class that other Reacord adapters should extend. Only use
|
||||||
* Only use this directly if you're making [a custom adapter](/guides/custom-adapters).
|
* this directly if you're making [a custom adapter](/guides/custom-adapters).
|
||||||
*/
|
*/
|
||||||
export abstract class Reacord {
|
export abstract class Reacord {
|
||||||
private renderers: Renderer[] = []
|
private renderers: Renderer[] = []
|
||||||
|
|
||||||
constructor(private readonly config: ReacordConfig = {}) {}
|
constructor(private readonly config: ReacordConfig = {}) {}
|
||||||
|
|
||||||
abstract send(...args: unknown[]): ReacordInstance
|
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
abstract reply(...args: unknown[]): ReacordInstance
|
for (const renderer of this.renderers) {
|
||||||
abstract ephemeralReply(...args: unknown[]): ReacordInstance
|
if (renderer.handleComponentInteraction(interaction)) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
private get maxInstances() {
|
||||||
for (const renderer of this.renderers) {
|
return this.config.maxInstances ?? 50
|
||||||
if (renderer.handleComponentInteraction(interaction)) return
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get maxInstances() {
|
protected createInstance(renderer: Renderer, initialContent?: ReactNode) {
|
||||||
return this.config.maxInstances ?? 50
|
if (this.renderers.length > this.maxInstances && this.renderers[0]) {
|
||||||
}
|
this.deactivate(this.renderers[0])
|
||||||
|
}
|
||||||
|
|
||||||
protected createInstance(renderer: Renderer, initialContent?: ReactNode) {
|
this.renderers.push(renderer)
|
||||||
if (this.renderers.length > this.maxInstances) {
|
|
||||||
this.deactivate(this.renderers[0]!)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderers.push(renderer)
|
const container: unknown = reconciler.createContainer(
|
||||||
|
renderer,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"reacord",
|
||||||
|
() => {},
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
const container = reconciler.createContainer(
|
const instance: ReacordInstance = {
|
||||||
renderer,
|
render: (content: ReactNode) => {
|
||||||
0,
|
reconciler.updateContainer(
|
||||||
// eslint-disable-next-line unicorn/no-null
|
<InstanceProvider value={instance}>
|
||||||
null,
|
<MessageProvider value={renderer.messageStore}>
|
||||||
false,
|
{content}
|
||||||
// eslint-disable-next-line unicorn/no-null
|
</MessageProvider>
|
||||||
null,
|
</InstanceProvider>,
|
||||||
"reacord",
|
container,
|
||||||
() => {},
|
)
|
||||||
// eslint-disable-next-line unicorn/no-null
|
return instance
|
||||||
null,
|
},
|
||||||
)
|
deactivate: () => {
|
||||||
|
this.deactivate(renderer)
|
||||||
|
},
|
||||||
|
destroy: () => {
|
||||||
|
this.renderers = this.renderers.filter((it) => it !== renderer)
|
||||||
|
renderer.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const instance: ReacordInstance = {
|
if (initialContent !== undefined) {
|
||||||
render: (content: ReactNode) => {
|
instance.render(initialContent)
|
||||||
reconciler.updateContainer(
|
}
|
||||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
|
||||||
container,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
deactivate: () => {
|
|
||||||
this.deactivate(renderer)
|
|
||||||
},
|
|
||||||
destroy: () => {
|
|
||||||
this.renderers = this.renderers.filter((it) => it !== renderer)
|
|
||||||
renderer.destroy()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialContent !== undefined) {
|
return instance
|
||||||
instance.render(initialContent)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return instance
|
private deactivate(renderer: Renderer) {
|
||||||
}
|
this.renderers = this.renderers.filter((it) => it !== renderer)
|
||||||
|
renderer.deactivate()
|
||||||
private deactivate(renderer: Renderer) {
|
}
|
||||||
this.renderers = this.renderers.filter((it) => it !== renderer)
|
|
||||||
renderer.deactivate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Message, MessageOptions } from "./message"
|
import type { Message, MessageOptions } from "./message"
|
||||||
|
|
||||||
export type Channel = {
|
export interface Channel {
|
||||||
send(message: MessageOptions): Promise<Message>
|
send(message: MessageOptions): Promise<Message>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,39 @@
|
|||||||
export class Container<T> {
|
export class Container<T> {
|
||||||
private items: T[] = []
|
private items: T[] = []
|
||||||
|
|
||||||
add(...items: T[]) {
|
add(...items: T[]) {
|
||||||
this.items.push(...items)
|
this.items.push(...items)
|
||||||
}
|
}
|
||||||
|
|
||||||
addBefore(item: T, before: T) {
|
addBefore(item: T, before: T) {
|
||||||
let index = this.items.indexOf(before)
|
let index = this.items.indexOf(before)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = this.items.length
|
index = this.items.length
|
||||||
}
|
}
|
||||||
this.items.splice(index, 0, item)
|
this.items.splice(index, 0, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(toRemove: T) {
|
remove(toRemove: T) {
|
||||||
this.items = this.items.filter((item) => item !== toRemove)
|
this.items = this.items.filter((item) => item !== toRemove)
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.items = []
|
this.items = []
|
||||||
}
|
}
|
||||||
|
|
||||||
find(predicate: (item: T) => boolean): T | undefined {
|
find(predicate: (item: T) => boolean): T | undefined {
|
||||||
return this.items.find(predicate)
|
return this.items.find(predicate)
|
||||||
}
|
}
|
||||||
|
|
||||||
findType<U extends T>(type: new (...args: any[]) => U): U | undefined {
|
findType<U extends T>(
|
||||||
for (const item of this.items) {
|
type: new (...args: Array<NonNullable<unknown>>) => U,
|
||||||
if (item instanceof type) return item
|
): U | undefined {
|
||||||
}
|
for (const item of this.items) {
|
||||||
}
|
if (item instanceof type) return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return this.items[Symbol.iterator]()
|
return this.items[Symbol.iterator]()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import type { Node } from "./node"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import type { Node } from "./node"
|
|
||||||
|
|
||||||
export function ReacordElement<Props>(props: {
|
export function ReacordElement<Props>(props: {
|
||||||
props: Props
|
props: Props
|
||||||
createNode: () => Node<Props>
|
createNode: () => Node<Props>
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return React.createElement("reacord-element", props)
|
return React.createElement("reacord-element", props)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,28 @@ export type ComponentInteraction = ButtonInteraction | SelectInteraction
|
|||||||
export type CommandInteraction = BaseInteraction<"command">
|
export type CommandInteraction = BaseInteraction<"command">
|
||||||
|
|
||||||
export type ButtonInteraction = BaseComponentInteraction<
|
export type ButtonInteraction = BaseComponentInteraction<
|
||||||
"button",
|
"button",
|
||||||
ButtonClickEvent
|
ButtonClickEvent
|
||||||
>
|
>
|
||||||
|
|
||||||
export type SelectInteraction = BaseComponentInteraction<
|
export type SelectInteraction = BaseComponentInteraction<
|
||||||
"select",
|
"select",
|
||||||
SelectChangeEvent
|
SelectChangeEvent
|
||||||
>
|
>
|
||||||
|
|
||||||
export type BaseInteraction<Type extends string> = {
|
export interface BaseInteraction<Type extends string> {
|
||||||
type: Type
|
type: Type
|
||||||
id: string
|
id: string
|
||||||
reply(messageOptions: MessageOptions): Promise<Message>
|
reply(messageOptions: MessageOptions): Promise<Message>
|
||||||
followUp(messageOptions: MessageOptions): Promise<Message>
|
followUp(messageOptions: MessageOptions): Promise<Message>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BaseComponentInteraction<
|
export type BaseComponentInteraction<
|
||||||
Type extends string,
|
Type extends string,
|
||||||
Event extends ComponentEvent,
|
Event extends ComponentEvent,
|
||||||
> = BaseInteraction<Type> & {
|
> = BaseInteraction<Type> & {
|
||||||
event: Event
|
event: Event
|
||||||
customId: string
|
customId: string
|
||||||
update(options: MessageOptions): Promise<void>
|
update(options: MessageOptions): Promise<void>
|
||||||
deferUpdate(): Promise<void>
|
deferUpdate(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
export class LimitedCollection<T> {
|
export class LimitedCollection<T> {
|
||||||
private items: T[] = []
|
private items: T[] = []
|
||||||
|
|
||||||
constructor(private readonly size: number) {}
|
constructor(private readonly size: number) {}
|
||||||
|
|
||||||
add(item: T) {
|
add(item: T) {
|
||||||
if (this.items.length >= this.size) {
|
if (this.items.length >= this.size) {
|
||||||
this.items.shift()
|
this.items.shift()
|
||||||
}
|
}
|
||||||
this.items.push(item)
|
this.items.push(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
has(item: T) {
|
has(item: T) {
|
||||||
return this.items.includes(item)
|
return this.items.includes(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
values(): readonly T[] {
|
values(): readonly T[] {
|
||||||
return this.items
|
return this.items
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return this.items[Symbol.iterator]()
|
return this.items[Symbol.iterator]()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
packages/reacord/library/internal/message-store.ts
Normal file
22
packages/reacord/library/internal/message-store.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ComponentEventMessage } from "../core/component-event"
|
||||||
|
|
||||||
|
export class MessageStore {
|
||||||
|
private value: ComponentEventMessage | undefined
|
||||||
|
private listeners = new Set<() => void>()
|
||||||
|
|
||||||
|
getSnapshot = () => this.value
|
||||||
|
|
||||||
|
subscribe = (listener: () => void) => {
|
||||||
|
this.listeners.add(listener)
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value: ComponentEventMessage | undefined) {
|
||||||
|
this.value = value
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +1,67 @@
|
|||||||
import { last } from "@reacord/helpers/last"
|
import type { ComponentEventMessage } from "../core/component-event"
|
||||||
import type { Except } from "type-fest"
|
|
||||||
import type { EmbedOptions } from "../core/components/embed-options"
|
import type { EmbedOptions } from "../core/components/embed-options"
|
||||||
import type { SelectProps } from "../core/components/select"
|
import type { SelectProps } from "../core/components/select"
|
||||||
|
import { last } from "@reacord/helpers/last"
|
||||||
|
import type { Except } from "type-fest"
|
||||||
|
|
||||||
export type MessageOptions = {
|
export interface MessageOptions {
|
||||||
content: string
|
content: string
|
||||||
embeds: EmbedOptions[]
|
embeds: EmbedOptions[]
|
||||||
actionRows: ActionRow[]
|
actionRows: ActionRow[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActionRow = ActionRowItem[]
|
export type ActionRow = ActionRowItem[]
|
||||||
|
|
||||||
export type ActionRowItem =
|
export type ActionRowItem =
|
||||||
| MessageButtonOptions
|
| MessageButtonOptions
|
||||||
| MessageLinkOptions
|
| MessageLinkOptions
|
||||||
| MessageSelectOptions
|
| MessageSelectOptions
|
||||||
|
|
||||||
export type MessageButtonOptions = {
|
export interface MessageButtonOptions {
|
||||||
type: "button"
|
type: "button"
|
||||||
customId: string
|
customId: string
|
||||||
label?: string
|
label?: string
|
||||||
style?: "primary" | "secondary" | "success" | "danger"
|
style?: "primary" | "secondary" | "success" | "danger"
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
emoji?: string
|
emoji?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageLinkOptions = {
|
export interface MessageLinkOptions {
|
||||||
type: "link"
|
type: "link"
|
||||||
url: string
|
url: string
|
||||||
label?: string
|
label?: string
|
||||||
emoji?: string
|
emoji?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageSelectOptions = Except<SelectProps, "children" | "value"> & {
|
export type MessageSelectOptions = Except<SelectProps, "children" | "value"> & {
|
||||||
type: "select"
|
type: "select"
|
||||||
customId: string
|
customId: string
|
||||||
options: MessageSelectOptionOptions[]
|
options: MessageSelectOptionOptions[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageSelectOptionOptions = {
|
export interface MessageSelectOptionOptions {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
description?: string
|
description?: string
|
||||||
emoji?: string
|
emoji?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message = {
|
export interface Message {
|
||||||
edit(options: MessageOptions): Promise<void>
|
data?: ComponentEventMessage
|
||||||
delete(): Promise<void>
|
edit(options: MessageOptions): Promise<void>
|
||||||
|
delete(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNextActionRow(options: MessageOptions): ActionRow {
|
export function getNextActionRow(options: MessageOptions): ActionRow {
|
||||||
let actionRow = last(options.actionRows)
|
let actionRow = last(options.actionRows)
|
||||||
if (
|
if (
|
||||||
actionRow == undefined ||
|
actionRow == undefined ||
|
||||||
actionRow.length >= 5 ||
|
actionRow.length >= 5 ||
|
||||||
actionRow[0]?.type === "select"
|
actionRow[0]?.type === "select"
|
||||||
) {
|
) {
|
||||||
actionRow = []
|
actionRow = []
|
||||||
options.actionRows.push(actionRow)
|
options.actionRows.push(actionRow)
|
||||||
}
|
}
|
||||||
return actionRow
|
return actionRow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
import { Container } from "./container.js"
|
import { Container } from "./container.js"
|
||||||
import type { ComponentInteraction } from "./interaction"
|
import type { ComponentInteraction } from "./interaction"
|
||||||
import type { MessageOptions } from "./message"
|
import type { MessageOptions } from "./message"
|
||||||
|
|
||||||
export abstract class Node<Props> {
|
export abstract class Node<Props> {
|
||||||
readonly children = new Container<Node<unknown>>()
|
readonly children = new Container<Node<unknown>>()
|
||||||
|
|
||||||
constructor(public props: Props) {}
|
constructor(public props: Props) {}
|
||||||
|
|
||||||
modifyMessageOptions(options: MessageOptions) {}
|
modifyMessageOptions(_options: MessageOptions) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
handleComponentInteraction(interaction: ComponentInteraction): boolean {
|
handleComponentInteraction(_interaction: ComponentInteraction): boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
get text(): string {
|
get text(): string {
|
||||||
return [...this.children].map((child) => child.text).join("")
|
return [...this.children].map((child) => child.text).join("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,105 +7,101 @@ import type { Renderer } from "./renderers/renderer"
|
|||||||
import { TextNode } from "./text-node.js"
|
import { TextNode } from "./text-node.js"
|
||||||
|
|
||||||
const config: HostConfig<
|
const config: HostConfig<
|
||||||
string, // Type,
|
string, // Type,
|
||||||
Record<string, unknown>, // Props,
|
Record<string, unknown>, // Props,
|
||||||
Renderer, // Container,
|
Renderer, // Container,
|
||||||
Node<unknown>, // Instance,
|
Node<unknown>, // Instance,
|
||||||
TextNode, // TextInstance,
|
TextNode, // TextInstance,
|
||||||
never, // SuspenseInstance,
|
never, // SuspenseInstance,
|
||||||
never, // HydratableInstance,
|
never, // HydratableInstance,
|
||||||
never, // PublicInstance,
|
never, // PublicInstance,
|
||||||
never, // HostContext,
|
null, // HostContext,
|
||||||
true, // UpdatePayload,
|
true, // UpdatePayload,
|
||||||
never, // ChildSet,
|
never, // ChildSet,
|
||||||
number, // TimeoutHandle,
|
number, // TimeoutHandle,
|
||||||
number // NoTimeout,
|
number // NoTimeout,
|
||||||
> = {
|
> = {
|
||||||
supportsMutation: true,
|
supportsMutation: true,
|
||||||
supportsPersistence: false,
|
supportsPersistence: false,
|
||||||
supportsHydration: false,
|
supportsHydration: false,
|
||||||
isPrimaryRenderer: true,
|
isPrimaryRenderer: true,
|
||||||
scheduleTimeout: global.setTimeout,
|
scheduleTimeout: global.setTimeout,
|
||||||
cancelTimeout: global.clearTimeout,
|
cancelTimeout: global.clearTimeout,
|
||||||
noTimeout: -1,
|
noTimeout: -1,
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
getRootHostContext: () => null,
|
||||||
getRootHostContext: () => null,
|
getChildHostContext: (parentContext) => parentContext,
|
||||||
getChildHostContext: (parentContext) => parentContext,
|
|
||||||
|
|
||||||
createInstance: (type, props) => {
|
createInstance: (type, props) => {
|
||||||
if (type !== "reacord-element") {
|
if (type !== "reacord-element") {
|
||||||
raise(`Unknown element type: ${type}`)
|
raise(`Unknown element type: ${type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof props.createNode !== "function") {
|
if (typeof props.createNode !== "function") {
|
||||||
raise(`Missing createNode function`)
|
raise(`Missing createNode function`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = props.createNode(props.props)
|
const node: unknown = props.createNode(props.props)
|
||||||
if (!(node instanceof Node)) {
|
if (!(node instanceof Node)) {
|
||||||
raise(`createNode function did not return a Node`)
|
raise(`createNode function did not return a Node`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
},
|
},
|
||||||
createTextInstance: (text) => new TextNode(text),
|
createTextInstance: (text) => new TextNode(text),
|
||||||
shouldSetTextContent: () => false,
|
shouldSetTextContent: () => false,
|
||||||
detachDeletedInstance: (instance) => {},
|
detachDeletedInstance: (_instance) => {},
|
||||||
beforeActiveInstanceBlur: () => {},
|
beforeActiveInstanceBlur: () => {},
|
||||||
afterActiveInstanceBlur: () => {},
|
afterActiveInstanceBlur: () => {},
|
||||||
// eslint-disable-next-line unicorn/no-null
|
getInstanceFromNode: (_node: unknown) => null,
|
||||||
getInstanceFromNode: (node: any) => null,
|
getInstanceFromScope: (_scopeInstance: unknown) => null,
|
||||||
// eslint-disable-next-line unicorn/no-null
|
|
||||||
getInstanceFromScope: (scopeInstance: any) => null,
|
|
||||||
|
|
||||||
clearContainer: (renderer) => {
|
clearContainer: (renderer) => {
|
||||||
renderer.nodes.clear()
|
renderer.nodes.clear()
|
||||||
},
|
},
|
||||||
appendChildToContainer: (renderer, child) => {
|
appendChildToContainer: (renderer, child) => {
|
||||||
renderer.nodes.add(child)
|
renderer.nodes.add(child)
|
||||||
},
|
},
|
||||||
removeChildFromContainer: (renderer, child) => {
|
removeChildFromContainer: (renderer, child) => {
|
||||||
renderer.nodes.remove(child)
|
renderer.nodes.remove(child)
|
||||||
},
|
},
|
||||||
insertInContainerBefore: (renderer, child, before) => {
|
insertInContainerBefore: (renderer, child, before) => {
|
||||||
renderer.nodes.addBefore(child, before)
|
renderer.nodes.addBefore(child, before)
|
||||||
},
|
},
|
||||||
|
|
||||||
appendInitialChild: (parent, child) => {
|
appendInitialChild: (parent, child) => {
|
||||||
parent.children.add(child)
|
parent.children.add(child)
|
||||||
},
|
},
|
||||||
appendChild: (parent, child) => {
|
appendChild: (parent, child) => {
|
||||||
parent.children.add(child)
|
parent.children.add(child)
|
||||||
},
|
},
|
||||||
removeChild: (parent, child) => {
|
removeChild: (parent, child) => {
|
||||||
parent.children.remove(child)
|
parent.children.remove(child)
|
||||||
},
|
},
|
||||||
insertBefore: (parent, child, before) => {
|
insertBefore: (parent, child, before) => {
|
||||||
parent.children.addBefore(child, before)
|
parent.children.addBefore(child, before)
|
||||||
},
|
},
|
||||||
|
|
||||||
prepareUpdate: () => true,
|
prepareUpdate: () => true,
|
||||||
commitUpdate: (node, payload, type, oldProps, newProps) => {
|
commitUpdate: (node, payload, type, oldProps, newProps) => {
|
||||||
node.props = newProps.props
|
node.props = newProps.props
|
||||||
},
|
},
|
||||||
commitTextUpdate: (node, oldText, newText) => {
|
commitTextUpdate: (node, oldText, newText) => {
|
||||||
node.props = newText
|
node.props = newText
|
||||||
},
|
},
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
prepareForCommit: () => null,
|
||||||
prepareForCommit: () => null,
|
resetAfterCommit: (renderer) => {
|
||||||
resetAfterCommit: (renderer) => {
|
renderer.render()
|
||||||
renderer.render()
|
},
|
||||||
},
|
prepareScopeUpdate: (_scopeInstance: unknown, _instance: unknown) => {},
|
||||||
prepareScopeUpdate: (scopeInstance: any, instance: any) => {},
|
|
||||||
|
|
||||||
preparePortalMount: () => raise("Portals are not supported"),
|
preparePortalMount: () => raise("Portals are not supported"),
|
||||||
getPublicInstance: () => raise("Refs are currently not supported"),
|
getPublicInstance: () => raise("Refs are currently not supported"),
|
||||||
|
|
||||||
finalizeInitialChildren: () => false,
|
finalizeInitialChildren: () => false,
|
||||||
|
|
||||||
getCurrentEventPriority: () => DefaultEventPriority,
|
getCurrentEventPriority: () => DefaultEventPriority,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reconciler = ReactReconciler(config)
|
export const reconciler = ReactReconciler(config)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type { Message, MessageOptions } from "../message"
|
|||||||
import { Renderer } from "./renderer"
|
import { Renderer } from "./renderer"
|
||||||
|
|
||||||
export class ChannelMessageRenderer extends Renderer {
|
export class ChannelMessageRenderer extends Renderer {
|
||||||
constructor(private channel: Channel) {
|
constructor(private channel: Channel) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||||
return this.channel.send(options)
|
return this.channel.send(options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 class InteractionReplyRenderer extends Renderer {
|
export type InteractionReplyRendererImplementation = {
|
||||||
constructor(private interaction: Interaction) {
|
interactionId: string
|
||||||
super()
|
reply: (options: MessageOptions) => Promise<Message>
|
||||||
}
|
followUp: (options: MessageOptions) => Promise<Message>
|
||||||
|
}
|
||||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
|
||||||
if (repliedInteractionIds.has(this.interaction.id)) {
|
export class InteractionReplyRenderer extends Renderer {
|
||||||
return this.interaction.followUp(options)
|
constructor(private implementation: InteractionReplyRendererImplementation) {
|
||||||
}
|
super()
|
||||||
|
}
|
||||||
repliedInteractionIds.add(this.interaction.id)
|
|
||||||
return this.interaction.reply(options)
|
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||||
}
|
if (repliedInteractionIds.has(this.implementation.interactionId)) {
|
||||||
|
return this.implementation.followUp(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
repliedInteractionIds.add(this.implementation.interactionId)
|
||||||
|
return this.implementation.reply(options)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +1,121 @@
|
|||||||
import { Subject } from "rxjs"
|
|
||||||
import { concatMap } from "rxjs/operators"
|
|
||||||
import { Container } from "../container.js"
|
import { Container } from "../container.js"
|
||||||
import type { ComponentInteraction } from "../interaction"
|
import type { ComponentInteraction } from "../interaction"
|
||||||
|
import { MessageStore } from "../message-store.js"
|
||||||
import type { Message, MessageOptions } from "../message"
|
import type { Message, MessageOptions } from "../message"
|
||||||
import type { Node } from "../node.js"
|
import type { Node } from "../node.js"
|
||||||
|
import { Subject } from "rxjs"
|
||||||
|
import { concatMap } from "rxjs/operators"
|
||||||
|
|
||||||
type UpdatePayload =
|
type UpdatePayload =
|
||||||
| { action: "update" | "deactivate"; options: MessageOptions }
|
| { action: "update" | "deactivate"; options: MessageOptions }
|
||||||
| { action: "deferUpdate"; interaction: ComponentInteraction }
|
| { action: "deferUpdate"; interaction: ComponentInteraction }
|
||||||
| { action: "destroy" }
|
| { action: "destroy" }
|
||||||
|
|
||||||
export abstract class Renderer {
|
export abstract class Renderer {
|
||||||
readonly nodes = new Container<Node<unknown>>()
|
readonly nodes = new Container<Node<unknown>>()
|
||||||
private componentInteraction?: ComponentInteraction
|
readonly messageStore = new MessageStore()
|
||||||
private message?: Message
|
private componentInteraction?: ComponentInteraction
|
||||||
private active = true
|
private message?: Message
|
||||||
private updates = new Subject<UpdatePayload>()
|
private active = true
|
||||||
|
private updates = new Subject<UpdatePayload>()
|
||||||
|
|
||||||
private updateSubscription = this.updates
|
private updateSubscription = this.updates
|
||||||
.pipe(concatMap((payload) => this.updateMessage(payload)))
|
.pipe(concatMap((payload) => this.updateMessage(payload)))
|
||||||
.subscribe({ error: console.error })
|
.subscribe({ error: console.error })
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
console.warn("Attempted to update a deactivated message")
|
console.warn("Attempted to update a deactivated message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updates.next({
|
this.updates.next({
|
||||||
options: this.getMessageOptions(),
|
options: this.getMessageOptions(),
|
||||||
action: "update",
|
action: "update",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
this.active = false
|
this.active = false
|
||||||
this.updates.next({
|
this.updates.next({
|
||||||
options: this.getMessageOptions(),
|
options: this.getMessageOptions(),
|
||||||
action: "deactivate",
|
action: "deactivate",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.active = false
|
this.active = false
|
||||||
this.updates.next({ action: "destroy" })
|
this.updates.next({ action: "destroy" })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleComponentInteraction(interaction: ComponentInteraction) {
|
handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
this.componentInteraction = interaction
|
for (const node of this.nodes) {
|
||||||
|
if (node.handleComponentInteraction(interaction)) {
|
||||||
|
this.componentInteraction = interaction
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updates.next({ action: "deferUpdate", interaction })
|
||||||
|
}, 500)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
protected abstract createMessage(options: MessageOptions): Promise<Message>
|
||||||
this.updates.next({ action: "deferUpdate", interaction })
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
for (const node of this.nodes) {
|
private getMessageOptions(): MessageOptions {
|
||||||
if (node.handleComponentInteraction(interaction)) {
|
const options: MessageOptions = {
|
||||||
return true
|
content: "",
|
||||||
}
|
embeds: [],
|
||||||
}
|
actionRows: [],
|
||||||
}
|
}
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
node.modifyMessageOptions(options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract createMessage(options: MessageOptions): Promise<Message>
|
private async updateMessage(payload: UpdatePayload) {
|
||||||
|
if (payload.action === "destroy") {
|
||||||
|
this.updateSubscription.unsubscribe()
|
||||||
|
this.messageStore.set(undefined)
|
||||||
|
await this.message?.delete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
private getMessageOptions(): MessageOptions {
|
if (payload.action === "deactivate") {
|
||||||
const options: MessageOptions = {
|
this.updateSubscription.unsubscribe()
|
||||||
content: "",
|
|
||||||
embeds: [],
|
|
||||||
actionRows: [],
|
|
||||||
}
|
|
||||||
for (const node of this.nodes) {
|
|
||||||
node.modifyMessageOptions(options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateMessage(payload: UpdatePayload) {
|
await this.message?.edit({
|
||||||
if (payload.action === "destroy") {
|
...payload.options,
|
||||||
this.updateSubscription.unsubscribe()
|
actionRows: payload.options.actionRows.map((row) =>
|
||||||
await this.message?.delete()
|
row.map((component) => ({
|
||||||
return
|
...component,
|
||||||
}
|
disabled: true,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
if (payload.action === "deactivate") {
|
return
|
||||||
this.updateSubscription.unsubscribe()
|
}
|
||||||
|
|
||||||
await this.message?.edit({
|
if (payload.action === "deferUpdate") {
|
||||||
...payload.options,
|
await payload.interaction.deferUpdate()
|
||||||
actionRows: payload.options.actionRows.map((row) =>
|
return
|
||||||
row.map((component) => ({
|
}
|
||||||
...component,
|
|
||||||
disabled: true,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
if (this.componentInteraction) {
|
||||||
}
|
const promise = this.componentInteraction.update(payload.options)
|
||||||
|
this.componentInteraction = undefined
|
||||||
|
await promise
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (payload.action === "deferUpdate") {
|
if (this.message) {
|
||||||
await payload.interaction.deferUpdate()
|
await this.message.edit(payload.options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.componentInteraction) {
|
this.message = await this.createMessage(payload.options)
|
||||||
const promise = this.componentInteraction.update(payload.options)
|
this.messageStore.set(this.message.data)
|
||||||
this.componentInteraction = undefined
|
}
|
||||||
await promise
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.message) {
|
|
||||||
await this.message.edit(payload.options)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.message = await this.createMessage(payload.options)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import type { MessageOptions } from "./message"
|
|||||||
import { Node } from "./node.js"
|
import { Node } from "./node.js"
|
||||||
|
|
||||||
export class TextNode extends Node<string> {
|
export class TextNode extends Node<string> {
|
||||||
override modifyMessageOptions(options: MessageOptions) {
|
override modifyMessageOptions(options: MessageOptions) {
|
||||||
options.content = options.content + this.props
|
options.content = options.content + this.props
|
||||||
}
|
}
|
||||||
|
|
||||||
override get text() {
|
override get text() {
|
||||||
return this.props
|
return this.props
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
export class Timeout {
|
export class Timeout {
|
||||||
private timeoutId?: NodeJS.Timeout
|
private timeoutId?: NodeJS.Timeout
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly time: number,
|
private readonly time: number,
|
||||||
private readonly callback: () => void,
|
private readonly callback: () => void,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
this.cancel()
|
this.cancel()
|
||||||
this.timeoutId = setTimeout(this.callback, this.time)
|
this.timeoutId = setTimeout(this.callback, this.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
if (this.timeoutId) {
|
if (this.timeoutId) {
|
||||||
clearTimeout(this.timeoutId)
|
clearTimeout(this.timeoutId)
|
||||||
this.timeoutId = undefined
|
this.timeoutId = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export * from "./core/components/link"
|
|||||||
export * from "./core/components/option"
|
export * from "./core/components/option"
|
||||||
export * from "./core/components/select"
|
export * from "./core/components/select"
|
||||||
export * from "./core/instance"
|
export * from "./core/instance"
|
||||||
export { useInstance } from "./core/instance-context"
|
export { useInstance, useMessage } from "./core/instance-context"
|
||||||
export * from "./core/reacord"
|
export * from "./core/reacord"
|
||||||
export * from "./core/reacord-discord-js"
|
export * from "./core/reacord-discord-js"
|
||||||
|
|||||||
@@ -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.2",
|
"version": "0.6.0",
|
||||||
"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",
|
||||||
@@ -24,6 +23,7 @@
|
|||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/main.js",
|
"import": "./dist/main.js",
|
||||||
@@ -36,22 +36,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cp ../../README.md . && cp ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --dts --sourcemap",
|
"build": "rm -rf dist && mkdir -p dist && cp ../../README.md ../../LICENSE . && bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --external react --external react/jsx-runtime --external react/jsx-dev-runtime && bun build library/main.ts --target=node --outdir=dist --format=cjs --sourcemap=external --entry-naming=main.cjs --external react --external react/jsx-runtime --external react/jsx-dev-runtime && tsc -p tsconfig.build.json",
|
||||||
"build-watch": "pnpm build -- --watch",
|
"build-watch": "bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --watch --external react --external react/jsx-runtime --external react/jsx-dev-runtime",
|
||||||
"test": "vitest --coverage --no-watch",
|
"test": "vitest --coverage --no-watch",
|
||||||
"test-dev": "vitest",
|
"test-dev": "vitest",
|
||||||
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/react": "^18.2.27",
|
||||||
"@types/react": "*",
|
"@types/react-reconciler": "^0.28.5",
|
||||||
"@types/react-reconciler": "^0.28.0",
|
|
||||||
"react-reconciler": "^0.29.0",
|
"react-reconciler": "^0.29.0",
|
||||||
"rxjs": "^7.5.6"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"discord.js": "^14",
|
"discord.js": "^14.25.1",
|
||||||
"react": ">=17"
|
"react": ">=17"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -61,23 +60,17 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@reacord/helpers": "workspace:*",
|
"@reacord/helpers": "workspace:*",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"c8": "^7.12.0",
|
"c8": "^8.0.1",
|
||||||
"discord.js": "^14.0.3",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.3.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^3.0.3",
|
||||||
"pretty-ms": "^8.0.0",
|
"pretty-ms": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tsup": "^6.1.3",
|
"tsx": "^3.13.0",
|
||||||
"tsx": "^3.8.0",
|
"type-fest": "^4.4.0"
|
||||||
"type-fest": "^2.17.0",
|
|
||||||
"typescript": "^4.7.4",
|
|
||||||
"vitest": "^0.18.1"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"esbuild": "latest"
|
|
||||||
},
|
},
|
||||||
"release-it": {
|
"release-it": {
|
||||||
"git": {
|
"git": {
|
||||||
|
|||||||
@@ -1,139 +1,193 @@
|
|||||||
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Embed,
|
||||||
|
EmbedField,
|
||||||
|
Link,
|
||||||
|
Option,
|
||||||
|
ReacordDiscordJs,
|
||||||
|
Select,
|
||||||
|
useInstance,
|
||||||
|
} from "../library/main.js"
|
||||||
import type { TextChannel } from "discord.js"
|
import type { TextChannel } from "discord.js"
|
||||||
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import { kebabCase } from "lodash-es"
|
import { kebabCase } from "lodash-es"
|
||||||
import * as React from "react"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Link,
|
|
||||||
Option,
|
|
||||||
ReacordDiscordJs,
|
|
||||||
Select,
|
|
||||||
useInstance,
|
|
||||||
} from "../library/main"
|
|
||||||
|
|
||||||
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
||||||
const reacord = new ReacordDiscordJs(client)
|
const reacord = new ReacordDiscordJs(client)
|
||||||
|
|
||||||
await client.login(process.env.TEST_BOT_TOKEN)
|
await client.login(process.env.TEST_BOT_TOKEN)
|
||||||
|
|
||||||
const guild = await client.guilds.fetch(process.env.TEST_GUILD_ID!)
|
const guild = await client.guilds.fetch(
|
||||||
|
process.env.TEST_GUILD_ID ?? raise("TEST_GUILD_ID not defined"),
|
||||||
|
)
|
||||||
|
|
||||||
const category = await guild.channels.fetch(process.env.TEST_CATEGORY_ID!)
|
const category = await guild.channels.fetch(
|
||||||
|
process.env.TEST_CATEGORY_ID ?? raise("TEST_CATEGORY_ID not defined"),
|
||||||
|
)
|
||||||
if (category?.type !== ChannelType.GuildCategory) {
|
if (category?.type !== ChannelType.GuildCategory) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`,
|
`channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [, channel] of category.children.cache) {
|
for (const [, channel] of category.children.cache) {
|
||||||
await channel.delete()
|
await channel.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefix = 0
|
let prefix = 0
|
||||||
const createTest = async (
|
const createTest = async (
|
||||||
name: string,
|
name: string,
|
||||||
block: (channel: TextChannel) => void | Promise<unknown>,
|
block: (channel: TextChannel) => unknown,
|
||||||
) => {
|
) => {
|
||||||
prefix += 1
|
prefix += 1
|
||||||
const channel = await category.children.create({
|
const channel = await category.children.create({
|
||||||
type: ChannelType.GuildText,
|
type: ChannelType.GuildText,
|
||||||
name: `${String(prefix).padStart(3, "0")}-${kebabCase(name)}`,
|
name: `${String(prefix).padStart(3, "0")}-${kebabCase(name)}`,
|
||||||
})
|
})
|
||||||
await block(channel)
|
await block(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
await createTest("basic", (channel) => {
|
await createTest("basic", (channel) => {
|
||||||
reacord.send(channel.id, "Hello, world!")
|
reacord.createChannelMessage(channel).render("Hello, world!")
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTest("readme counter", (channel) => {
|
||||||
|
interface EmbedCounterProps {
|
||||||
|
count: number
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbedCounter({ count, visible }: EmbedCounterProps) {
|
||||||
|
if (!visible) return <></>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Embed title="the counter">
|
||||||
|
<EmbedField name="is it even?">{count % 2 ? "no" : "yes"}</EmbedField>
|
||||||
|
</Embed>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
const [showEmbed, setShowEmbed] = useState(false)
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
const instance = useInstance()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
this button was clicked {count} times
|
||||||
|
<EmbedCounter count={count} visible={showEmbed} />
|
||||||
|
<Button
|
||||||
|
style="primary"
|
||||||
|
label="clicc"
|
||||||
|
onClick={() => setCount(count + 1)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style="secondary"
|
||||||
|
label={showEmbed ? "hide embed" : "show embed"}
|
||||||
|
onClick={() => setShowEmbed(!showEmbed)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style="danger"
|
||||||
|
label="deactivate"
|
||||||
|
onClick={() => instance.destroy()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reacord.createChannelMessage(channel).render(<Counter />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("counter", (channel) => {
|
await createTest("counter", (channel) => {
|
||||||
const Counter = () => {
|
function Counter() {
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = useState(0)
|
||||||
return (
|
|
||||||
<>
|
return (
|
||||||
count: {count}
|
<>
|
||||||
<Button
|
count: {count}
|
||||||
style="primary"
|
<Button
|
||||||
emoji="➕"
|
style="primary"
|
||||||
onClick={() => setCount(count + 1)}
|
emoji="➕"
|
||||||
/>
|
onClick={() => setCount(count + 1)}
|
||||||
<Button
|
/>
|
||||||
style="primary"
|
<Button
|
||||||
emoji="➖"
|
style="primary"
|
||||||
onClick={() => setCount(count - 1)}
|
emoji="➖"
|
||||||
/>
|
onClick={() => setCount(count - 1)}
|
||||||
<Button label="reset" onClick={() => setCount(0)} />
|
/>
|
||||||
</>
|
<Button label="reset" onClick={() => setCount(0)} />
|
||||||
)
|
</>
|
||||||
}
|
)
|
||||||
reacord.send(channel.id, <Counter />)
|
}
|
||||||
|
reacord.createChannelMessage(channel).render(<Counter />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("select", (channel) => {
|
await createTest("select", (channel) => {
|
||||||
function FruitSelect({ onConfirm }: { onConfirm: (choice: string) => void }) {
|
function FruitSelect({ onConfirm }: { onConfirm: (choice: string) => void }) {
|
||||||
const [value, setValue] = useState<string>()
|
const [value, setValue] = useState<string>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
placeholder="choose a fruit"
|
placeholder="choose a fruit"
|
||||||
value={value}
|
value={value}
|
||||||
onChangeValue={setValue}
|
onChangeValue={setValue}
|
||||||
>
|
>
|
||||||
<Option value="🍎" emoji="🍎" label="apple" description="it red" />
|
<Option value="🍎" emoji="🍎" label="apple" description="it red" />
|
||||||
<Option value="🍌" emoji="🍌" label="banana" description="bnanbna" />
|
<Option value="🍌" emoji="🍌" label="banana" description="bnanbna" />
|
||||||
<Option value="🍒" emoji="🍒" label="cherry" description="heh" />
|
<Option value="🍒" emoji="🍒" label="cherry" description="heh" />
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
label="confirm"
|
label="confirm"
|
||||||
disabled={value == undefined}
|
disabled={value == undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (value) onConfirm(value)
|
if (value) onConfirm(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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}`)
|
instance.deactivate()
|
||||||
instance.deactivate()
|
}}
|
||||||
}}
|
/>,
|
||||||
/>,
|
)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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"
|
onClick={(event) =>
|
||||||
onClick={(event) =>
|
event.reply(`${event.guild?.member.displayName} clic`)
|
||||||
event.reply(`${event.guild?.member.displayName} clic`)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Button
|
||||||
<Button
|
label="clic"
|
||||||
label="clic"
|
onClick={(event) => event.reply("you clic", { ephemeral: true })}
|
||||||
onClick={(event) => event.ephemeralReply("you clic")}
|
/>
|
||||||
/>
|
</>,
|
||||||
</>,
|
)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("delete this", (channel) => {
|
await createTest("delete this", (channel) => {
|
||||||
function DeleteThis() {
|
function DeleteThis() {
|
||||||
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" />)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react"
|
|
||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
import { ActionRow, Button, Select } from "../library/main"
|
import { ActionRow, Button, Select } from "../library/main"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
@@ -6,36 +5,36 @@ import { ReacordTester } from "./test-adapter"
|
|||||||
const testing = new ReacordTester()
|
const testing = new ReacordTester()
|
||||||
|
|
||||||
test("action row", async () => {
|
test("action row", async () => {
|
||||||
await testing.assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Button label="outside button" onClick={() => {}} />
|
<Button label="outside button" onClick={() => {}} />
|
||||||
<ActionRow>
|
<ActionRow>
|
||||||
<Button label="button inside action row" onClick={() => {}} />
|
<Button label="button inside action row" onClick={() => {}} />
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
<Select />
|
<Select />
|
||||||
<Button label="last row 1" onClick={() => {}} />
|
<Button label="last row 1" onClick={() => {}} />
|
||||||
<Button label="last row 2" onClick={() => {}} />
|
<Button label="last row 2" onClick={() => {}} />
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[{ type: "button", style: "secondary", label: "outside button" }],
|
[{ type: "button", style: "secondary", label: "outside button" }],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "button inside action row",
|
label: "button inside action row",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[{ type: "select", options: [], values: [] }],
|
[{ type: "select", options: [], values: [] }],
|
||||||
[
|
[
|
||||||
{ type: "button", style: "secondary", label: "last row 1" },
|
{ type: "button", style: "secondary", label: "last row 1" },
|
||||||
{ type: "button", style: "secondary", label: "last row 2" },
|
{ type: "button", style: "secondary", label: "last row 2" },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { spawnSync } from "node:child_process"
|
import { spawnSync } from "node:child_process"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { dirname } from "node:path"
|
||||||
import { createRequire } from "node:module"
|
import { createRequire } from "node:module"
|
||||||
import { beforeAll, expect, test } from "vitest"
|
import { beforeAll, expect, test } from "vitest"
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
spawnSync("pnpm", ["run", "build"])
|
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||||
|
spawnSync("bun", ["run", "build"], { cwd })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can require commonjs", () => {
|
test("can require commonjs", () => {
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
expect(() => require("../dist/main.cjs")).not.toThrow()
|
expect(() => require("../dist/main.cjs") as unknown).not.toThrow()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,275 +1,274 @@
|
|||||||
import React from "react"
|
|
||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
import {
|
import {
|
||||||
Embed,
|
Embed,
|
||||||
EmbedAuthor,
|
EmbedAuthor,
|
||||||
EmbedField,
|
EmbedField,
|
||||||
EmbedFooter,
|
EmbedFooter,
|
||||||
EmbedImage,
|
EmbedImage,
|
||||||
EmbedThumbnail,
|
EmbedThumbnail,
|
||||||
EmbedTitle,
|
EmbedTitle,
|
||||||
} from "../library/main"
|
} from "../library/main"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
|
|
||||||
const testing = new ReacordTester()
|
const testing = new ReacordTester()
|
||||||
|
|
||||||
test("kitchen sink", async () => {
|
test("kitchen sink", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await testing.assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed color={0xfe_ee_ef}>
|
<Embed color={0xfe_ee_ef}>
|
||||||
<EmbedAuthor name="author" iconUrl="https://example.com/author.png" />
|
<EmbedAuthor name="author" iconUrl="https://example.com/author.png" />
|
||||||
<EmbedTitle>title text</EmbedTitle>
|
<EmbedTitle>title text</EmbedTitle>
|
||||||
description text
|
description text
|
||||||
<EmbedThumbnail url="https://example.com/thumbnail.png" />
|
<EmbedThumbnail url="https://example.com/thumbnail.png" />
|
||||||
<EmbedImage url="https://example.com/image.png" />
|
<EmbedImage url="https://example.com/image.png" />
|
||||||
<EmbedField name="field name" value="field value" inline />
|
<EmbedField name="field name" value="field value" inline />
|
||||||
<EmbedField name="block field" value="block field value" />
|
<EmbedField name="block field" value="block field value" />
|
||||||
<EmbedFooter
|
<EmbedFooter
|
||||||
text="footer text"
|
text="footer text"
|
||||||
iconUrl="https://example.com/footer.png"
|
iconUrl="https://example.com/footer.png"
|
||||||
timestamp={now}
|
timestamp={now}
|
||||||
/>
|
/>
|
||||||
</Embed>
|
</Embed>
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
actionRows: [],
|
actionRows: [],
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
description: "description text",
|
description: "description text",
|
||||||
author: {
|
author: {
|
||||||
icon_url: "https://example.com/author.png",
|
icon_url: "https://example.com/author.png",
|
||||||
name: "author",
|
name: "author",
|
||||||
},
|
},
|
||||||
color: 0xfe_ee_ef,
|
color: 0xfe_ee_ef,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
inline: true,
|
inline: true,
|
||||||
name: "field name",
|
name: "field name",
|
||||||
value: "field value",
|
value: "field value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "block field",
|
name: "block field",
|
||||||
value: "block field value",
|
value: "block field value",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
footer: {
|
footer: {
|
||||||
icon_url: "https://example.com/footer.png",
|
icon_url: "https://example.com/footer.png",
|
||||||
text: "footer text",
|
text: "footer text",
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
url: "https://example.com/image.png",
|
url: "https://example.com/image.png",
|
||||||
},
|
},
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: "https://example.com/thumbnail.png",
|
url: "https://example.com/thumbnail.png",
|
||||||
},
|
},
|
||||||
timestamp: now.toISOString(),
|
timestamp: now.toISOString(),
|
||||||
title: "title text",
|
title: "title text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("author variants", async () => {
|
test("author variants", async () => {
|
||||||
await testing.assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedAuthor iconUrl="https://example.com/author.png">
|
<EmbedAuthor iconUrl="https://example.com/author.png">
|
||||||
author name
|
author name
|
||||||
</EmbedAuthor>
|
</EmbedAuthor>
|
||||||
</Embed>
|
</Embed>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedAuthor iconUrl="https://example.com/author.png" />
|
<EmbedAuthor iconUrl="https://example.com/author.png" />
|
||||||
</Embed>
|
</Embed>
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
actionRows: [],
|
actionRows: [],
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
author: {
|
author: {
|
||||||
icon_url: "https://example.com/author.png",
|
icon_url: "https://example.com/author.png",
|
||||||
name: "author name",
|
name: "author name",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
author: {
|
author: {
|
||||||
icon_url: "https://example.com/author.png",
|
icon_url: "https://example.com/author.png",
|
||||||
name: "",
|
name: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("field variants", async () => {
|
test("field variants", async () => {
|
||||||
await testing.assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedField name="field name" value="field value" />
|
<EmbedField name="field name" value="field value" />
|
||||||
<EmbedField name="field name" value="field value" inline />
|
<EmbedField name="field name" value="field value" inline />
|
||||||
<EmbedField name="field name" inline>
|
<EmbedField name="field name" inline>
|
||||||
field value
|
field value
|
||||||
</EmbedField>
|
</EmbedField>
|
||||||
<EmbedField name="field name" />
|
<EmbedField name="field name" />
|
||||||
</Embed>
|
</Embed>
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
actionRows: [],
|
actionRows: [],
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: "field name",
|
name: "field name",
|
||||||
value: "field value",
|
value: "field value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inline: true,
|
inline: true,
|
||||||
name: "field name",
|
name: "field name",
|
||||||
value: "field value",
|
value: "field value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inline: true,
|
inline: true,
|
||||||
name: "field name",
|
name: "field name",
|
||||||
value: "field value",
|
value: "field value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "field name",
|
name: "field name",
|
||||||
value: "",
|
value: "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("footer variants", async () => {
|
test("footer variants", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await testing.assertRender(
|
await testing.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedFooter text="footer text" />
|
<EmbedFooter text="footer text" />
|
||||||
</Embed>
|
</Embed>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedFooter
|
<EmbedFooter
|
||||||
text="footer text"
|
text="footer text"
|
||||||
iconUrl="https://example.com/footer.png"
|
iconUrl="https://example.com/footer.png"
|
||||||
/>
|
/>
|
||||||
</Embed>
|
</Embed>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedFooter timestamp={now}>footer text</EmbedFooter>
|
<EmbedFooter timestamp={now}>footer text</EmbedFooter>
|
||||||
</Embed>
|
</Embed>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedFooter iconUrl="https://example.com/footer.png" timestamp={now} />
|
<EmbedFooter iconUrl="https://example.com/footer.png" timestamp={now} />
|
||||||
</Embed>
|
</Embed>
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
actionRows: [],
|
actionRows: [],
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
footer: {
|
footer: {
|
||||||
text: "footer text",
|
text: "footer text",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
footer: {
|
footer: {
|
||||||
icon_url: "https://example.com/footer.png",
|
icon_url: "https://example.com/footer.png",
|
||||||
text: "footer text",
|
text: "footer text",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
footer: {
|
footer: {
|
||||||
text: "footer text",
|
text: "footer text",
|
||||||
},
|
},
|
||||||
timestamp: now.toISOString(),
|
timestamp: now.toISOString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
footer: {
|
footer: {
|
||||||
icon_url: "https://example.com/footer.png",
|
icon_url: "https://example.com/footer.png",
|
||||||
text: "",
|
text: "",
|
||||||
},
|
},
|
||||||
timestamp: now.toISOString(),
|
timestamp: now.toISOString(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("embed props", async () => {
|
test("embed props", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await testing.assertRender(
|
await testing.assertRender(
|
||||||
<Embed
|
<Embed
|
||||||
title="title text"
|
title="title text"
|
||||||
description="description text"
|
description="description text"
|
||||||
url="https://example.com/"
|
url="https://example.com/"
|
||||||
color={0xfe_ee_ef}
|
color={0xfe_ee_ef}
|
||||||
timestamp={now}
|
timestamp={now}
|
||||||
author={{
|
author={{
|
||||||
name: "author name",
|
name: "author name",
|
||||||
url: "https://example.com/author",
|
url: "https://example.com/author",
|
||||||
iconUrl: "https://example.com/author.png",
|
iconUrl: "https://example.com/author.png",
|
||||||
}}
|
}}
|
||||||
thumbnail={{
|
thumbnail={{
|
||||||
url: "https://example.com/thumbnail.png",
|
url: "https://example.com/thumbnail.png",
|
||||||
}}
|
}}
|
||||||
image={{
|
image={{
|
||||||
url: "https://example.com/image.png",
|
url: "https://example.com/image.png",
|
||||||
}}
|
}}
|
||||||
footer={{
|
footer={{
|
||||||
text: "footer text",
|
text: "footer text",
|
||||||
iconUrl: "https://example.com/footer.png",
|
iconUrl: "https://example.com/footer.png",
|
||||||
}}
|
}}
|
||||||
fields={[
|
fields={[
|
||||||
{ name: "field name", value: "field value", inline: true },
|
{ name: "field name", value: "field value", inline: true },
|
||||||
{ name: "block field", value: "block field value" },
|
{ name: "block field", value: "block field value" },
|
||||||
]}
|
]}
|
||||||
/>,
|
/>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
actionRows: [],
|
actionRows: [],
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: "title text",
|
title: "title text",
|
||||||
description: "description text",
|
description: "description text",
|
||||||
url: "https://example.com/",
|
url: "https://example.com/",
|
||||||
color: 0xfe_ee_ef,
|
color: 0xfe_ee_ef,
|
||||||
timestamp: now.toISOString(),
|
timestamp: now.toISOString(),
|
||||||
author: {
|
author: {
|
||||||
name: "author name",
|
name: "author name",
|
||||||
url: "https://example.com/author",
|
url: "https://example.com/author",
|
||||||
icon_url: "https://example.com/author.png",
|
icon_url: "https://example.com/author.png",
|
||||||
},
|
},
|
||||||
thumbnail: { url: "https://example.com/thumbnail.png" },
|
thumbnail: { url: "https://example.com/thumbnail.png" },
|
||||||
image: { url: "https://example.com/image.png" },
|
image: { url: "https://example.com/image.png" },
|
||||||
footer: {
|
footer: {
|
||||||
text: "footer text",
|
text: "footer text",
|
||||||
icon_url: "https://example.com/footer.png",
|
icon_url: "https://example.com/footer.png",
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "field name", value: "field value", inline: true },
|
{ name: "field name", value: "field value", inline: true },
|
||||||
{ name: "block field", value: "block field value" },
|
{ name: "block field", value: "block field value" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
|
|
||||||
test.todo("ephemeral reply")
|
test.todo("ephemeral reply")
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react"
|
|
||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
import { Link } from "../library/main"
|
import { Link } from "../library/main"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
@@ -6,37 +5,37 @@ import { ReacordTester } from "./test-adapter"
|
|||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
test("link", async () => {
|
test("link", async () => {
|
||||||
await tester.assertRender(
|
await tester.assertRender(
|
||||||
<>
|
<>
|
||||||
<Link url="https://example.com/">link text</Link>
|
<Link url="https://example.com/">link text</Link>
|
||||||
<Link label="link text" url="https://example.com/" />
|
<Link label="link text" url="https://example.com/" />
|
||||||
<Link label="link text" url="https://example.com/" disabled />
|
<Link label="link text" url="https://example.com/" disabled />
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
url: "https://example.com/",
|
url: "https://example.com/",
|
||||||
label: "link text",
|
label: "link text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
url: "https://example.com/",
|
url: "https://example.com/",
|
||||||
label: "link text",
|
label: "link text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
url: "https://example.com/",
|
url: "https://example.com/",
|
||||||
label: "link text",
|
label: "link text",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,270 +1,270 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { test } from "vitest"
|
|
||||||
import { Button, Embed, EmbedField, EmbedTitle } from "../library/main"
|
import { Button, Embed, EmbedField, EmbedTitle } from "../library/main"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
|
import * as React from "react"
|
||||||
|
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([
|
||||||
{
|
{
|
||||||
content: "count: 0",
|
content: "count: 0",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "show embed",
|
label: "show embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("show embed").click()
|
await tester.findButtonByLabel("show embed").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 0",
|
content: "count: 0",
|
||||||
embeds: [{ title: "the counter" }],
|
embeds: [{ title: "the counter" }],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "hide embed",
|
label: "hide embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 1",
|
content: "count: 1",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: "the counter",
|
title: "the counter",
|
||||||
fields: [{ name: "is it even?", value: "no" }],
|
fields: [{ name: "is it even?", value: "no" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "hide embed",
|
label: "hide embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 2",
|
content: "count: 2",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: "the counter",
|
title: "the counter",
|
||||||
fields: [{ name: "is it even?", value: "yes" }],
|
fields: [{ name: "is it even?", value: "yes" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "hide embed",
|
label: "hide embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("hide embed").click()
|
await tester.findButtonByLabel("hide embed").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 2",
|
content: "count: 2",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "show embed",
|
label: "show embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "show embed",
|
label: "show embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("deactivate").click()
|
await tester.findButtonByLabel("deactivate").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "show embed",
|
label: "show embed",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "primary",
|
style: "primary",
|
||||||
label: "clicc",
|
label: "clicc",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
label: "show embed",
|
label: "show embed",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
style: "danger",
|
style: "danger",
|
||||||
label: "deactivate",
|
label: "deactivate",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
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>
|
<Button label="some button" onClick={() => {}} />
|
||||||
<Button label="some button" onClick={() => {}} />
|
</>,
|
||||||
</>,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "some text",
|
content: "some text",
|
||||||
embeds: [{ description: "some embed" }],
|
embeds: [{ description: "some embed" }],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[{ type: "button", style: "secondary", label: "some button" }],
|
[{ type: "button", style: "secondary", label: "some button" }],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
reply.destroy()
|
reply.destroy()
|
||||||
await tester.assertMessages([])
|
await tester.assertMessages([])
|
||||||
})
|
})
|
||||||
|
|
||||||
// test multiple instances that can be updated independently,
|
// test multiple instances that can be updated independently,
|
||||||
@@ -272,34 +272,34 @@ test("delete", async () => {
|
|||||||
test.todo("multiple instances")
|
test.todo("multiple instances")
|
||||||
|
|
||||||
function KitchenSinkCounter(props: { onDeactivate: () => void }) {
|
function KitchenSinkCounter(props: { onDeactivate: () => void }) {
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = React.useState(0)
|
||||||
const [embedVisible, setEmbedVisible] = React.useState(false)
|
const [embedVisible, setEmbedVisible] = React.useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
count: {count}
|
count: {count}
|
||||||
{embedVisible && (
|
{embedVisible && (
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedTitle>the counter</EmbedTitle>
|
<EmbedTitle>the counter</EmbedTitle>
|
||||||
{count > 0 && (
|
{count > 0 && (
|
||||||
<EmbedField name="is it even?">
|
<EmbedField name="is it even?">
|
||||||
{count % 2 === 0 ? "yes" : "no"}
|
{count % 2 === 0 ? "yes" : "no"}
|
||||||
</EmbedField>
|
</EmbedField>
|
||||||
)}
|
)}
|
||||||
</Embed>
|
</Embed>
|
||||||
)}
|
)}
|
||||||
{embedVisible && (
|
{embedVisible && (
|
||||||
<Button label="hide embed" onClick={() => setEmbedVisible(false)} />
|
<Button label="hide embed" onClick={() => setEmbedVisible(false)} />
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
style="primary"
|
style="primary"
|
||||||
label="clicc"
|
label="clicc"
|
||||||
onClick={() => setCount(count + 1)}
|
onClick={() => setCount(count + 1)}
|
||||||
/>
|
/>
|
||||||
{!embedVisible && (
|
{!embedVisible && (
|
||||||
<Button label="show embed" onClick={() => setEmbedVisible(true)} />
|
<Button label="show embed" onClick={() => setEmbedVisible(true)} />
|
||||||
)}
|
)}
|
||||||
<Button style="danger" label="deactivate" onClick={props.onDeactivate} />
|
<Button style="danger" label="deactivate" onClick={props.onDeactivate} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,160 +1,160 @@
|
|||||||
import React, { useState } from "react"
|
import { useState } from "react"
|
||||||
import { expect, test, vi } from "vitest"
|
import { expect, test, vi } from "vitest"
|
||||||
import { Button, Option, Select } from "../library/main"
|
import { Button, Option, Select } from "../library/main"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
|
|
||||||
test("single select", async () => {
|
test("single select", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
const onSelect = vi.fn()
|
const onSelect = vi.fn()
|
||||||
|
|
||||||
function TestSelect() {
|
function TestSelect() {
|
||||||
const [value, setValue] = useState<string>()
|
const [value, setValue] = useState<string>()
|
||||||
const [disabled, setDisabled] = useState(false)
|
const [disabled, setDisabled] = useState(false)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
placeholder="choose one"
|
placeholder="choose one"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onSelect}
|
onChange={onSelect}
|
||||||
onChangeValue={setValue}
|
onChangeValue={setValue}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Option value="1" />
|
<Option value="1" />
|
||||||
<Option value="2" label="two" />
|
<Option value="2" label="two" />
|
||||||
<Option value="3">three</Option>
|
<Option value="3">three</Option>
|
||||||
</Select>
|
</Select>
|
||||||
<Button label="disable" onClick={() => setDisabled(true)} />
|
<Button label="disable" onClick={() => setDisabled(true)} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertSelect(values: string[], disabled = false) {
|
async function assertSelect(values: string[], disabled = false) {
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "select",
|
type: "select",
|
||||||
placeholder: "choose one",
|
placeholder: "choose one",
|
||||||
values,
|
values,
|
||||||
disabled,
|
disabled,
|
||||||
options: [
|
options: [
|
||||||
{ label: "1", value: "1" },
|
{ label: "1", value: "1" },
|
||||||
{ label: "two", value: "2" },
|
{ label: "two", value: "2" },
|
||||||
{ label: "three", value: "3" },
|
{ label: "three", value: "3" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[{ type: "button", style: "secondary", label: "disable" }],
|
[{ type: "button", style: "secondary", label: "disable" }],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
await assertSelect([])
|
||||||
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
await tester.findSelectByPlaceholder("choose one").select("2")
|
||||||
await assertSelect([])
|
await assertSelect(["2"])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ values: ["2"] }),
|
||||||
|
)
|
||||||
|
|
||||||
await tester.findSelectByPlaceholder("choose one").select("2")
|
await tester.findButtonByLabel("disable").click()
|
||||||
await assertSelect(["2"])
|
await assertSelect(["2"], true)
|
||||||
expect(onSelect).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ values: ["2"] }),
|
|
||||||
)
|
|
||||||
|
|
||||||
await tester.findButtonByLabel("disable").click()
|
await tester.findSelectByPlaceholder("choose one").select("1")
|
||||||
await assertSelect(["2"], true)
|
await assertSelect(["2"], true)
|
||||||
|
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||||
await tester.findSelectByPlaceholder("choose one").select("1")
|
|
||||||
await assertSelect(["2"], true)
|
|
||||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("multiple select", async () => {
|
test("multiple select", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
const onSelect = vi.fn()
|
const onSelect = vi.fn()
|
||||||
|
|
||||||
function TestSelect() {
|
function TestSelect() {
|
||||||
const [values, setValues] = useState<string[]>([])
|
const [values, setValues] = useState<string[]>([])
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
placeholder="select"
|
placeholder="select"
|
||||||
multiple
|
multiple
|
||||||
values={values}
|
values={values}
|
||||||
onChange={onSelect}
|
onChange={onSelect}
|
||||||
onChangeMultiple={setValues}
|
onChangeMultiple={setValues}
|
||||||
>
|
>
|
||||||
<Option value="1">one</Option>
|
<Option value="1">one</Option>
|
||||||
<Option value="2">two</Option>
|
<Option value="2">two</Option>
|
||||||
<Option value="3">three</Option>
|
<Option value="3">three</Option>
|
||||||
</Select>
|
</Select>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertSelect(values: string[]) {
|
async function assertSelect(values: string[]) {
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "select",
|
type: "select",
|
||||||
placeholder: "select",
|
placeholder: "select",
|
||||||
values,
|
values,
|
||||||
minValues: 0,
|
minValues: 0,
|
||||||
maxValues: 25,
|
maxValues: 25,
|
||||||
options: [
|
options: [
|
||||||
{ label: "one", value: "1" },
|
{ label: "one", value: "1" },
|
||||||
{ label: "two", value: "2" },
|
{ label: "two", value: "2" },
|
||||||
{ label: "three", value: "3" },
|
{ label: "three", value: "3" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
await assertSelect([])
|
||||||
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
await tester.findSelectByPlaceholder("select").select("1", "3")
|
||||||
await assertSelect([])
|
await assertSelect(expect.arrayContaining(["1", "3"]) as unknown as string[])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
values: expect.arrayContaining(["1", "3"]) as unknown,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
await tester.findSelectByPlaceholder("select").select("1", "3")
|
await tester.findSelectByPlaceholder("select").select("2")
|
||||||
await assertSelect(expect.arrayContaining(["1", "3"]) as unknown as string[])
|
await assertSelect(expect.arrayContaining(["2"]) as unknown as string[])
|
||||||
expect(onSelect).toHaveBeenCalledWith(
|
expect(onSelect).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ values: expect.arrayContaining(["1", "3"]) }),
|
expect.objectContaining({
|
||||||
)
|
values: expect.arrayContaining(["2"]) as unknown,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
await tester.findSelectByPlaceholder("select").select("2")
|
await tester.findSelectByPlaceholder("select").select()
|
||||||
await assertSelect(expect.arrayContaining(["2"]) as unknown as string[])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledWith(
|
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ values: [] }))
|
||||||
expect.objectContaining({ values: expect.arrayContaining(["2"]) }),
|
|
||||||
)
|
|
||||||
|
|
||||||
await tester.findSelectByPlaceholder("select").select()
|
|
||||||
await assertSelect([])
|
|
||||||
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ values: [] }))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[{ type: "select", placeholder: "select", options: [], values: [] }],
|
[{ type: "select", placeholder: "select", options: [], values: [] }],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test.todo("select minValues and maxValues")
|
test.todo("select minValues and maxValues")
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
/* eslint-disable require-await */
|
|
||||||
import { logPretty } from "@reacord/helpers/log-pretty"
|
import { logPretty } from "@reacord/helpers/log-pretty"
|
||||||
import { omit } from "@reacord/helpers/omit"
|
import { omit } from "@reacord/helpers/omit"
|
||||||
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
|
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
|
||||||
@@ -10,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"
|
||||||
@@ -22,267 +21,273 @@ import { Reacord } from "../library/core/reacord"
|
|||||||
import type { Channel } from "../library/internal/channel"
|
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]
|
||||||
|
|
||||||
/**
|
/** A Record adapter for automated tests. WIP */
|
||||||
* A Record adapter for automated tests. WIP
|
|
||||||
*/
|
|
||||||
export class ReacordTester extends Reacord {
|
export class ReacordTester extends Reacord {
|
||||||
private messageContainer = new Container<TestMessage>()
|
private messageContainer = new Container<TestMessage>()
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ maxInstances: 2 })
|
super({ maxInstances: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
get messages(): readonly TestMessage[] {
|
get messages(): readonly TestMessage[] {
|
||||||
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(
|
return this.createInstance(
|
||||||
new InteractionReplyRenderer(
|
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||||
new TestCommandInteraction(this.messageContainer),
|
)
|
||||||
),
|
}
|
||||||
initialContent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
|
public createInteractionReply(
|
||||||
return this.reply(initialContent)
|
_options?: ComponentEventReplyOptions,
|
||||||
}
|
): ReacordInstance {
|
||||||
|
return this.createInstance(
|
||||||
|
new InteractionReplyRenderer(
|
||||||
|
new TestCommandInteraction(this.messageContainer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
assertMessages(expected: MessageSample[]) {
|
assertMessages(expected: MessageSample[]) {
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
expect(this.sampleMessages()).toEqual(expected)
|
expect(this.sampleMessages()).toEqual(expected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
logMessages() {
|
logMessages() {
|
||||||
logPretty(this.sampleMessages())
|
logPretty(this.sampleMessages())
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleMessages() {
|
sampleMessages() {
|
||||||
return pruneNullishValues(
|
return pruneNullishValues(
|
||||||
this.messages.map((message) => ({
|
this.messages.map((message) => ({
|
||||||
...message.options,
|
...message.options,
|
||||||
actionRows: message.options.actionRows.map((row) =>
|
actionRows: message.options.actionRows.map((row) =>
|
||||||
row.map((component) =>
|
row.map((component) =>
|
||||||
omit(component, [
|
omit(component, [
|
||||||
"customId",
|
"customId",
|
||||||
"onClick",
|
"onClick",
|
||||||
"onSelect",
|
"onSelect",
|
||||||
"onSelectValue",
|
"onSelectValue",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
findButtonByLabel(label: string) {
|
findButtonByLabel(label: string) {
|
||||||
return {
|
return {
|
||||||
click: () => {
|
click: () => {
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
for (const [component, message] of this.eachComponent()) {
|
for (const [component, message] of this.eachComponent()) {
|
||||||
if (component.type === "button" && component.label === label) {
|
if (component.type === "button" && component.label === label) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
new TestButtonInteraction(component.customId, message, this),
|
new TestButtonInteraction(component.customId, message, this),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
raise(`Couldn't find button with label "${label}"`)
|
raise(`Couldn't find button with label "${label}"`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findSelectByPlaceholder(placeholder: string) {
|
findSelectByPlaceholder(placeholder: string) {
|
||||||
return {
|
return {
|
||||||
select: (...values: string[]) => {
|
select: (...values: string[]) => {
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
for (const [component, message] of this.eachComponent()) {
|
for (const [component, message] of this.eachComponent()) {
|
||||||
if (
|
if (
|
||||||
component.type === "select" &&
|
component.type === "select" &&
|
||||||
component.placeholder === placeholder
|
component.placeholder === placeholder
|
||||||
) {
|
) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
new TestSelectInteraction(
|
new TestSelectInteraction(
|
||||||
component.customId,
|
component.customId,
|
||||||
message,
|
message,
|
||||||
values,
|
values,
|
||||||
this,
|
this,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createMessage(options: MessageOptions) {
|
createMessage(options: MessageOptions) {
|
||||||
return new TestMessage(options, this.messageContainer)
|
return new TestMessage(options, this.messageContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
private *eachComponent() {
|
private *eachComponent() {
|
||||||
for (const message of this.messageContainer) {
|
for (const message of this.messageContainer) {
|
||||||
for (const component of message.options.actionRows.flat()) {
|
for (const component of message.options.actionRows.flat()) {
|
||||||
yield [component, message] as const
|
yield [component, message] as const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestMessage implements Message {
|
class TestMessage implements Message {
|
||||||
constructor(
|
constructor(
|
||||||
public options: MessageOptions,
|
public options: MessageOptions,
|
||||||
private container: Container<TestMessage>,
|
private container: Container<TestMessage>,
|
||||||
) {
|
) {
|
||||||
container.add(this)
|
container.add(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit(options: MessageOptions): Promise<void> {
|
async edit(options: MessageOptions): Promise<void> {
|
||||||
this.options = options
|
this.options = options
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
async delete(): Promise<void> {
|
||||||
this.container.remove(this)
|
this.container.remove(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>) {}
|
||||||
|
|
||||||
async reply(messageOptions: MessageOptions): Promise<Message> {
|
async reply(messageOptions: MessageOptions): Promise<Message> {
|
||||||
await setTimeout()
|
await setTimeout()
|
||||||
return new TestMessage(messageOptions, this.messageContainer)
|
return new TestMessage(messageOptions, this.messageContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
async followUp(messageOptions: MessageOptions): Promise<Message> {
|
async followUp(messageOptions: MessageOptions): Promise<Message> {
|
||||||
await setTimeout()
|
await setTimeout()
|
||||||
return new TestMessage(messageOptions, this.messageContainer)
|
return new TestMessage(messageOptions, this.messageContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestInteraction {
|
class TestInteraction {
|
||||||
readonly id = randomUUID()
|
readonly id = randomUUID()
|
||||||
readonly channelId = "test-channel-id"
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly customId: string,
|
readonly customId: string,
|
||||||
readonly message: TestMessage,
|
readonly message: TestMessage,
|
||||||
private tester: ReacordTester,
|
private tester: ReacordTester,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async update(options: MessageOptions): Promise<void> {
|
async update(options: MessageOptions): Promise<void> {
|
||||||
this.message.options = options
|
this.message.options = options
|
||||||
}
|
}
|
||||||
|
|
||||||
async deferUpdate(): Promise<void> {}
|
async deferUpdate(): Promise<void> {}
|
||||||
|
|
||||||
async reply(messageOptions: MessageOptions): Promise<Message> {
|
async reply(messageOptions: MessageOptions): Promise<Message> {
|
||||||
return this.tester.createMessage(messageOptions)
|
return this.tester.createMessage(messageOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
async followUp(messageOptions: MessageOptions): Promise<Message> {
|
async followUp(messageOptions: MessageOptions): Promise<Message> {
|
||||||
return this.tester.createMessage(messageOptions)
|
return this.tester.createMessage(messageOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestButtonInteraction
|
class TestButtonInteraction
|
||||||
extends TestInteraction
|
extends TestInteraction
|
||||||
implements ButtonInteraction
|
implements ButtonInteraction
|
||||||
{
|
{
|
||||||
readonly type = "button"
|
readonly type = "button"
|
||||||
readonly event: ButtonClickEvent
|
readonly event: ButtonClickEvent
|
||||||
|
|
||||||
constructor(customId: string, message: TestMessage, tester: ReacordTester) {
|
constructor(customId: string, message: TestMessage, tester: ReacordTester) {
|
||||||
super(customId, message, tester)
|
super(customId, message, tester)
|
||||||
this.event = new TestButtonClickEvent(tester)
|
this.event = new TestButtonClickEvent(tester)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestSelectInteraction
|
class TestSelectInteraction
|
||||||
extends TestInteraction
|
extends TestInteraction
|
||||||
implements SelectInteraction
|
implements SelectInteraction
|
||||||
{
|
{
|
||||||
readonly type = "select"
|
readonly type = "select"
|
||||||
readonly event: SelectChangeEvent
|
readonly event: SelectChangeEvent
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
customId: string,
|
customId: string,
|
||||||
message: TestMessage,
|
message: TestMessage,
|
||||||
readonly values: string[],
|
readonly values: string[],
|
||||||
tester: ReacordTester,
|
tester: ReacordTester,
|
||||||
) {
|
) {
|
||||||
super(customId, message, tester)
|
super(customId, message, tester)
|
||||||
this.event = new TestSelectChangeEvent(values, tester)
|
this.event = new TestSelectChangeEvent(values, tester)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestComponentEvent {
|
class TestComponentEvent {
|
||||||
constructor(private tester: ReacordTester) {}
|
constructor(private tester: ReacordTester) {}
|
||||||
|
|
||||||
message: MessageInfo = {} as any // todo
|
message: ComponentEventMessage = {} as ComponentEventMessage // todo
|
||||||
channel: ChannelInfo = {} as any // todo
|
channel: ComponentEventChannel = {} as ComponentEventChannel // todo
|
||||||
user: UserInfo = {} as any // todo
|
user: ComponentEventUser = {} as ComponentEventUser // todo
|
||||||
guild: GuildInfo = {} as any // 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestButtonClickEvent
|
class TestButtonClickEvent
|
||||||
extends TestComponentEvent
|
extends TestComponentEvent
|
||||||
implements ButtonClickEvent {}
|
implements ButtonClickEvent {}
|
||||||
|
|
||||||
class TestSelectChangeEvent
|
class TestSelectChangeEvent
|
||||||
extends TestComponentEvent
|
extends TestComponentEvent
|
||||||
implements SelectChangeEvent
|
implements SelectChangeEvent
|
||||||
{
|
{
|
||||||
constructor(readonly values: string[], tester: ReacordTester) {
|
constructor(
|
||||||
super(tester)
|
readonly values: string[],
|
||||||
}
|
tester: ReacordTester,
|
||||||
|
) {
|
||||||
|
super(tester)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestChannel implements Channel {
|
class TestChannel implements Channel {
|
||||||
constructor(private messageContainer: Container<TestMessage>) {}
|
constructor(private messageContainer: Container<TestMessage>) {}
|
||||||
|
|
||||||
async send(messageOptions: MessageOptions): Promise<Message> {
|
async send(messageOptions: MessageOptions): Promise<Message> {
|
||||||
return new TestMessage(messageOptions, this.messageContainer)
|
return new TestMessage(messageOptions, this.messageContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,88 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Embed,
|
Embed,
|
||||||
EmbedAuthor,
|
EmbedAuthor,
|
||||||
EmbedField,
|
EmbedField,
|
||||||
EmbedFooter,
|
EmbedFooter,
|
||||||
EmbedTitle,
|
EmbedTitle,
|
||||||
Link,
|
Link,
|
||||||
Option,
|
Option,
|
||||||
Select,
|
Select,
|
||||||
} from "../library/main"
|
} from "../library/main"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
|
|
||||||
test("text children in other components", async () => {
|
test("text children in other components", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const SomeText = () => <>some text</>
|
const SomeText = () => <>some text</>
|
||||||
|
|
||||||
await tester.assertRender(
|
await tester.assertRender(
|
||||||
<>
|
<>
|
||||||
<Embed>
|
<Embed>
|
||||||
<EmbedTitle>
|
<EmbedTitle>
|
||||||
<SomeText />
|
<SomeText />
|
||||||
</EmbedTitle>
|
</EmbedTitle>
|
||||||
<EmbedAuthor>
|
<EmbedAuthor>
|
||||||
<SomeText />
|
<SomeText />
|
||||||
</EmbedAuthor>
|
</EmbedAuthor>
|
||||||
<EmbedField name={<SomeText />}>
|
<EmbedField name={<SomeText />}>
|
||||||
<SomeText /> <Button label="ignore this" onClick={() => {}} />
|
<SomeText /> <Button label="ignore this" onClick={() => {}} />
|
||||||
nailed it
|
nailed it
|
||||||
</EmbedField>
|
</EmbedField>
|
||||||
<EmbedFooter>
|
<EmbedFooter>
|
||||||
<SomeText />
|
<SomeText />
|
||||||
</EmbedFooter>
|
</EmbedFooter>
|
||||||
</Embed>
|
</Embed>
|
||||||
<Button label={<SomeText />} onClick={() => {}} />
|
<Button label={<SomeText />} onClick={() => {}} />
|
||||||
<Link url="https://discord.com" label={<SomeText />} />
|
<Link url="https://discord.com" label={<SomeText />} />
|
||||||
<Select>
|
<Select>
|
||||||
<Option value="1">
|
<Option value="1">
|
||||||
<SomeText />
|
<SomeText />
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="2" label={<SomeText />} description={<SomeText />} />
|
<Option value="2" label={<SomeText />} description={<SomeText />} />
|
||||||
</Select>
|
</Select>
|
||||||
</>,
|
</>,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: "some text",
|
title: "some text",
|
||||||
author: {
|
author: {
|
||||||
name: "some text",
|
name: "some text",
|
||||||
},
|
},
|
||||||
fields: [{ name: "some text", value: "some text nailed it" }],
|
fields: [{ name: "some text", value: "some text nailed it" }],
|
||||||
footer: {
|
footer: {
|
||||||
text: "some text",
|
text: "some text",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
label: "some text",
|
label: "some text",
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
url: "https://discord.com",
|
url: "https://discord.com",
|
||||||
label: "some text",
|
label: "some text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "select",
|
type: "select",
|
||||||
values: [],
|
values: [],
|
||||||
options: [
|
options: [
|
||||||
{ value: "1", label: "some text" },
|
{ value: "1", label: "some text" },
|
||||||
{ value: "2", label: "some text", description: "some text" },
|
{ value: "2", label: "some text", description: "some text" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,72 +1,73 @@
|
|||||||
import React from "react"
|
|
||||||
import { describe, expect, it } from "vitest"
|
|
||||||
import type { ReacordInstance } from "../library/main"
|
import type { ReacordInstance } from "../library/main"
|
||||||
import { Button, useInstance } from "../library/main"
|
import { Button, useInstance } from "../library/main"
|
||||||
import type { MessageSample } from "./test-adapter"
|
import type { MessageSample } from "./test-adapter"
|
||||||
import { ReacordTester } from "./test-adapter"
|
import { ReacordTester } from "./test-adapter"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
describe("useInstance", () => {
|
describe("useInstance", () => {
|
||||||
it("returns the instance of itself", async () => {
|
it("returns the instance of itself", async () => {
|
||||||
let instanceFromHook: ReacordInstance | undefined
|
let instanceFromHook: ReacordInstance | undefined
|
||||||
|
|
||||||
function TestComponent({ name }: { name: string }) {
|
function TestComponent({ name }: { name: string }) {
|
||||||
const instance = useInstance()
|
const instance = useInstance()
|
||||||
instanceFromHook ??= instance
|
instanceFromHook ??= instance
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label={`create ${name}`}
|
label={`create ${name}`}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.reply(<TestComponent name="child" />)
|
event.reply(<TestComponent name="child" />)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label={`destroy ${name}`}
|
label={`destroy ${name}`}
|
||||||
onClick={() => instance.destroy()}
|
onClick={() => instance.destroy()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function messageOutput(name: string): MessageSample {
|
function messageOutput(name: string): MessageSample {
|
||||||
return {
|
return {
|
||||||
content: "",
|
content: "",
|
||||||
embeds: [],
|
embeds: [],
|
||||||
actionRows: [
|
actionRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
label: `create ${name}`,
|
label: `create ${name}`,
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
label: `destroy ${name}`,
|
label: `destroy ${name}`,
|
||||||
style: "secondary",
|
style: "secondary",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
await tester.findButtonByLabel("create parent").click()
|
await tester.findButtonByLabel("create parent").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
messageOutput("parent"),
|
messageOutput("parent"),
|
||||||
messageOutput("child"),
|
messageOutput("child"),
|
||||||
])
|
])
|
||||||
|
|
||||||
// this test ensures that the only the child instance is destroyed,
|
// this test ensures that the only the child instance is destroyed,
|
||||||
// and not the parent instance
|
// and not the parent instance
|
||||||
await tester.findButtonByLabel("destroy child").click()
|
await tester.findButtonByLabel("destroy child").click()
|
||||||
await tester.assertMessages([messageOutput("parent")])
|
await tester.assertMessages([messageOutput("parent")])
|
||||||
|
|
||||||
await tester.findButtonByLabel("destroy parent").click()
|
await tester.findButtonByLabel("destroy parent").click()
|
||||||
await tester.assertMessages([])
|
await tester.assertMessages([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
10
packages/reacord/tsconfig.build.json
Normal file
10
packages/reacord/tsconfig.build.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"include": ["**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"]
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/website/.gitignore
vendored
11
packages/website/.gitignore
vendored
@@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
/.cache
|
|
||||||
/build
|
|
||||||
/public/build
|
|
||||||
.env
|
|
||||||
/public/api
|
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
*.out.css
|
|
||||||
/api
|
|
||||||
.astro
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# website
|
|
||||||
|
|
||||||
## 0.4.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [9813a01]
|
|
||||||
- reacord@0.5.2
|
|
||||||
|
|
||||||
## 0.4.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [72f4a4a]
|
|
||||||
- Updated dependencies [7536bde]
|
|
||||||
- Updated dependencies [e335165]
|
|
||||||
- reacord@0.5.1
|
|
||||||
|
|
||||||
## 0.4.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [aa65da5]
|
|
||||||
- reacord@0.5.0
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import prefetch from "@astrojs/prefetch"
|
|
||||||
import react from "@astrojs/react"
|
|
||||||
import tailwind from "@astrojs/tailwind"
|
|
||||||
import { defineConfig } from "astro/config"
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
integrations: [
|
|
||||||
tailwind({
|
|
||||||
config: {
|
|
||||||
applyBaseStyles: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
react(),
|
|
||||||
prefetch(),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"name": "website",
|
|
||||||
"version": "0.4.3",
|
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "run-p --race --print-label dev:*",
|
|
||||||
"dev:typedoc": "typedoc --watch",
|
|
||||||
"dev:astro": "astro dev",
|
|
||||||
"test": "node ./scripts/test.js",
|
|
||||||
"test-dev": "run-p --race --print-label dev:* test-dev:*",
|
|
||||||
"test-dev:cypress": "wait-on http-get://localhost:3000 && cypress open",
|
|
||||||
"start": "astro preview",
|
|
||||||
"build": "typedoc && astro build",
|
|
||||||
"typecheck": "tsc --noEmit && tsc --project cypress/tsconfig.json --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/prefetch": "^0.2.0",
|
|
||||||
"@astrojs/react": "^2.1.0",
|
|
||||||
"@fontsource/jetbrains-mono": "^4.5.12",
|
|
||||||
"@fontsource/rubik": "^4.5.14",
|
|
||||||
"@heroicons/react": "^2.0.16",
|
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"astro": "^2.1.2",
|
|
||||||
"clsx": "^1.2.1",
|
|
||||||
"reacord": "workspace:*",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@astrojs/tailwind": "^3.1.0",
|
|
||||||
"@types/node": "*",
|
|
||||||
"@types/react": "^18.0.28",
|
|
||||||
"@types/react-dom": "^18.0.11",
|
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"tailwindcss": "^3.2.7",
|
|
||||||
"typedoc": "^0.23.26",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"wait-on": "^7.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="53" height="35" viewBox="0 0 53 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="3" cy="3" r="1" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 146 B |
Binary file not shown.
|
Before Width: | Height: | Size: 658 B |
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
import { HeartIcon } from "@heroicons/react/20/solid"
|
|
||||||
import ExternalLink from "./external-link.astro"
|
|
||||||
---
|
|
||||||
|
|
||||||
<footer class="container text-xs opacity-75">
|
|
||||||
<address class="not-italic">
|
|
||||||
© {new Date().getFullYear()} itsMapleLeaf
|
|
||||||
</address>
|
|
||||||
<p>
|
|
||||||
Coded with <HeartIcon className="inline w-4 align-sub" /> using{" "}
|
|
||||||
<ExternalLink class="link" href="https://astro.build">Astro</ExternalLink>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user