Compare commits
113 Commits
reacord@0.
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9594542869 | ||
|
|
17978a5252 | ||
|
|
95fb342183 | ||
|
|
0772ca4502 | ||
|
|
11153dfe0f | ||
|
|
fb0a997855 | ||
|
|
da1c62f2f0 | ||
|
|
cdc22b7916 | ||
|
|
7fee69c8ae | ||
|
|
c2e5dc04dd | ||
|
|
390da4cab6 | ||
|
|
def0c46f13 | ||
|
|
8b6e283810 | ||
|
|
13fcf7ddc9 | ||
|
|
ce12351a24 | ||
|
|
73bb098774 | ||
|
|
4ee4d4ab91 | ||
|
|
f998a0e09a | ||
|
|
453192cc96 | ||
|
|
d387f669ab | ||
|
|
9aec87ae9f | ||
|
|
65d1d68bb0 | ||
|
|
dfb7562c97 | ||
|
|
9e2be6c2e0 | ||
|
|
d078995516 | ||
|
|
82b3575f2d | ||
|
|
82b811c98b | ||
|
|
3a786310b1 | ||
|
|
ced48a3ecb | ||
|
|
37b75a99e2 | ||
|
|
f2f215d6b9 | ||
|
|
1f67e7c263 | ||
|
|
d4f1bb4d4b | ||
|
|
47c9b75940 | ||
|
|
41c87e3dcc | ||
|
|
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 | ||
|
|
69b3112d32 | ||
|
|
b4fb6bc47c | ||
|
|
7aaef5f85f | ||
|
|
b755290569 | ||
|
|
f0ad743080 | ||
|
|
af3d1c5058 | ||
|
|
bdee9454f2 | ||
|
|
84348d287f | ||
|
|
6da6008d2c | ||
|
|
bece6c42fc | ||
|
|
abc809c9fb | ||
|
|
d35659f6f6 | ||
|
|
3969e6471f | ||
|
|
95041acfd4 | ||
|
|
eb0409f1a2 | ||
|
|
fbab145f39 | ||
|
|
f59323f245 | ||
|
|
1c37d37c28 | ||
|
|
408ab4ce89 | ||
|
|
a8a64e601a | ||
|
|
d88b982830 | ||
|
|
d87c27183a | ||
|
|
b141ca1868 | ||
|
|
dfa7f8090c | ||
|
|
82068d2d83 | ||
|
|
216ae7a29a | ||
|
|
9813a01a19 | ||
|
|
0be321b64e | ||
|
|
acbf21842f | ||
|
|
65be2ef201 | ||
|
|
c244124f6f | ||
|
|
4db8db0f2d | ||
|
|
1fa4bc800b | ||
|
|
e3351654ea | ||
|
|
d1ca002939 | ||
|
|
38a86bb783 | ||
|
|
72f4a4afff | ||
|
|
eed5715f1f | ||
|
|
e486da0881 | ||
|
|
b275d9b330 | ||
|
|
bab134d697 | ||
|
|
df9bdfaf77 | ||
|
|
35d7f0b33f | ||
|
|
4f9fb4310f | ||
|
|
7b74628732 | ||
|
|
7536bdee43 | ||
|
|
ef8d915e3b |
5
.changeset/five-wolves-destroy.md
Normal file
5
.changeset/five-wolves-destroy.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"reacord": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: more descriptive component event types
|
||||||
33
.changeset/many-pets-melt.md
Normal file
33
.changeset/many-pets-melt.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
"reacord": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add new descriptive adapter methods
|
||||||
|
|
||||||
|
The reacord instance names have been updated, and the old names are now deprecated.
|
||||||
|
|
||||||
|
- `send` -> `createChannelMessage`
|
||||||
|
- `reply` -> `createInteractionReply`
|
||||||
|
|
||||||
|
These new methods also accept discord JS options. Usage example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// can accept either a channel object or a channel ID
|
||||||
|
reacord.createChannelMessage(channel)
|
||||||
|
reacord.createChannelMessage(channel, {
|
||||||
|
tts: true,
|
||||||
|
})
|
||||||
|
reacord.createChannelMessage(channel, {
|
||||||
|
reply: {
|
||||||
|
messageReference: "123456789012345678",
|
||||||
|
failIfNotExists: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
reacord.createInteractionReply(interaction)
|
||||||
|
reacord.createInteractionReply(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
These new methods replace the old ones, which are now deprecated.
|
||||||
@@ -1,25 +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"),
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["packages/website/cypress/**"],
|
|
||||||
parserOptions: {
|
|
||||||
project: require.resolve("./packages/website/cypress/tsconfig.json"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
45
.github/workflows/main.yml
vendored
45
.github/workflows/main.yml
vendored
@@ -1,45 +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:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
# https://github.com/actions/setup-node#supported-version-syntax
|
|
||||||
node-version: "16"
|
|
||||||
- run: npm i -g pnpm@7.5.0
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: ${{ matrix.command.run }}
|
|
||||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -13,19 +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@v2
|
- uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
- name: setup node
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
version: 8
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
- name: install pnpm
|
with:
|
||||||
run: npm install -g pnpm
|
node-version: 18
|
||||||
|
cache: pnpm
|
||||||
- name: install deps
|
- run: pnpm install --frozen-lockfile
|
||||||
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()
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,6 +5,7 @@ coverage
|
|||||||
.env
|
.env
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
|
|
||||||
build
|
build
|
||||||
.cache
|
.cache
|
||||||
|
.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,5 +1,5 @@
|
|||||||
<center>
|
<center>
|
||||||
<img src="./packages/website/app/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
|
<img src="packages/website/src/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
|
||||||
</center>
|
</center>
|
||||||
|
|
||||||
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
||||||
|
|||||||
48
package.json
48
package.json
@@ -2,25 +2,43 @@
|
|||||||
"name": "reacord-monorepo",
|
"name": "reacord-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"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",
|
||||||
|
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
||||||
|
"astro-sync": "pnpm --filter website exec astro sync",
|
||||||
|
"test": "vitest",
|
||||||
"build": "pnpm -r run build",
|
"build": "pnpm -r run build",
|
||||||
|
"build:website": "pnpm --filter website... run build",
|
||||||
"start": "pnpm -C packages/website run start",
|
"start": "pnpm -C packages/website run start",
|
||||||
|
"start:website": "pnpm -C packages/website run start",
|
||||||
"release": "pnpm -r run build && changeset publish"
|
"release": "pnpm -r run build && changeset publish"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.24.0",
|
"@changesets/cli": "^2.26.2",
|
||||||
"@itsmapleleaf/configs": "^1.1.3",
|
"@itsmapleleaf/configs": "github:itsMapleLeaf/configs",
|
||||||
"@rushstack/eslint-patch": "^1.1.3",
|
"eslint": "^8.51.0",
|
||||||
"@types/eslint": "^8.4.1",
|
"npm-run-all": "^4.1.5",
|
||||||
"eslint": "^8.14.0",
|
"prettier": "^3.0.3",
|
||||||
"node": "^16",
|
"react": "^18.2.0",
|
||||||
"prettier": "^2.6.2",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^5.2.2",
|
||||||
|
"vitest": "^0.34.6"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"prettier": "@itsmapleleaf/configs/prettier",
|
||||||
"esbuild": "latest"
|
"eslintConfig": {
|
||||||
},
|
"extends": [
|
||||||
"prettier": "@itsmapleleaf/configs/prettier"
|
"./node_modules/@itsmapleleaf/configs/eslint.config.cjs",
|
||||||
|
"./node_modules/@itsmapleleaf/configs/eslint.config.react.cjs"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"packages/website/public/api"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/require-await": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
packages/helpers/convert-object-property-case.test.ts
Normal file
42
packages/helpers/convert-object-property-case.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
|
||||||
|
import type {
|
||||||
|
CamelCasedPropertiesDeep,
|
||||||
|
SnakeCasedPropertiesDeep,
|
||||||
|
} from "type-fest"
|
||||||
|
import { expect, test } from "vitest"
|
||||||
|
|
||||||
|
test("camelCaseDeep", () => {
|
||||||
|
const input = {
|
||||||
|
some_prop: {
|
||||||
|
some_deep_prop: "some_deep_value",
|
||||||
|
},
|
||||||
|
someOtherProp: "someOtherValue",
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected: CamelCasedPropertiesDeep<typeof input> = {
|
||||||
|
someProp: {
|
||||||
|
someDeepProp: "some_deep_value",
|
||||||
|
},
|
||||||
|
someOtherProp: "someOtherValue",
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(camelCaseDeep(input)).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("snakeCaseDeep", () => {
|
||||||
|
const input = {
|
||||||
|
someProp: {
|
||||||
|
someDeepProp: "someDeepValue",
|
||||||
|
},
|
||||||
|
some_other_prop: "someOtherValue",
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected: SnakeCasedPropertiesDeep<typeof input> = {
|
||||||
|
some_prop: {
|
||||||
|
some_deep_prop: "someDeepValue",
|
||||||
|
},
|
||||||
|
some_other_prop: "someOtherValue",
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(snakeCaseDeep(input)).toEqual(expected)
|
||||||
|
})
|
||||||
35
packages/helpers/convert-object-property-case.ts
Normal file
35
packages/helpers/convert-object-property-case.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { camelCase, isObject, snakeCase } from "lodash-es"
|
||||||
|
import type {
|
||||||
|
CamelCasedPropertiesDeep,
|
||||||
|
SnakeCasedPropertiesDeep,
|
||||||
|
UnknownRecord,
|
||||||
|
} from "type-fest"
|
||||||
|
|
||||||
|
function convertKeyCaseDeep<Input, Output>(
|
||||||
|
input: Input,
|
||||||
|
convertKey: (key: string) => string,
|
||||||
|
): Output {
|
||||||
|
if (!isObject(input)) {
|
||||||
|
return input as unknown as Output
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return input.map((item) =>
|
||||||
|
convertKeyCaseDeep(item, convertKey),
|
||||||
|
) as unknown as Output
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = {} as UnknownRecord
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
|
||||||
|
}
|
||||||
|
return output as Output
|
||||||
|
}
|
||||||
|
|
||||||
|
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {
|
||||||
|
return convertKeyCaseDeep(input, camelCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snakeCaseDeep<T>(input: T): SnakeCasedPropertiesDeep<T> {
|
||||||
|
return convertKeyCaseDeep(input, snakeCase)
|
||||||
|
}
|
||||||
7
packages/helpers/is-instance-of.ts
Normal file
7
packages/helpers/is-instance-of.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** For narrowing instance types with array.filter */
|
||||||
|
export const isInstanceOf =
|
||||||
|
<Instance, Args extends unknown[]>(
|
||||||
|
constructor: new (...args: Args) => Instance,
|
||||||
|
) =>
|
||||||
|
(value: unknown): value is Instance =>
|
||||||
|
value instanceof constructor
|
||||||
3
packages/helpers/is-object.ts
Normal file
3
packages/helpers/is-object.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function isObject(value: unknown): value is object {
|
||||||
|
return typeof value === "object" && value !== null
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/helpers/log-pretty.ts
Normal file
11
packages/helpers/log-pretty.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { inspect } from "node:util"
|
||||||
|
|
||||||
|
export function logPretty(value: unknown) {
|
||||||
|
console.info(
|
||||||
|
inspect(value, {
|
||||||
|
// depth: Number.POSITIVE_INFINITY,
|
||||||
|
depth: 10,
|
||||||
|
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 }
|
||||||
10
packages/helpers/omit.ts
Normal file
10
packages/helpers/omit.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function omit<Subject extends object, Key extends PropertyKey>(
|
||||||
|
subject: Subject,
|
||||||
|
keys: Key[],
|
||||||
|
) {
|
||||||
|
const keySet = new Set<PropertyKey>(keys)
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(subject).filter(([key]) => !keySet.has(key)),
|
||||||
|
// hack: conditional type preserves unions
|
||||||
|
) as Subject extends unknown ? Omit<Subject, Key> : never
|
||||||
|
}
|
||||||
15
packages/helpers/package.json
Normal file
15
packages/helpers/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@reacord/helpers",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc -b"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash-es": "^4.17.9",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"type-fest": "^4.4.0",
|
||||||
|
"vitest": "^0.34.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/helpers/pick.ts
Normal file
11
packages/helpers/pick.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { LoosePick } from "./types"
|
||||||
|
|
||||||
|
export function pick<T extends object, K extends keyof T | PropertyKey>(
|
||||||
|
object: T,
|
||||||
|
keys: K[],
|
||||||
|
) {
|
||||||
|
const keySet = new Set<PropertyKey>(keys)
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(object).filter(([key]) => keySet.has(key)),
|
||||||
|
) as LoosePick<T, K>
|
||||||
|
}
|
||||||
34
packages/helpers/prune-nullish-values.test.ts
Normal file
34
packages/helpers/prune-nullish-values.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { expect, test } from "vitest"
|
||||||
|
import type { PruneNullishValues } from "./prune-nullish-values"
|
||||||
|
import { pruneNullishValues } from "./prune-nullish-values"
|
||||||
|
|
||||||
|
test("pruneNullishValues", () => {
|
||||||
|
interface InputType {
|
||||||
|
a: string
|
||||||
|
b: string | null | undefined
|
||||||
|
c?: string
|
||||||
|
d: {
|
||||||
|
a: string
|
||||||
|
b: string | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: InputType = {
|
||||||
|
a: "a",
|
||||||
|
b: null,
|
||||||
|
c: undefined,
|
||||||
|
d: {
|
||||||
|
a: "a",
|
||||||
|
b: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: PruneNullishValues<InputType> = {
|
||||||
|
a: "a",
|
||||||
|
d: {
|
||||||
|
a: "a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(pruneNullishValues(input)).toEqual(output)
|
||||||
|
})
|
||||||
46
packages/helpers/prune-nullish-values.ts
Normal file
46
packages/helpers/prune-nullish-values.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { isObject } from "./is-object"
|
||||||
|
|
||||||
|
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
|
||||||
|
if (!isObject(input)) {
|
||||||
|
return input as PruneNullishValues<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return input
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(
|
||||||
|
(item) => pruneNullishValues(item) as unknown,
|
||||||
|
) as PruneNullishValues<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, unknown> = {}
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
if (value != undefined) {
|
||||||
|
result[key] = pruneNullishValues(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result as PruneNullishValues<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PruneNullishValues<Input> = Input extends object
|
||||||
|
? OptionalKeys<
|
||||||
|
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
|
||||||
|
KeysWithNullishValues<Input>
|
||||||
|
>
|
||||||
|
: Input
|
||||||
|
|
||||||
|
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
|
||||||
|
[Key in Keys]?: Input[Key]
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeysWithNullishValues<Input> = NonNullable<
|
||||||
|
Values<{
|
||||||
|
[Key in keyof Input]: null extends Input[Key]
|
||||||
|
? Key
|
||||||
|
: undefined extends Input[Key]
|
||||||
|
? Key
|
||||||
|
: never
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
|
||||||
|
type Values<Input> = Input[keyof Input]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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,
|
||||||
21
packages/helpers/retry-with-timeout.ts
Normal file
21
packages/helpers/retry-with-timeout.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
|
||||||
|
const maxTime = 500
|
||||||
|
const waitPeriod = 50
|
||||||
|
|
||||||
|
export async function retryWithTimeout<T>(
|
||||||
|
callback: () => Promise<T> | T,
|
||||||
|
): Promise<T> {
|
||||||
|
const startTime = Date.now()
|
||||||
|
// eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await callback()
|
||||||
|
} catch (error) {
|
||||||
|
if (Date.now() - startTime > maxTime) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
await setTimeout(waitPeriod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/helpers/to-error.ts
Normal file
3
packages/helpers/to-error.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function toError(value: unknown) {
|
||||||
|
return value instanceof Error ? value : new Error(String(value))
|
||||||
|
}
|
||||||
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)
|
||||||
21
packages/helpers/types.ts
Normal file
21
packages/helpers/types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { raise } from "./raise.ts"
|
||||||
|
|
||||||
|
export type MaybePromise<T> = T | PromiseLike<T>
|
||||||
|
|
||||||
|
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
|
||||||
|
? Value
|
||||||
|
: Type[keyof Type]
|
||||||
|
|
||||||
|
export type LoosePick<Shape, Keys extends PropertyKey> = Simplify<{
|
||||||
|
[Key in Extract<Keys, keyof Shape>]: Shape[Key]
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type LooseOmit<Shape, Keys extends PropertyKey> = Simplify<{
|
||||||
|
[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")
|
||||||
23
packages/helpers/wait-for.ts
Normal file
23
packages/helpers/wait-for.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
import type { MaybePromise } from "./types.ts"
|
||||||
|
|
||||||
|
const maxTime = 1000
|
||||||
|
|
||||||
|
export async function waitFor<Result>(
|
||||||
|
predicate: () => MaybePromise<Result>,
|
||||||
|
): Promise<Awaited<Result>> {
|
||||||
|
const startTime = Date.now()
|
||||||
|
let lastError: unknown
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxTime) {
|
||||||
|
try {
|
||||||
|
return await predicate()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
await setTimeout(50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
|
throw lastError ?? new Error("Timeout")
|
||||||
|
}
|
||||||
24
packages/helpers/with-logged-method-calls.ts
Normal file
24
packages/helpers/with-logged-method-calls.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { inspect } from "node:util"
|
||||||
|
|
||||||
|
export function withLoggedMethodCalls<T extends object>(value: T) {
|
||||||
|
return new Proxy(value as Record<string | symbol, unknown>, {
|
||||||
|
get(target, property) {
|
||||||
|
const value = target[property]
|
||||||
|
if (typeof value !== "function") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return (...values: unknown[]) => {
|
||||||
|
console.info(
|
||||||
|
`${String(property)}(${values
|
||||||
|
.map((value) =>
|
||||||
|
typeof value === "object" && value !== null
|
||||||
|
? value.constructor.name
|
||||||
|
: inspect(value, { colors: true }),
|
||||||
|
)
|
||||||
|
.join(", ")})`,
|
||||||
|
)
|
||||||
|
return value.apply(target, values) as unknown
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) as T
|
||||||
|
}
|
||||||
@@ -1,5 +1,48 @@
|
|||||||
# reacord
|
# reacord
|
||||||
|
|
||||||
|
## 0.5.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- ced48a3: distribute d.ts files again instead of the source
|
||||||
|
|
||||||
|
distributing the source causes typecheck errors when the modules it imports from (in this case, `@reacord/helpers`) don't exist in the end users' projects, so we'll just distribute d.ts files instead like normal. failed experiment :(
|
||||||
|
|
||||||
|
## 0.5.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 41c87e3: fix type definitions
|
||||||
|
|
||||||
|
`"types"` wasn't updated, oops!
|
||||||
|
|
||||||
|
technically the typedefs were already correctly defined via `"exports"`, but this may not be picked up depending on the tsconfig, so I'll ensure both are used for compatibility purposes. but this might be worth a note in the docs; pretty much every modern TS Node project should be using a tsconfig that doesn't require setting `"types"`
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 9813a01: import react-reconciler/constants.js for esm
|
||||||
|
|
||||||
|
ESM projects which tried to import reacord would fail due to the lack of .js on this import
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 72f4a4a: upgrade dependencies and remove some unneeded
|
||||||
|
- 7536bde: add types in exports to work with TS nodenext
|
||||||
|
- e335165: fix links
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor 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,42 +0,0 @@
|
|||||||
import type {
|
|
||||||
CamelCasedPropertiesDeep,
|
|
||||||
SnakeCasedPropertiesDeep,
|
|
||||||
} from "type-fest"
|
|
||||||
import { expect, test } from "vitest"
|
|
||||||
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
|
|
||||||
|
|
||||||
test("camelCaseDeep", () => {
|
|
||||||
const input = {
|
|
||||||
some_prop: {
|
|
||||||
some_deep_prop: "some_deep_value",
|
|
||||||
},
|
|
||||||
someOtherProp: "someOtherValue",
|
|
||||||
}
|
|
||||||
|
|
||||||
const expected: CamelCasedPropertiesDeep<typeof input> = {
|
|
||||||
someProp: {
|
|
||||||
someDeepProp: "some_deep_value",
|
|
||||||
},
|
|
||||||
someOtherProp: "someOtherValue",
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(camelCaseDeep(input)).toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("snakeCaseDeep", () => {
|
|
||||||
const input = {
|
|
||||||
someProp: {
|
|
||||||
someDeepProp: "someDeepValue",
|
|
||||||
},
|
|
||||||
some_other_prop: "someOtherValue",
|
|
||||||
}
|
|
||||||
|
|
||||||
const expected: SnakeCasedPropertiesDeep<typeof input> = {
|
|
||||||
some_prop: {
|
|
||||||
some_deep_prop: "someDeepValue",
|
|
||||||
},
|
|
||||||
some_other_prop: "someOtherValue",
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(snakeCaseDeep(input)).toEqual(expected)
|
|
||||||
})
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { camelCase, isObject, snakeCase } from "lodash-es"
|
|
||||||
import type {
|
|
||||||
CamelCasedPropertiesDeep,
|
|
||||||
SnakeCasedPropertiesDeep,
|
|
||||||
} from "type-fest"
|
|
||||||
|
|
||||||
function convertKeyCaseDeep<Input, Output>(
|
|
||||||
input: Input,
|
|
||||||
convertKey: (key: string) => string,
|
|
||||||
): Output {
|
|
||||||
if (!isObject(input)) {
|
|
||||||
return input as unknown as Output
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(input)) {
|
|
||||||
return input.map((item) =>
|
|
||||||
convertKeyCaseDeep(item, convertKey),
|
|
||||||
) as unknown as Output
|
|
||||||
}
|
|
||||||
|
|
||||||
const output: any = {}
|
|
||||||
for (const [key, value] of Object.entries(input)) {
|
|
||||||
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {
|
|
||||||
return convertKeyCaseDeep(input, camelCase)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function snakeCaseDeep<T>(input: T): SnakeCasedPropertiesDeep<T> {
|
|
||||||
return convertKeyCaseDeep(input, snakeCase)
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* for narrowing instance types with array.filter
|
|
||||||
*/
|
|
||||||
export const isInstanceOf =
|
|
||||||
<T>(Constructor: new (...args: any[]) => T) =>
|
|
||||||
(value: unknown): value is T =>
|
|
||||||
value instanceof Constructor
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export function isObject<T>(
|
|
||||||
value: T,
|
|
||||||
): value is Exclude<T, Primitive | AnyFunction> {
|
|
||||||
return typeof value === "object" && value !== null
|
|
||||||
}
|
|
||||||
type Primitive = string | number | boolean | undefined | null
|
|
||||||
type AnyFunction = (...args: any[]) => any
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { inspect } from "node:util"
|
|
||||||
|
|
||||||
export function logPretty(value: unknown) {
|
|
||||||
console.info(
|
|
||||||
inspect(value, {
|
|
||||||
// depth: Number.POSITIVE_INFINITY,
|
|
||||||
depth: 10,
|
|
||||||
colors: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export function omit<Subject extends object, Key extends PropertyKey>(
|
|
||||||
subject: Subject,
|
|
||||||
keys: Key[],
|
|
||||||
// hack: using a conditional type preserves union types
|
|
||||||
): Subject extends any ? Omit<Subject, Key> : never {
|
|
||||||
const result: any = {}
|
|
||||||
for (const key in subject) {
|
|
||||||
if (!keys.includes(key as unknown as Key)) {
|
|
||||||
result[key] = subject[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { LoosePick, UnknownRecord } from "./types"
|
|
||||||
|
|
||||||
export function pick<T, K extends keyof T | PropertyKey>(
|
|
||||||
object: T,
|
|
||||||
keys: K[],
|
|
||||||
): LoosePick<T, K> {
|
|
||||||
const result: any = {}
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = (object as UnknownRecord)[key]
|
|
||||||
if (value !== undefined) {
|
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { expect, test } from "vitest"
|
|
||||||
import type { PruneNullishValues } from "./prune-nullish-values"
|
|
||||||
import { pruneNullishValues } from "./prune-nullish-values"
|
|
||||||
|
|
||||||
test("pruneNullishValues", () => {
|
|
||||||
type InputType = {
|
|
||||||
a: string
|
|
||||||
b: string | null | undefined
|
|
||||||
c?: string
|
|
||||||
d: {
|
|
||||||
a: string
|
|
||||||
b: string | undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const input: InputType = {
|
|
||||||
a: "a",
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
|
||||||
b: null,
|
|
||||||
c: undefined,
|
|
||||||
d: {
|
|
||||||
a: "a",
|
|
||||||
b: undefined,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const output: PruneNullishValues<InputType> = {
|
|
||||||
a: "a",
|
|
||||||
d: {
|
|
||||||
a: "a",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(pruneNullishValues(input)).toEqual(output)
|
|
||||||
})
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { isObject } from "./is-object"
|
|
||||||
|
|
||||||
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
|
|
||||||
if (Array.isArray(input)) {
|
|
||||||
return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isObject(input)) {
|
|
||||||
return input as any
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: any = {}
|
|
||||||
for (const [key, value] of Object.entries(input)) {
|
|
||||||
if (value != undefined) {
|
|
||||||
result[key] = pruneNullishValues(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PruneNullishValues<Input> = Input extends object
|
|
||||||
? OptionalKeys<
|
|
||||||
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
|
|
||||||
KeysWithNullishValues<Input>
|
|
||||||
>
|
|
||||||
: Input
|
|
||||||
|
|
||||||
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
|
|
||||||
[Key in Keys]?: Input[Key]
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeysWithNullishValues<Input> = NonNullable<
|
|
||||||
Values<{
|
|
||||||
[Key in keyof Input]: null extends Input[Key]
|
|
||||||
? Key
|
|
||||||
: undefined extends Input[Key]
|
|
||||||
? Key
|
|
||||||
: never
|
|
||||||
}>
|
|
||||||
>
|
|
||||||
|
|
||||||
type Values<Input> = Input[keyof Input]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { setTimeout } from "node:timers/promises"
|
|
||||||
|
|
||||||
const maxTime = 500
|
|
||||||
const waitPeriod = 50
|
|
||||||
|
|
||||||
export async function retryWithTimeout<T>(
|
|
||||||
callback: () => Promise<T> | T,
|
|
||||||
): Promise<T> {
|
|
||||||
const startTime = Date.now()
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
return await callback()
|
|
||||||
} catch (error) {
|
|
||||||
if (Date.now() - startTime > maxTime) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
await setTimeout(waitPeriod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function toError(value: unknown) {
|
|
||||||
return value instanceof Error ? value : new Error(String(value))
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export type MaybePromise<T> = T | Promise<T>
|
|
||||||
|
|
||||||
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
|
|
||||||
? Value
|
|
||||||
: Type[keyof Type]
|
|
||||||
|
|
||||||
export type UnknownRecord = Record<PropertyKey, unknown>
|
|
||||||
|
|
||||||
export type LoosePick<Shape, Keys extends PropertyKey> = {
|
|
||||||
[Key in Keys]: Shape extends Record<Key, infer Value> ? Value : never
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { setTimeout } from "node:timers/promises"
|
|
||||||
|
|
||||||
const maxTime = 1000
|
|
||||||
|
|
||||||
export async function waitFor<Result>(
|
|
||||||
predicate: () => Result,
|
|
||||||
): Promise<Awaited<Result>> {
|
|
||||||
const startTime = Date.now()
|
|
||||||
let lastError: unknown
|
|
||||||
|
|
||||||
while (Date.now() - startTime < maxTime) {
|
|
||||||
try {
|
|
||||||
return await predicate()
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error
|
|
||||||
await setTimeout(50)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastError ?? new Error("Timeout")
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { inspect } from "node:util"
|
|
||||||
|
|
||||||
export function withLoggedMethodCalls<T extends object>(value: T) {
|
|
||||||
return new Proxy(value as Record<string | symbol, unknown>, {
|
|
||||||
get(target, property) {
|
|
||||||
const value = target[property]
|
|
||||||
if (typeof value !== "function") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return (...values: any[]) => {
|
|
||||||
console.info(
|
|
||||||
`${String(property)}(${values
|
|
||||||
.map((value) =>
|
|
||||||
typeof value === "object" && value !== null
|
|
||||||
? value.constructor.name
|
|
||||||
: inspect(value, { colors: true }),
|
|
||||||
)
|
|
||||||
.join(", ")})`,
|
|
||||||
)
|
|
||||||
return value.apply(target, values)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) as T
|
|
||||||
}
|
|
||||||
@@ -1,52 +1,60 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import type { ReacordInstance } from "./instance"
|
import type { ReacordInstance } from "./instance"
|
||||||
|
|
||||||
|
/** @category Component Event */
|
||||||
|
export interface ComponentEvent {
|
||||||
/**
|
/**
|
||||||
* @category Component Event
|
* The message associated with this event. For example: with a button click,
|
||||||
*/
|
|
||||||
export type ComponentEvent = {
|
|
||||||
/**
|
|
||||||
* The message associated with this event.
|
|
||||||
* For example: with a button click,
|
|
||||||
* this is the message that the button is on.
|
* this is the message that the button is on.
|
||||||
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/channel#message-object
|
* @see https://discord.com/developers/docs/resources/channel#message-object
|
||||||
*/
|
*/
|
||||||
message: MessageInfo
|
message: ComponentEventMessage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The channel that this event occurred in.
|
* The channel that this event occurred in.
|
||||||
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/channel#channel-object
|
* @see https://discord.com/developers/docs/resources/channel#channel-object
|
||||||
*/
|
*/
|
||||||
channel: ChannelInfo
|
channel: ComponentEventChannel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user that triggered this event.
|
* The user that triggered this event.
|
||||||
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/user#user-object
|
* @see https://discord.com/developers/docs/resources/user#user-object
|
||||||
*/
|
*/
|
||||||
user: UserInfo
|
user: ComponentEventUser
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The guild that this event occurred in.
|
* The guild that this event occurred in.
|
||||||
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/guild#guild-object
|
* @see https://discord.com/developers/docs/resources/guild#guild-object
|
||||||
*/
|
*/
|
||||||
guild?: GuildInfo
|
guild?: ComponentEventGuild
|
||||||
|
|
||||||
|
/** Create a new reply to this event. */
|
||||||
|
reply(
|
||||||
|
content?: ReactNode,
|
||||||
|
options?: ComponentEventReplyOptions,
|
||||||
|
): ReacordInstance
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new reply to this event.
|
* Create an ephemeral reply to this event, shown only to the user who
|
||||||
*/
|
* triggered it.
|
||||||
reply(content?: ReactNode): ReacordInstance
|
*
|
||||||
|
* @deprecated Use event.reply(content, { ephemeral: true })
|
||||||
/**
|
|
||||||
* Create an ephemeral reply to this event,
|
|
||||||
* shown only to the user who triggered it.
|
|
||||||
*/
|
*/
|
||||||
ephemeralReply(content?: ReactNode): ReacordInstance
|
ephemeralReply(content?: ReactNode): ReacordInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventReplyOptions {
|
||||||
*/
|
ephemeral?: boolean
|
||||||
export type ChannelInfo = {
|
tts?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @category Component Event */
|
||||||
|
export interface ComponentEventChannel {
|
||||||
id: string
|
id: string
|
||||||
name?: string
|
name?: string
|
||||||
topic?: string
|
topic?: string
|
||||||
@@ -57,14 +65,12 @@ export type ChannelInfo = {
|
|||||||
rateLimitPerUser?: number
|
rateLimitPerUser?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventMessage {
|
||||||
*/
|
|
||||||
export type MessageInfo = {
|
|
||||||
id: string
|
id: string
|
||||||
channelId: string
|
channelId: string
|
||||||
authorId: UserInfo
|
authorId: string
|
||||||
member?: GuildMemberInfo
|
member?: ComponentEventGuildMember
|
||||||
content: string
|
content: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
editedTimestamp?: string
|
editedTimestamp?: string
|
||||||
@@ -74,19 +80,15 @@ export type MessageInfo = {
|
|||||||
mentions: string[]
|
mentions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventGuild {
|
||||||
*/
|
|
||||||
export type GuildInfo = {
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
member: GuildMemberInfo
|
member: ComponentEventGuildMember
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventGuildMember {
|
||||||
*/
|
|
||||||
export type GuildMemberInfo = {
|
|
||||||
id: string
|
id: string
|
||||||
nick?: string
|
nick?: string
|
||||||
displayName: string
|
displayName: string
|
||||||
@@ -100,14 +102,12 @@ export type GuildMemberInfo = {
|
|||||||
communicationDisabledUntil?: string
|
communicationDisabledUntil?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Component Event */
|
||||||
* @category Component Event
|
export interface ComponentEventUser {
|
||||||
*/
|
|
||||||
export type UserInfo = {
|
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
discriminator: string
|
discriminator: string
|
||||||
tag: string
|
tag: string
|
||||||
avatarUrl: string
|
avatarUrl: string | null
|
||||||
accentColor?: number
|
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
|
||||||
@@ -37,11 +38,19 @@ export function ActionRow(props: ActionRowProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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,9 +2,10 @@ 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
|
||||||
|
|
||||||
@@ -12,13 +13,12 @@ export type ButtonSharedProps = {
|
|||||||
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 { nanoid } from "nanoid"
|
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,30 +7,23 @@ 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)}>
|
||||||
@@ -43,10 +35,10 @@ export function Button(props: ButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ButtonNode extends Node<ButtonProps> {
|
class ButtonNode extends Node<ButtonProps> {
|
||||||
private customId = nanoid()
|
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 ""
|
||||||
}
|
}
|
||||||
@@ -74,4 +66,4 @@ class ButtonNode extends Node<ButtonProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButtonLabelNode extends Node<{}> {}
|
class ButtonLabelNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
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 {
|
||||||
*/
|
|
||||||
export type EmbedAuthorProps = {
|
|
||||||
name?: ReactNode
|
name?: ReactNode
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
url?: string
|
url?: string
|
||||||
iconUrl?: 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)}>
|
||||||
@@ -38,4 +33,4 @@ class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorTextNode extends Node<{}> {}
|
class AuthorTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
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 {
|
||||||
*/
|
|
||||||
export type EmbedFieldProps = {
|
|
||||||
name: ReactNode
|
name: ReactNode
|
||||||
value?: ReactNode
|
value?: ReactNode
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
children?: ReactNode
|
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)}>
|
||||||
@@ -25,7 +20,7 @@ export function EmbedField(props: EmbedFieldProps) {
|
|||||||
{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>
|
||||||
)
|
)
|
||||||
@@ -42,5 +37,5 @@ class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FieldNameNode extends Node<{}> {}
|
class FieldNameNode extends Node<Record<string, never>> {}
|
||||||
class FieldValueNode extends Node<{}> {}
|
class FieldValueNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
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 {
|
||||||
*/
|
|
||||||
export type EmbedFooterProps = {
|
|
||||||
text?: ReactNode
|
text?: ReactNode
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
iconUrl?: string
|
iconUrl?: string
|
||||||
timestamp?: string | number | Date
|
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)}>
|
||||||
@@ -42,4 +37,4 @@ class EmbedFooterNode extends EmbedChildNode<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FooterTextNode extends Node<{}> {}
|
class FooterTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
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 {
|
||||||
*/
|
|
||||||
export type EmbedImageProps = {
|
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedImage(props: EmbedImageProps) {
|
export function EmbedImage(props: EmbedImageProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement
|
<ReacordElement
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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"> & {
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
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 {
|
||||||
*/
|
|
||||||
export type EmbedThumbnailProps = {
|
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @category Embed */
|
||||||
* @category Embed
|
|
||||||
*/
|
|
||||||
export function EmbedThumbnail(props: EmbedThumbnailProps) {
|
export function EmbedThumbnail(props: EmbedThumbnailProps) {
|
||||||
return (
|
return (
|
||||||
<ReacordElement
|
<ReacordElement
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
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 {
|
||||||
*/
|
|
||||||
export type EmbedTitleProps = {
|
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
url?: string
|
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)}>
|
||||||
@@ -33,4 +28,4 @@ class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TitleTextNode extends Node<{}> {}
|
class TitleTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react"
|
import { snakeCaseDeep } from "@reacord/helpers/convert-object-property-case"
|
||||||
import { snakeCaseDeep } from "../../../helpers/convert-object-property-case"
|
import { omit } from "@reacord/helpers/omit"
|
||||||
import { omit } from "../../../helpers/omit"
|
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,7 +12,7 @@ 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
|
||||||
@@ -53,7 +53,7 @@ class EmbedNode extends Node<EmbedProps> {
|
|||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
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
|
||||||
@@ -15,14 +12,12 @@ export type LinkProps = ButtonSharedProps & {
|
|||||||
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>
|
||||||
)
|
)
|
||||||
@@ -40,4 +35,4 @@ class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkTextNode extends Node<{}> {}
|
class LinkTextNode extends Node<Record<string, never>> {}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ export class OptionNode extends Node<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,5 +1,4 @@
|
|||||||
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,
|
||||||
@@ -7,10 +6,8 @@ import {
|
|||||||
OptionNode,
|
OptionNode,
|
||||||
} from "./option-node"
|
} from "./option-node"
|
||||||
|
|
||||||
/**
|
/** @category Select */
|
||||||
* @category Select
|
export interface OptionProps {
|
||||||
*/
|
|
||||||
export type OptionProps = {
|
|
||||||
/** The internal value of this option */
|
/** The internal value of this option */
|
||||||
value: string
|
value: string
|
||||||
/** The text shown to the user. This takes priority over `children` */
|
/** The text shown to the user. This takes priority over `children` */
|
||||||
@@ -23,19 +20,16 @@ export type OptionProps = {
|
|||||||
/**
|
/**
|
||||||
* 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,
|
||||||
@@ -46,7 +40,7 @@ export function Option({
|
|||||||
<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 && (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { nanoid } from "nanoid"
|
import { isInstanceOf } from "@reacord/helpers/is-instance-of"
|
||||||
|
import { randomUUID } from "node:crypto"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import React from "react"
|
|
||||||
import { isInstanceOf } from "../../../helpers/is-instance-of"
|
|
||||||
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 {
|
||||||
@@ -12,11 +11,10 @@ import type {
|
|||||||
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"
|
||||||
|
|
||||||
/**
|
/** @category Select */
|
||||||
* @category Select
|
export interface SelectProps {
|
||||||
*/
|
|
||||||
export type SelectProps = {
|
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
/** Sets the currently selected value */
|
/** Sets the currently selected value */
|
||||||
value?: string
|
value?: string
|
||||||
@@ -31,8 +29,8 @@ export type SelectProps = {
|
|||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* With `multiple`, the minimum number of values that can be selected.
|
* With `multiple`, the minimum number of values that can be selected. When
|
||||||
* When `multiple` is false or not defined, this is always 1.
|
* `multiple` is false or not defined, this is always 1.
|
||||||
*
|
*
|
||||||
* This only limits the number of values that can be received by the user.
|
* 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.
|
* This does not limit the number of values that can be displayed by you.
|
||||||
@@ -40,44 +38,44 @@ export type SelectProps = {
|
|||||||
minValues?: number
|
minValues?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* With `multiple`, the maximum number of values that can be selected.
|
* With `multiple`, the maximum number of values that can be selected. When
|
||||||
* When `multiple` is false or not defined, this is always 1.
|
* `multiple` is false or not defined, this is always 1.
|
||||||
*
|
*
|
||||||
* This only limits the number of values that can be received by the user.
|
* 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.
|
* This does not limit the number of values that can be displayed by you.
|
||||||
*/
|
*/
|
||||||
maxValues?: number
|
maxValues?: number
|
||||||
|
|
||||||
/** When true, the select will be slightly faded, and cannot be interacted with. */
|
/**
|
||||||
|
* When true, the select will be slightly faded, and cannot be interacted
|
||||||
|
* with.
|
||||||
|
*/
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user inputs a selection.
|
* Called when the user inputs a selection. Receives the entire select change
|
||||||
* Receives the entire select change event,
|
* event, which can be used to create new replies, etc.
|
||||||
* which can be used to create new replies, etc.
|
|
||||||
*/
|
*/
|
||||||
onChange?: (event: SelectChangeEvent) => void
|
onChange?: (event: SelectChangeEvent) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience shorthand for `onChange`, which receives the first selected value.
|
* Convenience shorthand for `onChange`, which receives the first selected
|
||||||
|
* value.
|
||||||
*/
|
*/
|
||||||
onChangeValue?: (value: string, event: SelectChangeEvent) => void
|
onChangeValue?: (value: string, event: SelectChangeEvent) => void
|
||||||
|
|
||||||
/**
|
/** Convenience shorthand for `onChange`, which receives all selected values. */
|
||||||
* Convenience shorthand for `onChange`, which receives all selected values.
|
|
||||||
*/
|
|
||||||
onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void
|
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) {
|
||||||
@@ -89,7 +87,7 @@ export function Select(props: SelectProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SelectNode extends Node<SelectProps> {
|
class SelectNode extends Node<SelectProps> {
|
||||||
readonly customId = nanoid()
|
readonly customId = randomUUID()
|
||||||
|
|
||||||
override modifyMessageOptions(message: MessageOptions): void {
|
override modifyMessageOptions(message: MessageOptions): void {
|
||||||
const actionRow: ActionRow = []
|
const actionRow: ActionRow = []
|
||||||
@@ -105,12 +103,13 @@ class SelectNode extends Node<SelectProps> {
|
|||||||
values,
|
values,
|
||||||
minValues = 0,
|
minValues = 0,
|
||||||
maxValues = 25,
|
maxValues = 25,
|
||||||
children,
|
|
||||||
onChange,
|
|
||||||
onChangeValue,
|
|
||||||
onChangeMultiple,
|
|
||||||
...props
|
...props
|
||||||
} = this.props
|
} = omit(this.props, [
|
||||||
|
"children",
|
||||||
|
"onChange",
|
||||||
|
"onChangeValue",
|
||||||
|
"onChangeMultiple",
|
||||||
|
])
|
||||||
|
|
||||||
const item: ActionRowItem = {
|
const item: ActionRowItem = {
|
||||||
...props,
|
...props,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { raise } from "../../helpers/raise"
|
|
||||||
import type { ReacordInstance } from "./instance"
|
|
||||||
|
|
||||||
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ 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
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
import { safeJsonStringify } from "@reacord/helpers/json"
|
||||||
|
import { pick } from "@reacord/helpers/pick"
|
||||||
|
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
|
||||||
|
import { raise } from "@reacord/helpers/raise"
|
||||||
import * as Discord from "discord.js"
|
import * as Discord from "discord.js"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import type { Except } from "type-fest"
|
import type { Except } from "type-fest"
|
||||||
import { pick } from "../../helpers/pick"
|
|
||||||
import { pruneNullishValues } from "../../helpers/prune-nullish-values"
|
|
||||||
import { raise } from "../../helpers/raise"
|
|
||||||
import type { ComponentInteraction } from "../internal/interaction"
|
import type { ComponentInteraction } from "../internal/interaction"
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -14,11 +14,12 @@ import type {
|
|||||||
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
|
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
|
||||||
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
|
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
|
||||||
import type {
|
import type {
|
||||||
ChannelInfo,
|
ComponentEventChannel,
|
||||||
GuildInfo,
|
ComponentEventGuild,
|
||||||
GuildMemberInfo,
|
ComponentEventGuildMember,
|
||||||
MessageInfo,
|
ComponentEventMessage,
|
||||||
UserInfo,
|
ComponentEventReplyOptions,
|
||||||
|
ComponentEventUser,
|
||||||
} from "./component-event"
|
} from "./component-event"
|
||||||
import type { ReacordInstance } from "./instance"
|
import type { ReacordInstance } from "./instance"
|
||||||
import type { ReacordConfig } from "./reacord"
|
import type { ReacordConfig } from "./reacord"
|
||||||
@@ -26,14 +27,18 @@ 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(
|
||||||
|
private client: Discord.Client,
|
||||||
|
config: ReacordConfig = {},
|
||||||
|
) {
|
||||||
super(config)
|
super(config)
|
||||||
|
|
||||||
client.on("interactionCreate", (interaction) => {
|
client.on("interactionCreate", (interaction) => {
|
||||||
if (interaction.isButton() || interaction.isSelectMenu()) {
|
if (interaction.isButton() || interaction.isStringSelectMenu()) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
this.createReacordComponentInteraction(interaction),
|
this.createReacordComponentInteraction(interaction),
|
||||||
)
|
)
|
||||||
@@ -43,59 +48,116 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to a channel.
|
* Sends a message to a channel.
|
||||||
|
*
|
||||||
|
* @param target Discord channel object.
|
||||||
|
* @param [options] Options for the channel message
|
||||||
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
|
* @see {@link Discord.MessageCreateOptions}
|
||||||
|
*/
|
||||||
|
public createChannelMessage(
|
||||||
|
target: Discord.ChannelResolvable,
|
||||||
|
options: Discord.MessageCreateOptions = {},
|
||||||
|
): ReacordInstance {
|
||||||
|
return this.createInstance(
|
||||||
|
this.createChannelMessageRenderer(target, options),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replies to a command interaction by sending a message.
|
||||||
|
*
|
||||||
|
* @param interaction Discord command interaction object.
|
||||||
|
* @param [options] Custom options for the interaction reply method.
|
||||||
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
|
* @see {@link Discord.InteractionReplyOptions}
|
||||||
|
*/
|
||||||
|
public createInteractionReply(
|
||||||
|
interaction: Discord.CommandInteraction,
|
||||||
|
options: Discord.InteractionReplyOptions = {},
|
||||||
|
): ReacordInstance {
|
||||||
|
return this.createInstance(
|
||||||
|
this.createInteractionReplyRenderer(interaction, options),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to a channel.
|
||||||
|
*
|
||||||
|
* @deprecated Use reacord.createChannelMessage() instead.
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
*/
|
*/
|
||||||
override send(
|
public send(
|
||||||
channelId: string,
|
channel: Discord.ChannelResolvable,
|
||||||
initialContent?: React.ReactNode,
|
initialContent?: React.ReactNode,
|
||||||
): ReacordInstance {
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
this.createChannelRenderer(channelId),
|
this.createChannelMessageRenderer(channel, {}),
|
||||||
initialContent,
|
initialContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message as a reply to a command interaction.
|
* Sends a message as a reply to a command interaction.
|
||||||
|
*
|
||||||
|
* @deprecated Use reacord.createInteractionReply() instead.
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
*/
|
*/
|
||||||
override reply(
|
public reply(
|
||||||
interaction: Discord.CommandInteraction,
|
interaction: Discord.CommandInteraction,
|
||||||
initialContent?: React.ReactNode,
|
initialContent?: React.ReactNode,
|
||||||
): ReacordInstance {
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
this.createInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, {}),
|
||||||
initialContent,
|
initialContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an ephemeral message as a reply to a command interaction.
|
* Sends an ephemeral message as a reply to a command interaction.
|
||||||
|
*
|
||||||
|
* @deprecated Use reacord.createInteractionReply(interaction, { ephemeral:
|
||||||
|
* true })
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
*/
|
*/
|
||||||
override ephemeralReply(
|
public ephemeralReply(
|
||||||
interaction: Discord.CommandInteraction,
|
interaction: Discord.CommandInteraction,
|
||||||
initialContent?: React.ReactNode,
|
initialContent?: React.ReactNode,
|
||||||
): ReacordInstance {
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
initialContent,
|
initialContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private createChannelRenderer(channelId: string) {
|
private createChannelMessageRenderer(
|
||||||
|
channelResolvable: Discord.ChannelResolvable,
|
||||||
|
messageCreateOptions?: Discord.MessageCreateOptions,
|
||||||
|
) {
|
||||||
return new ChannelMessageRenderer({
|
return new ChannelMessageRenderer({
|
||||||
send: async (options) => {
|
send: async (messageOptions) => {
|
||||||
const channel =
|
let channel = this.client.channels.resolve(channelResolvable)
|
||||||
this.client.channels.cache.get(channelId) ??
|
if (!channel && typeof channelResolvable === "string") {
|
||||||
(await this.client.channels.fetch(channelId)) ??
|
channel = await this.client.channels.fetch(channelResolvable)
|
||||||
raise(`Channel ${channelId} not found`)
|
|
||||||
|
|
||||||
if (!channel.isTextBased()) {
|
|
||||||
raise(`Channel ${channelId} is not a text channel`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await channel.send(getDiscordMessageOptions(options))
|
if (!channel) {
|
||||||
|
const id =
|
||||||
|
typeof channelResolvable === "string"
|
||||||
|
? channelResolvable
|
||||||
|
: channelResolvable.id
|
||||||
|
raise(`Channel ${id} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel.isTextBased()) {
|
||||||
|
raise(`Channel ${channel.id} must be a text channel`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await channel.send({
|
||||||
|
...getDiscordMessageOptions(messageOptions),
|
||||||
|
...messageCreateOptions,
|
||||||
|
})
|
||||||
return createReacordMessage(message)
|
return createReacordMessage(message)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -105,48 +167,25 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
interaction:
|
interaction:
|
||||||
| Discord.CommandInteraction
|
| Discord.CommandInteraction
|
||||||
| Discord.MessageComponentInteraction,
|
| Discord.MessageComponentInteraction,
|
||||||
|
interactionReplyOptions: Discord.InteractionReplyOptions,
|
||||||
) {
|
) {
|
||||||
return new InteractionReplyRenderer({
|
return new InteractionReplyRenderer({
|
||||||
type: "command",
|
interactionId: interaction.id,
|
||||||
id: interaction.id,
|
reply: async (messageOptions) => {
|
||||||
reply: async (options) => {
|
|
||||||
const message = await interaction.reply({
|
const message = await interaction.reply({
|
||||||
...getDiscordMessageOptions(options),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
|
...interactionReplyOptions,
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
})
|
})
|
||||||
return createReacordMessage(message as Discord.Message)
|
return createReacordMessage(message)
|
||||||
},
|
},
|
||||||
followUp: async (options) => {
|
followUp: async (messageOptions) => {
|
||||||
const message = await interaction.followUp({
|
const message = await interaction.followUp({
|
||||||
...getDiscordMessageOptions(options),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
|
...interactionReplyOptions,
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
})
|
})
|
||||||
return createReacordMessage(message as Discord.Message)
|
return createReacordMessage(message)
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private createEphemeralInteractionReplyRenderer(
|
|
||||||
interaction:
|
|
||||||
| Discord.CommandInteraction
|
|
||||||
| Discord.MessageComponentInteraction,
|
|
||||||
) {
|
|
||||||
return new InteractionReplyRenderer({
|
|
||||||
type: "command",
|
|
||||||
id: interaction.id,
|
|
||||||
reply: async (options) => {
|
|
||||||
await interaction.reply({
|
|
||||||
...getDiscordMessageOptions(options),
|
|
||||||
ephemeral: true,
|
|
||||||
})
|
|
||||||
return createEphemeralReacordMessage()
|
|
||||||
},
|
|
||||||
followUp: async (options) => {
|
|
||||||
await interaction.followUp({
|
|
||||||
...getDiscordMessageOptions(options),
|
|
||||||
ephemeral: true,
|
|
||||||
})
|
|
||||||
return createEphemeralReacordMessage()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -155,7 +194,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
interaction: Discord.MessageComponentInteraction,
|
interaction: Discord.MessageComponentInteraction,
|
||||||
): ComponentInteraction {
|
): ComponentInteraction {
|
||||||
// todo please dear god clean this up
|
// todo please dear god clean this up
|
||||||
const channel: ChannelInfo = interaction.channel
|
const channel: ComponentEventChannel = interaction.channel
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(
|
...pruneNullishValues(
|
||||||
pick(interaction.channel, [
|
pick(interaction.channel, [
|
||||||
@@ -171,7 +210,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
: raise("Non-channel interactions are not supported")
|
: raise("Non-channel interactions are not supported")
|
||||||
|
|
||||||
const message: MessageInfo =
|
const message: ComponentEventMessage =
|
||||||
interaction.message instanceof Discord.Message
|
interaction.message instanceof Discord.Message
|
||||||
? {
|
? {
|
||||||
...pick(interaction.message, [
|
...pick(interaction.message, [
|
||||||
@@ -189,10 +228,12 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
? new Date(interaction.message.editedTimestamp).toISOString()
|
? new Date(interaction.message.editedTimestamp).toISOString()
|
||||||
: undefined,
|
: undefined,
|
||||||
mentions: interaction.message.mentions.users.map((u) => u.id),
|
mentions: interaction.message.mentions.users.map((u) => u.id),
|
||||||
|
authorId: interaction.message.author.id,
|
||||||
|
mentionEveryone: interaction.message.mentions.everyone,
|
||||||
}
|
}
|
||||||
: raise("Message not found")
|
: raise("Message not found")
|
||||||
|
|
||||||
const member: GuildMemberInfo | undefined =
|
const member: ComponentEventGuildMember | undefined =
|
||||||
interaction.member instanceof Discord.GuildMember
|
interaction.member instanceof Discord.GuildMember
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(
|
...pruneNullishValues(
|
||||||
@@ -207,26 +248,28 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
displayName: interaction.member.displayName,
|
displayName: interaction.member.displayName,
|
||||||
roles: [...interaction.member.roles.cache.map((role) => role.id)],
|
roles: interaction.member.roles.cache.map((role) => role.id),
|
||||||
joinedAt: interaction.member.joinedAt?.toISOString(),
|
joinedAt: interaction.member.joinedAt?.toISOString(),
|
||||||
premiumSince: interaction.member.premiumSince?.toISOString(),
|
premiumSince: interaction.member.premiumSince?.toISOString(),
|
||||||
communicationDisabledUntil:
|
communicationDisabledUntil:
|
||||||
interaction.member.communicationDisabledUntil?.toISOString(),
|
interaction.member.communicationDisabledUntil?.toISOString(),
|
||||||
|
color: interaction.member.displayColor,
|
||||||
|
displayAvatarUrl: interaction.member.displayAvatarURL(),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const guild: GuildInfo | undefined = interaction.guild
|
const guild: ComponentEventGuild | undefined = interaction.guild
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
||||||
member: member ?? raise("unexpected: member is undefined"),
|
member: member ?? raise("unexpected: member is undefined"),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const user: UserInfo = {
|
const user: ComponentEventUser = {
|
||||||
...pruneNullishValues(
|
...pruneNullishValues(
|
||||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
||||||
),
|
),
|
||||||
avatarUrl: interaction.user.avatarURL()!,
|
avatarUrl: interaction.user.avatarURL(),
|
||||||
accentColor: interaction.user.accentColor ?? undefined,
|
accentColor: interaction.user.accentColor ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +277,11 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
id: interaction.id,
|
id: interaction.id,
|
||||||
customId: interaction.customId,
|
customId: interaction.customId,
|
||||||
update: async (options: MessageOptions) => {
|
update: async (options: MessageOptions) => {
|
||||||
|
if (interaction.deferred || interaction.replied) {
|
||||||
|
await interaction.message.edit(getDiscordMessageOptions(options))
|
||||||
|
} else {
|
||||||
await interaction.update(getDiscordMessageOptions(options))
|
await interaction.update(getDiscordMessageOptions(options))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deferUpdate: async () => {
|
deferUpdate: async () => {
|
||||||
if (interaction.replied || interaction.deferred) return
|
if (interaction.replied || interaction.deferred) return
|
||||||
@@ -245,14 +292,14 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
...getDiscordMessageOptions(options),
|
...getDiscordMessageOptions(options),
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
})
|
})
|
||||||
return createReacordMessage(message as Discord.Message)
|
return createReacordMessage(message)
|
||||||
},
|
},
|
||||||
followUp: async (options) => {
|
followUp: async (options) => {
|
||||||
const message = await interaction.followUp({
|
const message = await interaction.followUp({
|
||||||
...getDiscordMessageOptions(options),
|
...getDiscordMessageOptions(options),
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
})
|
})
|
||||||
return createReacordMessage(message as Discord.Message)
|
return createReacordMessage(message)
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
channel,
|
channel,
|
||||||
@@ -260,15 +307,18 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
user,
|
user,
|
||||||
guild,
|
guild,
|
||||||
|
|
||||||
reply: (content?: ReactNode) =>
|
reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
|
||||||
this.createInstance(
|
this.createInstance(
|
||||||
this.createInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, options ?? {}),
|
||||||
content,
|
content,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/** @deprecated Use event.reply(content, { ephemeral: true }) */
|
||||||
ephemeralReply: (content: ReactNode) =>
|
ephemeralReply: (content: ReactNode) =>
|
||||||
this.createInstance(
|
this.createInstance(
|
||||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
content,
|
content,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -281,7 +331,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isSelectMenu()) {
|
if (interaction.isStringSelectMenu()) {
|
||||||
return {
|
return {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
type: "select",
|
type: "select",
|
||||||
@@ -307,19 +357,6 @@ function createReacordMessage(message: Discord.Message): Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEphemeralReacordMessage(): Message {
|
|
||||||
return {
|
|
||||||
edit: () => {
|
|
||||||
console.warn("Ephemeral messages can't be edited")
|
|
||||||
return Promise.resolve()
|
|
||||||
},
|
|
||||||
delete: () => {
|
|
||||||
console.warn("Ephemeral messages can't be deleted")
|
|
||||||
return Promise.resolve()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
||||||
const styleMap = {
|
const styleMap = {
|
||||||
primary: Discord.ButtonStyle.Primary,
|
primary: Discord.ButtonStyle.Primary,
|
||||||
@@ -335,8 +372,7 @@ function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
|||||||
// 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,
|
||||||
@@ -353,6 +389,19 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (component.type === "link") {
|
||||||
|
return {
|
||||||
|
type: Discord.ComponentType.Button,
|
||||||
|
url: component.url,
|
||||||
|
label: component.label ?? "",
|
||||||
|
style: Discord.ButtonStyle.Link,
|
||||||
|
disabled: component.disabled,
|
||||||
|
emoji: component.emoji,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// future proofing
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (component.type === "select") {
|
if (component.type === "select") {
|
||||||
return {
|
return {
|
||||||
...component,
|
...component,
|
||||||
@@ -364,13 +413,16 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raise(`Unsupported component type: ${component.type}`)
|
component satisfies never
|
||||||
|
throw new Error(
|
||||||
|
`Invalid component type ${safeJsonStringify(component)}}`,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.content && !options.embeds?.length) {
|
if (!options.content && !options.embeds.length) {
|
||||||
options.content = "_ _"
|
options.content = "_ _"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,28 @@
|
|||||||
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 } from "./instance-context.js"
|
||||||
import { InstanceProvider } from "./instance-context"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
|
||||||
|
/** @category Core */
|
||||||
|
export interface ReacordConfig {
|
||||||
/**
|
/**
|
||||||
* @category Core
|
* The max number of active instances. When this limit is exceeded, the oldest
|
||||||
*/
|
* instances will be disabled.
|
||||||
export type ReacordConfig = {
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
abstract reply(...args: unknown[]): ReacordInstance
|
|
||||||
abstract ephemeralReply(...args: unknown[]): ReacordInstance
|
|
||||||
|
|
||||||
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
for (const renderer of this.renderers) {
|
for (const renderer of this.renderers) {
|
||||||
if (renderer.handleComponentInteraction(interaction)) return
|
if (renderer.handleComponentInteraction(interaction)) return
|
||||||
@@ -41,13 +34,22 @@ export abstract class Reacord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected createInstance(renderer: Renderer, initialContent?: ReactNode) {
|
protected createInstance(renderer: Renderer, initialContent?: ReactNode) {
|
||||||
if (this.renderers.length > this.maxInstances) {
|
if (this.renderers.length > this.maxInstances && this.renderers[0]) {
|
||||||
this.deactivate(this.renderers[0]!)
|
this.deactivate(this.renderers[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderers.push(renderer)
|
this.renderers.push(renderer)
|
||||||
|
|
||||||
const container = reconciler.createContainer(renderer, 0, false, {})
|
const container: unknown = reconciler.createContainer(
|
||||||
|
renderer,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"reacord",
|
||||||
|
() => {},
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
const instance: ReacordInstance = {
|
const instance: ReacordInstance = {
|
||||||
render: (content: ReactNode) => {
|
render: (content: ReactNode) => {
|
||||||
@@ -55,6 +57,7 @@ export abstract class Reacord {
|
|||||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
||||||
container,
|
container,
|
||||||
)
|
)
|
||||||
|
return instance
|
||||||
},
|
},
|
||||||
deactivate: () => {
|
deactivate: () => {
|
||||||
this.deactivate(renderer)
|
this.deactivate(renderer)
|
||||||
|
|||||||
@@ -1,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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export class Container<T> {
|
|||||||
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>(
|
||||||
|
type: new (...args: Array<NonNullable<unknown>>) => U,
|
||||||
|
): U | undefined {
|
||||||
for (const item of this.items) {
|
for (const item of this.items) {
|
||||||
if (item instanceof type) return item
|
if (item instanceof type) return item
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
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
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type SelectInteraction = BaseComponentInteraction<
|
|||||||
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>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Except } from "type-fest"
|
|
||||||
import { last } from "../../helpers/last"
|
|
||||||
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[]
|
||||||
@@ -16,7 +16,7 @@ export type ActionRowItem =
|
|||||||
| MessageLinkOptions
|
| MessageLinkOptions
|
||||||
| MessageSelectOptions
|
| MessageSelectOptions
|
||||||
|
|
||||||
export type MessageButtonOptions = {
|
export interface MessageButtonOptions {
|
||||||
type: "button"
|
type: "button"
|
||||||
customId: string
|
customId: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -25,7 +25,7 @@ export type MessageButtonOptions = {
|
|||||||
emoji?: string
|
emoji?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageLinkOptions = {
|
export interface MessageLinkOptions {
|
||||||
type: "link"
|
type: "link"
|
||||||
url: string
|
url: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -39,14 +39,14 @@ export type MessageSelectOptions = Except<SelectProps, "children" | "value"> & {
|
|||||||
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>
|
edit(options: MessageOptions): Promise<void>
|
||||||
delete(): Promise<void>
|
delete(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* 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"
|
||||||
@@ -8,9 +7,11 @@ export abstract class Node<Props> {
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
import type { HostConfig } from "react-reconciler"
|
import type { HostConfig } from "react-reconciler"
|
||||||
import ReactReconciler from "react-reconciler"
|
import ReactReconciler from "react-reconciler"
|
||||||
import { raise } from "../../helpers/raise.js"
|
import { DefaultEventPriority } from "react-reconciler/constants.js"
|
||||||
import { Node } from "./node.js"
|
import { Node } from "./node.js"
|
||||||
import type { Renderer } from "./renderers/renderer"
|
import type { Renderer } from "./renderers/renderer"
|
||||||
import { TextNode } from "./text-node.js"
|
import { TextNode } from "./text-node.js"
|
||||||
@@ -20,8 +21,6 @@ const config: HostConfig<
|
|||||||
number, // TimeoutHandle,
|
number, // TimeoutHandle,
|
||||||
number // NoTimeout,
|
number // NoTimeout,
|
||||||
> = {
|
> = {
|
||||||
// config
|
|
||||||
now: Date.now,
|
|
||||||
supportsMutation: true,
|
supportsMutation: true,
|
||||||
supportsPersistence: false,
|
supportsPersistence: false,
|
||||||
supportsHydration: false,
|
supportsHydration: false,
|
||||||
@@ -30,7 +29,6 @@ const config: HostConfig<
|
|||||||
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,
|
||||||
|
|
||||||
@@ -43,7 +41,7 @@ const config: HostConfig<
|
|||||||
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`)
|
||||||
}
|
}
|
||||||
@@ -52,8 +50,11 @@ const config: HostConfig<
|
|||||||
},
|
},
|
||||||
createTextInstance: (text) => new TextNode(text),
|
createTextInstance: (text) => new TextNode(text),
|
||||||
shouldSetTextContent: () => false,
|
shouldSetTextContent: () => false,
|
||||||
// @ts-expect-error
|
detachDeletedInstance: (_instance) => {},
|
||||||
detachDeletedInstance: (instance) => {},
|
beforeActiveInstanceBlur: () => {},
|
||||||
|
afterActiveInstanceBlur: () => {},
|
||||||
|
getInstanceFromNode: (_node: unknown) => null,
|
||||||
|
getInstanceFromScope: (_scopeInstance: unknown) => null,
|
||||||
|
|
||||||
clearContainer: (renderer) => {
|
clearContainer: (renderer) => {
|
||||||
renderer.nodes.clear()
|
renderer.nodes.clear()
|
||||||
@@ -89,16 +90,18 @@ const config: HostConfig<
|
|||||||
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) => {},
|
||||||
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reconciler = ReactReconciler(config)
|
export const reconciler = ReactReconciler(config)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Interaction } from "../interaction"
|
|
||||||
import type { Message, MessageOptions } from "../message"
|
import type { Message, MessageOptions } from "../message"
|
||||||
import { Renderer } from "./renderer"
|
import { Renderer } from "./renderer"
|
||||||
|
|
||||||
@@ -6,17 +5,23 @@ import { Renderer } from "./renderer"
|
|||||||
// so we know whether to call reply() or followUp()
|
// so we know whether to call reply() or followUp()
|
||||||
const repliedInteractionIds = new Set<string>()
|
const repliedInteractionIds = new Set<string>()
|
||||||
|
|
||||||
|
export type InteractionReplyRendererImplementation = {
|
||||||
|
interactionId: string
|
||||||
|
reply: (options: MessageOptions) => Promise<Message>
|
||||||
|
followUp: (options: MessageOptions) => Promise<Message>
|
||||||
|
}
|
||||||
|
|
||||||
export class InteractionReplyRenderer extends Renderer {
|
export class InteractionReplyRenderer extends Renderer {
|
||||||
constructor(private interaction: Interaction) {
|
constructor(private implementation: InteractionReplyRendererImplementation) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||||
if (repliedInteractionIds.has(this.interaction.id)) {
|
if (repliedInteractionIds.has(this.implementation.interactionId)) {
|
||||||
return this.interaction.followUp(options)
|
return this.implementation.followUp(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
repliedInteractionIds.add(this.interaction.id)
|
repliedInteractionIds.add(this.implementation.interactionId)
|
||||||
return this.interaction.reply(options)
|
return this.implementation.reply(options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
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 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 }
|
||||||
@@ -47,14 +47,12 @@ export abstract class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleComponentInteraction(interaction: ComponentInteraction) {
|
handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
if (node.handleComponentInteraction(interaction)) {
|
||||||
this.componentInteraction = interaction
|
this.componentInteraction = interaction
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updates.next({ action: "deferUpdate", interaction })
|
this.updates.next({ action: "deferUpdate", interaction })
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
for (const node of this.nodes) {
|
|
||||||
if (node.handleComponentInteraction(interaction)) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.0",
|
"version": "0.5.5",
|
||||||
"types": "./dist/main.d.ts",
|
|
||||||
"homepage": "https://reacord.mapleleaf.dev",
|
"homepage": "https://reacord.mapleleaf.dev",
|
||||||
"repository": "https://github.com/itsMapleLeaf/reacord.git",
|
"repository": "https://github.com/itsMapleLeaf/reacord.git",
|
||||||
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
|
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
|
||||||
@@ -24,10 +23,12 @@
|
|||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/main.js",
|
"import": "./dist/main.js",
|
||||||
"require": "./dist/main.cjs"
|
"require": "./dist/main.cjs",
|
||||||
|
"types": "./dist/main.d.ts"
|
||||||
},
|
},
|
||||||
"./package.json": {
|
"./package.json": {
|
||||||
"import": "./package.json",
|
"import": "./package.json",
|
||||||
@@ -35,21 +36,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cp ../../README.md . && cp ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --dts --sourcemap",
|
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve",
|
||||||
"build-watch": "pnpm build -- --watch",
|
"build-watch": "pnpm build -- --watch",
|
||||||
"test": "vitest --coverage --no-watch",
|
"test": "vitest --coverage --no-watch",
|
||||||
"test-dev": "vitest",
|
"test-dev": "vitest",
|
||||||
"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"
|
||||||
"release": "bash scripts/release.sh"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "^20.8.4",
|
||||||
"@types/react": "*",
|
"@types/react": "^18.2.27",
|
||||||
"@types/react-reconciler": "^0.26.6",
|
"@types/react-reconciler": "^0.28.5",
|
||||||
"nanoid": "^3.3.3",
|
"react-reconciler": "^0.29.0",
|
||||||
"react-reconciler": "^0.27.0",
|
"rxjs": "^7.8.1"
|
||||||
"rxjs": "^7.5.5"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"discord.js": "^14",
|
"discord.js": "^14",
|
||||||
@@ -61,25 +60,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@reacord/helpers": "workspace:*",
|
||||||
"c8": "^7.11.2",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"discord.js": "^14.0.3",
|
"c8": "^8.0.1",
|
||||||
"dotenv": "^16.0.0",
|
"cpy-cli": "^5.0.0",
|
||||||
|
"discord.js": "^14.13.0",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^3.0.3",
|
||||||
"pretty-ms": "^7.0.1",
|
"pretty-ms": "^8.0.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.2.0",
|
||||||
"release-it": "^14.14.2",
|
"tsup": "^7.2.0",
|
||||||
"tsup": "^5.12.6",
|
"tsx": "^3.13.0",
|
||||||
"tsx": "^3.8.0",
|
"type-fest": "^4.4.0"
|
||||||
"type-fest": "^2.12.2",
|
|
||||||
"typescript": "^4.6.3",
|
|
||||||
"vite": "^2.9.5",
|
|
||||||
"vitest": "^0.10.0"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"esbuild": "latest"
|
|
||||||
},
|
},
|
||||||
"release-it": {
|
"release-it": {
|
||||||
"git": {
|
"git": {
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
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 * as React from "react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
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}`,
|
||||||
@@ -33,7 +39,7 @@ for (const [, channel] of category.children.cache) {
|
|||||||
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({
|
||||||
@@ -44,7 +50,7 @@ const createTest = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createTest("basic", (channel) => {
|
await createTest("basic", (channel) => {
|
||||||
reacord.send(channel.id, "Hello, world!")
|
reacord.createChannelMessage(channel).render("Hello, world!")
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("counter", (channel) => {
|
await createTest("counter", (channel) => {
|
||||||
@@ -67,7 +73,7 @@ await createTest("counter", (channel) => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
reacord.send(channel.id, <Counter />)
|
reacord.createChannelMessage(channel).render(<Counter />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("select", (channel) => {
|
await createTest("select", (channel) => {
|
||||||
@@ -96,8 +102,7 @@ await createTest("select", (channel) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = reacord.send(
|
const instance = reacord.createChannelMessage(channel).render(
|
||||||
channel.id,
|
|
||||||
<FruitSelect
|
<FruitSelect
|
||||||
onConfirm={(value) => {
|
onConfirm={(value) => {
|
||||||
instance.render(`you chose ${value}`)
|
instance.render(`you chose ${value}`)
|
||||||
@@ -108,8 +113,7 @@ await createTest("select", (channel) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await createTest("ephemeral button", (channel) => {
|
await createTest("ephemeral button", (channel) => {
|
||||||
reacord.send(
|
reacord.createChannelMessage(channel).render(
|
||||||
channel.id,
|
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="public clic"
|
label="public clic"
|
||||||
@@ -119,7 +123,7 @@ await createTest("ephemeral button", (channel) => {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="clic"
|
label="clic"
|
||||||
onClick={(event) => event.ephemeralReply("you clic")}
|
onClick={(event) => event.reply("you clic", { ephemeral: true })}
|
||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
)
|
)
|
||||||
@@ -130,5 +134,11 @@ await createTest("delete this", (channel) => {
|
|||||||
const instance = useInstance()
|
const instance = useInstance()
|
||||||
return <Button label="delete this" onClick={() => instance.destroy()} />
|
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||||
}
|
}
|
||||||
reacord.send(channel.id, <DeleteThis />)
|
reacord.createChannelMessage(channel).render(<DeleteThis />)
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTest("link", (channel) => {
|
||||||
|
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"
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ beforeAll(() => {
|
|||||||
|
|
||||||
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,4 +1,3 @@
|
|||||||
import React from "react"
|
|
||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
import {
|
import {
|
||||||
Embed,
|
Embed,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
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([
|
||||||
{
|
{
|
||||||
@@ -35,7 +36,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("show embed").click()
|
await tester.findButtonByLabel("show embed").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 0",
|
content: "count: 0",
|
||||||
@@ -62,7 +63,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 1",
|
content: "count: 1",
|
||||||
@@ -94,7 +95,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 2",
|
content: "count: 2",
|
||||||
@@ -126,7 +127,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("hide embed").click()
|
await tester.findButtonByLabel("hide embed").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 2",
|
content: "count: 2",
|
||||||
@@ -153,7 +154,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
@@ -180,7 +181,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("deactivate").click()
|
await tester.findButtonByLabel("deactivate").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
@@ -210,7 +211,7 @@ test("rendering behavior", async () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
tester.findButtonByLabel("clicc").click()
|
await tester.findButtonByLabel("clicc").click()
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "count: 3",
|
content: "count: 3",
|
||||||
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
|
|||||||
test("delete", async () => {
|
test("delete", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = tester.reply()
|
const reply = tester.createInteractionReply().render(
|
||||||
reply.render(
|
|
||||||
<>
|
<>
|
||||||
some text
|
some text
|
||||||
<Embed>some embed</Embed>
|
<Embed>some embed</Embed>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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"
|
||||||
@@ -53,22 +53,20 @@ test("single select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
tester.findSelectByPlaceholder("choose one").select("2")
|
await tester.findSelectByPlaceholder("choose one").select("2")
|
||||||
await assertSelect(["2"])
|
await assertSelect(["2"])
|
||||||
expect(onSelect).toHaveBeenCalledWith(
|
expect(onSelect).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ values: ["2"] }),
|
expect.objectContaining({ values: ["2"] }),
|
||||||
)
|
)
|
||||||
|
|
||||||
tester.findButtonByLabel("disable").click()
|
await tester.findButtonByLabel("disable").click()
|
||||||
await assertSelect(["2"], true)
|
await assertSelect(["2"], true)
|
||||||
|
|
||||||
tester.findSelectByPlaceholder("choose one").select("1")
|
await tester.findSelectByPlaceholder("choose one").select("1")
|
||||||
await assertSelect(["2"], true)
|
await assertSelect(["2"], true)
|
||||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
@@ -119,33 +117,35 @@ test("multiple select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
tester.findSelectByPlaceholder("select").select("1", "3")
|
await tester.findSelectByPlaceholder("select").select("1", "3")
|
||||||
await assertSelect(expect.arrayContaining(["1", "3"]) as unknown as string[])
|
await assertSelect(expect.arrayContaining(["1", "3"]) as unknown as string[])
|
||||||
expect(onSelect).toHaveBeenCalledWith(
|
expect(onSelect).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ values: expect.arrayContaining(["1", "3"]) }),
|
expect.objectContaining({
|
||||||
|
values: expect.arrayContaining(["1", "3"]) as unknown,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
tester.findSelectByPlaceholder("select").select("2")
|
await tester.findSelectByPlaceholder("select").select("2")
|
||||||
await assertSelect(expect.arrayContaining(["2"]) as unknown as string[])
|
await assertSelect(expect.arrayContaining(["2"]) as unknown as string[])
|
||||||
expect(onSelect).toHaveBeenCalledWith(
|
expect(onSelect).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ values: expect.arrayContaining(["2"]) }),
|
expect.objectContaining({
|
||||||
|
values: expect.arrayContaining(["2"]) as unknown,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
tester.findSelectByPlaceholder("select").select()
|
await tester.findSelectByPlaceholder("select").select()
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ values: [] }))
|
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" />)
|
||||||
tester.findSelectByPlaceholder("select").select("something")
|
await tester.findSelectByPlaceholder("select").select("something")
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
content: "",
|
content: "",
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
import { logPretty } from "@reacord/helpers/log-pretty"
|
||||||
/* eslint-disable require-await */
|
import { omit } from "@reacord/helpers/omit"
|
||||||
import { nanoid } from "nanoid"
|
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
|
||||||
|
import { raise } from "@reacord/helpers/raise"
|
||||||
|
import { waitFor } from "@reacord/helpers/wait-for"
|
||||||
|
import { randomUUID } from "node:crypto"
|
||||||
import { setTimeout } from "node:timers/promises"
|
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 { logPretty } from "../helpers/log-pretty"
|
|
||||||
import { omit } from "../helpers/omit"
|
|
||||||
import { pruneNullishValues } from "../helpers/prune-nullish-values"
|
|
||||||
import { raise } from "../helpers/raise"
|
|
||||||
import { waitFor } from "../helpers/wait-for"
|
|
||||||
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"
|
||||||
@@ -23,18 +22,18 @@ 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>()
|
||||||
|
|
||||||
@@ -46,26 +45,28 @@ export class ReacordTester extends Reacord {
|
|||||||
return [...this.messageContainer]
|
return [...this.messageContainer]
|
||||||
}
|
}
|
||||||
|
|
||||||
override send(initialContent?: ReactNode): ReacordInstance {
|
public createChannelMessage(): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||||
initialContent,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override reply(initialContent?: ReactNode): ReacordInstance {
|
public createMessageReply(): ReacordInstance {
|
||||||
|
return this.createInstance(
|
||||||
|
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public createInteractionReply(
|
||||||
|
_options?: ComponentEventReplyOptions,
|
||||||
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
new InteractionReplyRenderer(
|
new InteractionReplyRenderer(
|
||||||
new TestCommandInteraction(this.messageContainer),
|
new TestCommandInteraction(this.messageContainer),
|
||||||
),
|
),
|
||||||
initialContent,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
|
|
||||||
return this.reply(initialContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertMessages(expected: MessageSample[]) {
|
assertMessages(expected: MessageSample[]) {
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
expect(this.sampleMessages()).toEqual(expected)
|
expect(this.sampleMessages()).toEqual(expected)
|
||||||
@@ -73,7 +74,7 @@ export class ReacordTester extends Reacord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
||||||
const instance = this.reply()
|
const instance = this.createInteractionReply()
|
||||||
instance.render(content)
|
instance.render(content)
|
||||||
await this.assertMessages(expected)
|
await this.assertMessages(expected)
|
||||||
instance.destroy()
|
instance.destroy()
|
||||||
@@ -175,9 +176,8 @@ class TestMessage implements Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCommandInteraction implements CommandInteraction {
|
class TestCommandInteraction implements InteractionReplyRendererImplementation {
|
||||||
readonly type = "command"
|
readonly interactionId = "test-command-interaction"
|
||||||
readonly id = "test-command-interaction"
|
|
||||||
readonly channelId = "test-channel-id"
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
constructor(private messageContainer: Container<TestMessage>) {}
|
constructor(private messageContainer: Container<TestMessage>) {}
|
||||||
@@ -194,7 +194,7 @@ class TestCommandInteraction implements CommandInteraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TestInteraction {
|
class TestInteraction {
|
||||||
readonly id = nanoid()
|
readonly id = randomUUID()
|
||||||
readonly channelId = "test-channel-id"
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -252,17 +252,19 @@ class TestSelectInteraction
|
|||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +276,10 @@ class TestSelectChangeEvent
|
|||||||
extends TestComponentEvent
|
extends TestComponentEvent
|
||||||
implements SelectChangeEvent
|
implements SelectChangeEvent
|
||||||
{
|
{
|
||||||
constructor(readonly values: string[], tester: ReacordTester) {
|
constructor(
|
||||||
|
readonly values: string[],
|
||||||
|
tester: ReacordTester,
|
||||||
|
) {
|
||||||
super(tester)
|
super(tester)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { test } from "vitest"
|
import { test } from "vitest"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
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 () => {
|
||||||
@@ -50,7 +49,9 @@ describe("useInstance", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
const instance = tester.send(<TestComponent name="parent" />)
|
const instance = tester
|
||||||
|
.createChannelMessage()
|
||||||
|
.render(<TestComponent name="parent" />)
|
||||||
|
|
||||||
await tester.assertMessages([messageOutput("parent")])
|
await tester.assertMessages([messageOutput("parent")])
|
||||||
expect(instanceFromHook).toBe(instance)
|
expect(instanceFromHook).toBe(instance)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json"
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/// <reference types="vitest" />
|
|
||||||
import { defineConfig } from "vite"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
build: {
|
|
||||||
sourcemap: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
3
packages/website/.gitignore
vendored
3
packages/website/.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
/.cache
|
/.cache
|
||||||
/build
|
/build
|
||||||
/public/build
|
/public/build
|
||||||
@@ -8,3 +7,5 @@ node_modules
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
*.out.css
|
*.out.css
|
||||||
|
/api
|
||||||
|
.astro
|
||||||
|
|||||||
@@ -1,5 +1,45 @@
|
|||||||
# website
|
# website
|
||||||
|
|
||||||
|
## 0.4.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [ced48a3]
|
||||||
|
- reacord@0.5.5
|
||||||
|
|
||||||
|
## 0.4.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [41c87e3]
|
||||||
|
- reacord@0.5.4
|
||||||
|
|
||||||
|
## 0.4.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [104b175]
|
||||||
|
- Updated dependencies [156cf90]
|
||||||
|
- Updated dependencies [0bab505]
|
||||||
|
- Updated dependencies [d76f316]
|
||||||
|
- reacord@0.5.3
|
||||||
|
|
||||||
|
## 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
|
## 0.4.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import { hydrate } from "react-dom"
|
|
||||||
import { RemixBrowser } from "@remix-run/react"
|
|
||||||
|
|
||||||
hydrate(<RemixBrowser />, document)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { renderToString } from "react-dom/server"
|
|
||||||
import type { EntryContext } from "@remix-run/node"
|
|
||||||
import { RemixServer } from "@remix-run/react"
|
|
||||||
|
|
||||||
export default function handleRequest(
|
|
||||||
request: Request,
|
|
||||||
responseStatusCode: number,
|
|
||||||
responseHeaders: Headers,
|
|
||||||
remixContext: EntryContext,
|
|
||||||
) {
|
|
||||||
const markup = renderToString(
|
|
||||||
<RemixServer context={remixContext} url={request.url} />,
|
|
||||||
)
|
|
||||||
|
|
||||||
responseHeaders.set("Content-Type", "text/html")
|
|
||||||
|
|
||||||
return new Response("<!DOCTYPE html>" + markup, {
|
|
||||||
status: responseStatusCode,
|
|
||||||
headers: responseHeaders,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { HeartIcon } from "@heroicons/react/solid"
|
|
||||||
import clsx from "clsx"
|
|
||||||
import { ExternalLink } from "~/modules/dom/external-link"
|
|
||||||
import { linkClass, maxWidthContainer } from "~/modules/ui/components"
|
|
||||||
|
|
||||||
export function AppFooter() {
|
|
||||||
return (
|
|
||||||
<footer className={clsx(maxWidthContainer, "text-xs opacity-75")}>
|
|
||||||
<address className="not-italic">
|
|
||||||
© {new Date().getFullYear()} itsMapleLeaf
|
|
||||||
</address>
|
|
||||||
<p>
|
|
||||||
Coded with <HeartIcon className="inline w-4 align-sub" /> using{" "}
|
|
||||||
<ExternalLink className={linkClass()} href="https://remix.run">
|
|
||||||
Remix
|
|
||||||
</ExternalLink>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Uses{" "}
|
|
||||||
<ExternalLink className={linkClass()} href="https://umami.is/">
|
|
||||||
umami
|
|
||||||
</ExternalLink>{" "}
|
|
||||||
for simple, non-identifying analytics.
|
|
||||||
</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