Compare commits
134 Commits
1.0.1
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f0d41615b | ||
|
|
35fb6b4381 | ||
|
|
9f3e47d327 | ||
|
|
e29678a6a3 | ||
|
|
eb4670ec43 | ||
|
|
22b2bc0a5e | ||
|
|
2a12b79ef7 | ||
|
|
9b6abe374a | ||
|
|
b649b2a923 | ||
|
|
b64c316dc9 | ||
|
|
bfeefb4c73 | ||
|
|
734fa06425 | ||
|
|
5a738b7c68 | ||
|
|
f03a88bd78 | ||
|
|
e53059ee08 | ||
|
|
099f7a3116 | ||
|
|
d2264a2a43 | ||
|
|
ef26f67c8e | ||
|
|
5b7ec655eb | ||
|
|
a381ba6320 | ||
|
|
4f2fc267f9 | ||
|
|
2687f98952 | ||
|
|
0a27632f05 | ||
|
|
6b9106c975 | ||
|
|
e6ad6a552e | ||
|
|
47bfaad508 | ||
|
|
61a06c3f1a | ||
|
|
b42c24295f | ||
|
|
71e689ece3 | ||
|
|
7bc806ec63 | ||
|
|
eab5ed033f | ||
|
|
ff57dbddba | ||
|
|
de207f0fa7 | ||
|
|
02eb7d83f6 | ||
|
|
2c50d9be6b | ||
|
|
594ef00d09 | ||
|
|
7626a1f461 | ||
|
|
83ca6f489e | ||
|
|
b7a10bb9ab | ||
|
|
f6be6d7cf8 | ||
|
|
dfbba41be4 | ||
|
|
024fc6853b | ||
|
|
5bd2d24c56 | ||
|
|
5c50bed33d | ||
|
|
07fd598bf9 | ||
|
|
a234e3a582 | ||
|
|
a77f2f9b4d | ||
|
|
787d5840d2 | ||
|
|
353507e17a | ||
|
|
1b41ca03d9 | ||
|
|
3ca2b12dfc | ||
|
|
7cf7402cea | ||
|
|
c2a729a5e0 | ||
|
|
e9977b2a76 | ||
|
|
a4b8fb5fc2 | ||
|
|
9a5653bc02 | ||
|
|
50fd3b2d11 | ||
|
|
58f294b790 | ||
|
|
e1d3ad1675 | ||
|
|
11a17feaae | ||
|
|
ca69a0b4c5 | ||
|
|
6e70b55d02 | ||
|
|
dedc1e0666 | ||
|
|
084c90613f | ||
|
|
dbe8a64ffc | ||
|
|
73fd35bf46 | ||
|
|
1a0b4b5703 | ||
|
|
62a427b7a7 | ||
|
|
64e7038532 | ||
|
|
7e88e733d5 | ||
|
|
f3822b625d | ||
|
|
df1b39256d | ||
|
|
fc8cfa5419 | ||
|
|
e3a526e2d7 | ||
|
|
4f3f6e26cf | ||
|
|
2214efe1bc | ||
|
|
030c211230 | ||
|
|
7f6db77c4f | ||
|
|
924448dc2c | ||
|
|
77c3f8d1bc | ||
|
|
80943afcf6 | ||
|
|
f792888268 | ||
|
|
aad9a30266 | ||
|
|
0ec7e5e116 | ||
|
|
2b65e98cd3 | ||
|
|
448722225e | ||
|
|
cb6d5a3828 | ||
|
|
354c22108e | ||
|
|
ec96dde694 | ||
|
|
445e9537d4 | ||
|
|
c12d40d0da | ||
|
|
7e20d5029e | ||
|
|
262b931810 | ||
|
|
4a85dd69cf | ||
|
|
34bb2bbc13 | ||
|
|
b541301cb1 | ||
|
|
a766183f01 | ||
|
|
47e3d0bdb5 | ||
|
|
c51b024329 | ||
|
|
e37140dcd0 | ||
|
|
705dcd001b | ||
|
|
f9c6f5cd9d | ||
|
|
f7ff7d0dfe | ||
|
|
29fb8e0064 | ||
|
|
4009b19f9c | ||
|
|
ec347f585d | ||
|
|
160d71684f | ||
|
|
79c90c1b00 | ||
|
|
23dec32476 | ||
|
|
a24e027a48 | ||
|
|
6e70937825 | ||
|
|
e4b61819d3 | ||
|
|
6bf58e8379 | ||
|
|
5dfe72c093 | ||
|
|
c4ac7a1b6d | ||
|
|
401b2d4990 | ||
|
|
0d3f29515e | ||
|
|
1664aaee15 | ||
|
|
8501e33103 | ||
|
|
a2a52e237d | ||
|
|
05516c5823 | ||
|
|
45692999bf | ||
|
|
1fcb3cedc1 | ||
|
|
0261fec676 | ||
|
|
0e86f19da2 | ||
|
|
31220db8fe | ||
|
|
66ae518e51 | ||
|
|
8d505c9564 | ||
|
|
5f2311b09c | ||
|
|
ce88fab714 | ||
|
|
b53d3bce13 | ||
|
|
c997286340 | ||
|
|
80f4bb3d98 | ||
|
|
1cfcb20d4d |
6
.gitmodules
vendored
@@ -1,3 +1,9 @@
|
|||||||
[submodule "external/lang"]
|
[submodule "external/lang"]
|
||||||
path = external/lang
|
path = external/lang
|
||||||
url = https://github.com/revoltchat/translations
|
url = https://github.com/revoltchat/translations
|
||||||
|
[submodule "external/components"]
|
||||||
|
path = external/components
|
||||||
|
url = https://github.com/revoltchat/components
|
||||||
|
[submodule "external/revolt.js"]
|
||||||
|
path = external/revolt.js
|
||||||
|
url = https://github.com/revoltchat/revolt.js
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ The following code is pending a partial or full rewrite:
|
|||||||
|
|
||||||
- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||||
- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||||
- `src/context/intermediate`: modal system is being rewritten from scratch
|
|
||||||
- `src/context/revoltjs`: client state management needs to be rewritten and include support for concurrent clients
|
|
||||||
- `src/lib`: this needs to be organised
|
- `src/lib`: this needs to be organised
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
@@ -45,6 +43,7 @@ Get revite up and running locally.
|
|||||||
git clone --recursive https://github.com/revoltchat/revite
|
git clone --recursive https://github.com/revoltchat/revite
|
||||||
cd revite
|
cd revite
|
||||||
yarn
|
yarn
|
||||||
|
yarn build:deps
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,6 +56,7 @@ You can now access the client at http://local.revolt.chat:3000.
|
|||||||
| `yarn pull` | Setup assets required for Revite. |
|
| `yarn pull` | Setup assets required for Revite. |
|
||||||
| `yarn dev` | Start the Revolt client in development mode. |
|
| `yarn dev` | Start the Revolt client in development mode. |
|
||||||
| `yarn build` | Build the Revolt client. |
|
| `yarn build` | Build the Revolt client. |
|
||||||
|
| `yarn build:deps` | Build external dependencies. |
|
||||||
| `yarn preview` | Start a local server with the built client. |
|
| `yarn preview` | Start a local server with the built client. |
|
||||||
| `yarn lint` | Run ESLint on the client. |
|
| `yarn lint` | Run ESLint on the client. |
|
||||||
| `yarn fmt` | Run Prettier on the client. |
|
| `yarn fmt` | Run Prettier on the client. |
|
||||||
|
|||||||
1
external/components
vendored
Submodule
2
external/lang
vendored
1
external/revolt.js
vendored
Submodule
49
package.json
@@ -3,7 +3,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/setup_assets.js --check && vite",
|
"dev": "node scripts/setup_assets.js --check && vite",
|
||||||
"pull": "node scripts/setup_assets.js",
|
"pull": "node scripts/setup_assets.js",
|
||||||
"build": "rimraf build && node scripts/setup_assets.js --check && vite build",
|
"build:deps": "cd external && cd components && yarn && yarn build:esm && cd .. && cd revolt.js && yarn && yarn build",
|
||||||
|
"build": "yarn && rimraf build && node scripts/setup_assets.js --check && yarn build:deps && vite build",
|
||||||
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
|
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
|
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
|
||||||
@@ -46,10 +47,12 @@
|
|||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"klaw": "^3.0.0",
|
"klaw": "^3.0.0",
|
||||||
"sirv-cli": "^1.0.14",
|
"sirv-cli": "^1.0.14",
|
||||||
"vite": "^2.6.14"
|
"vite": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-decorators": "^7.17.9",
|
"@babel/plugin-proposal-decorators": "^7.17.9",
|
||||||
|
"@floating-ui/react-dom": "^1.0.0",
|
||||||
|
"@floating-ui/react-dom-interactions": "^0.9.1",
|
||||||
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
||||||
"@fontsource/bitter": "^4.5.7",
|
"@fontsource/bitter": "^4.5.7",
|
||||||
"@fontsource/comic-neue": "^4.4.5",
|
"@fontsource/comic-neue": "^4.4.5",
|
||||||
@@ -70,25 +73,25 @@
|
|||||||
"@fontsource/space-mono": "^4.4.5",
|
"@fontsource/space-mono": "^4.4.5",
|
||||||
"@fontsource/ubuntu": "^4.4.5",
|
"@fontsource/ubuntu": "^4.4.5",
|
||||||
"@fontsource/ubuntu-mono": "^4.4.5",
|
"@fontsource/ubuntu-mono": "^4.4.5",
|
||||||
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
"@hcaptcha/react-hcaptcha": "^1.4.4",
|
||||||
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
||||||
"@preact/preset-vite": "^2.0.0",
|
"@preact/preset-vite": "^2.0.0",
|
||||||
"@revoltchat/ui": "1.0.46",
|
"@revoltchat/ui": "^1.0.77",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@styled-icons/boxicons-logos": "^10.38.0",
|
"@styled-icons/boxicons-logos": "^10.38.0",
|
||||||
"@styled-icons/boxicons-regular": "^10.38.0",
|
"@styled-icons/boxicons-regular": "^10.38.0",
|
||||||
"@styled-icons/boxicons-solid": "^10.38.0",
|
"@styled-icons/boxicons-solid": "^10.38.0",
|
||||||
"@styled-icons/simple-icons": "^10.33.0",
|
"@styled-icons/simple-icons": "^10.45.0",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@traptitech/markdown-it-katex": "^3.4.3",
|
"@traptitech/markdown-it-katex": "^3.4.3",
|
||||||
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
||||||
|
"@types/lodash": "^4",
|
||||||
"@types/lodash.defaultsdeep": "^4.6.6",
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
||||||
"@types/lodash.isequal": "^4.5.5",
|
"@types/lodash.isequal": "^4.5.5",
|
||||||
"@types/markdown-it": "^12.0.2",
|
"@types/node": "^15.14.9",
|
||||||
"@types/node": "^15.12.4",
|
|
||||||
"@types/preact-i18n": "^2.3.0",
|
"@types/preact-i18n": "^2.3.0",
|
||||||
"@types/prismjs": "^1.16.5",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react-beautiful-dnd": "^13",
|
"@types/react-beautiful-dnd": "^13",
|
||||||
"@types/react-helmet": "^6.1.1",
|
"@types/react-helmet": "^6.1.1",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
@@ -106,15 +109,16 @@
|
|||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-config-preact": "^1.1.4",
|
"eslint-config-preact": "^1.1.4",
|
||||||
"eslint-plugin-jsdoc": "^39.3.2",
|
"eslint-plugin-jsdoc": "^39.3.2",
|
||||||
|
"eslint-plugin-mobx": "^0.0.8",
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
"history": "4",
|
"history": "4",
|
||||||
"json-stringify-deterministic": "^1.0.2",
|
"json-stringify-deterministic": "^1.0.2",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lodash.defaultsdeep": "^4.6.1",
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"long": "^5.2.0",
|
"long": "^5.2.0",
|
||||||
"markdown-it": "^12.0.6",
|
"mdast-util-to-hast": "^12.1.2",
|
||||||
"markdown-it-emoji": "^2.0.0",
|
|
||||||
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
||||||
"mobx": "^6.6.0",
|
"mobx": "^6.6.0",
|
||||||
"mobx-react-lite": "3.4.0",
|
"mobx-react-lite": "3.4.0",
|
||||||
@@ -122,7 +126,7 @@
|
|||||||
"preact-context-menu": "0.4.1",
|
"preact-context-menu": "0.4.1",
|
||||||
"preact-i18n": "^2.4.0-preactx",
|
"preact-i18n": "^2.4.0-preactx",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.28.0",
|
||||||
"qrcode.react": "^3.0.2",
|
"qrcode.react": "^3.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-device-detect": "2.2.2",
|
"react-device-detect": "2.2.2",
|
||||||
@@ -132,17 +136,30 @@
|
|||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scroll": "^1.8.2",
|
"react-scroll": "^1.8.2",
|
||||||
"react-virtuoso": "^2.12.0",
|
"react-virtuoso": "^2.12.0",
|
||||||
"revolt.js": "6.0.3",
|
"rehype-katex": "^6.0.2",
|
||||||
|
"rehype-prism": "^2.1.3",
|
||||||
|
"rehype-react": "^7.1.1",
|
||||||
|
"remark-breaks": "^3.0.2",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-parse": "^10.0.1",
|
||||||
|
"remark-rehype": "^10.1.0",
|
||||||
|
"revolt.js": "6.0.17",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.35.1",
|
"sass": "^1.35.1",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"shade-blend-color": "^1.0.0",
|
"shade-blend-color": "^1.0.0",
|
||||||
|
"slate": "^0.81.1",
|
||||||
|
"slate-history": "^0.66.0",
|
||||||
|
"slate-react": "^0.81.0",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"ulid": "^2.3.0",
|
"ulid": "^2.3.0",
|
||||||
|
"unified": "^10.1.2",
|
||||||
|
"unist-util-visit": "^4.1.0",
|
||||||
"use-resize-observer": "^7.0.0",
|
"use-resize-observer": "^7.0.0",
|
||||||
"vite-plugin-pwa": "^0.11.13",
|
"vite-plugin-pwa": "^0.12.3",
|
||||||
"workbox-precaching": "^6.1.5"
|
"workbox-precaching": "^6.1.5"
|
||||||
},
|
},
|
||||||
"name": "client",
|
"name": "client",
|
||||||
@@ -150,5 +167,9 @@
|
|||||||
"repository": "https://github.com/revoltchat/revite.git",
|
"repository": "https://github.com/revoltchat/revite.git",
|
||||||
"author": "Paul <paulmakles@gmail.com>",
|
"author": "Paul <paulmakles@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "yarn@3.2.0"
|
"packageManager": "yarn@3.2.0",
|
||||||
|
"resolutions": {
|
||||||
|
"@revoltchat/ui": "portal:external/components",
|
||||||
|
"revolt.js": "portal:external/revolt.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/components
Submodule
1
packages/hast-util-table-cell-style
Submodule
1
packages/revolt.js
Submodule
@@ -14,8 +14,10 @@ POST_INSTALL="pm2 restart revite"
|
|||||||
export REVOLT_SAAS=https://github.com/revoltchat/assets
|
export REVOLT_SAAS=https://github.com/revoltchat/assets
|
||||||
|
|
||||||
|
|
||||||
|
# Exit when any command fails
|
||||||
|
set -e
|
||||||
|
|
||||||
# 1. Build Revite
|
# 1. Build Revite
|
||||||
yarn
|
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
# 2. Archive built files
|
# 2. Archive built files
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ export const emojiDictionary = {
|
|||||||
hole: "🕳️",
|
hole: "🕳️",
|
||||||
bomb: "💣",
|
bomb: "💣",
|
||||||
speech_balloon: "💬",
|
speech_balloon: "💬",
|
||||||
eye_speech_bubble: "👁️🗨️",
|
|
||||||
left_speech_bubble: "🗨️",
|
left_speech_bubble: "🗨️",
|
||||||
right_anger_bubble: "🗯️",
|
right_anger_bubble: "🗯️",
|
||||||
thought_balloon: "💭",
|
thought_balloon: "💭",
|
||||||
@@ -1952,5 +1951,6 @@ export const emojiDictionary = {
|
|||||||
huggies: "custom:huggies.png",
|
huggies: "custom:huggies.png",
|
||||||
noted: "custom:noted.gif",
|
noted: "custom:noted.gif",
|
||||||
waving: "custom:waving.png",
|
waving: "custom:waving.png",
|
||||||
|
mogusvented: "custom:mogusvented.png",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { Channel, User } from "revolt.js";
|
import { Channel, User } from "revolt.js";
|
||||||
|
import { Emoji as CustomEmoji } from "revolt.js/esm/maps/Emojis";
|
||||||
import styled, { css } from "styled-components/macro";
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
import { StateUpdater, useState } from "preact/hooks";
|
import { StateUpdater, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import ChannelIcon from "./ChannelIcon";
|
import ChannelIcon from "./ChannelIcon";
|
||||||
import Emoji from "./Emoji";
|
import Emoji from "./Emoji";
|
||||||
|
import ServerIcon from "./ServerIcon";
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
import UserIcon from "./user/UserIcon";
|
import UserIcon from "./user/UserIcon";
|
||||||
|
|
||||||
export type AutoCompleteState =
|
export type AutoCompleteState =
|
||||||
@@ -15,7 +19,7 @@ export type AutoCompleteState =
|
|||||||
| ({ selected: number; within: boolean } & (
|
| ({ selected: number; within: boolean } & (
|
||||||
| {
|
| {
|
||||||
type: "emoji";
|
type: "emoji";
|
||||||
matches: string[];
|
matches: (string | CustomEmoji)[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "user";
|
type: "user";
|
||||||
@@ -105,16 +109,23 @@ export function useAutoComplete(
|
|||||||
|
|
||||||
if (type === "emoji") {
|
if (type === "emoji") {
|
||||||
// ! TODO: we should convert it to a Binary Search Tree and use that
|
// ! TODO: we should convert it to a Binary Search Tree and use that
|
||||||
const matches = Object.keys(emojiDictionary)
|
const matches = [
|
||||||
.filter((emoji: string) => emoji.match(regex))
|
...Object.keys(emojiDictionary).filter((emoji: string) =>
|
||||||
.splice(0, 5);
|
emoji.match(regex),
|
||||||
|
),
|
||||||
|
...Array.from(client.emojis.values()).filter((emoji) =>
|
||||||
|
emoji.name.match(regex),
|
||||||
|
),
|
||||||
|
].splice(0, 5);
|
||||||
|
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
const currentPosition =
|
const currentPosition =
|
||||||
state.type !== "none" ? state.selected : 0;
|
state.type !== "none" ? state.selected : 0;
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
// @ts-ignore-next-line are you high
|
||||||
type: "emoji",
|
type: "emoji",
|
||||||
|
// @ts-ignore-next-line
|
||||||
matches,
|
matches,
|
||||||
selected: Math.min(currentPosition, matches.length - 1),
|
selected: Math.min(currentPosition, matches.length - 1),
|
||||||
within: false,
|
within: false,
|
||||||
@@ -234,10 +245,13 @@ export function useAutoComplete(
|
|||||||
|
|
||||||
const content = el.value.split("");
|
const content = el.value.split("");
|
||||||
if (state.type === "emoji") {
|
if (state.type === "emoji") {
|
||||||
|
const selected = state.matches[state.selected];
|
||||||
content.splice(
|
content.splice(
|
||||||
index,
|
index,
|
||||||
search.length,
|
search.length,
|
||||||
state.matches[state.selected],
|
selected instanceof CustomEmoji
|
||||||
|
? selected._id
|
||||||
|
: selected,
|
||||||
": ",
|
": ",
|
||||||
);
|
);
|
||||||
} else if (state.type === "user") {
|
} else if (state.type === "user") {
|
||||||
@@ -389,12 +403,17 @@ export default function AutoComplete({
|
|||||||
setState,
|
setState,
|
||||||
onClick,
|
onClick,
|
||||||
}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
|
}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
|
||||||
|
const client = useClient();
|
||||||
return (
|
return (
|
||||||
<Base detached={detached}>
|
<Base detached={detached}>
|
||||||
<div>
|
<div>
|
||||||
{state.type === "emoji" &&
|
{state.type === "emoji" &&
|
||||||
state.matches.map((match, i) => (
|
state.matches.map((match, i) => (
|
||||||
<button
|
<button
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
key={match}
|
key={match}
|
||||||
className={i === state.selected ? "active" : ""}
|
className={i === state.selected ? "active" : ""}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
@@ -413,15 +432,61 @@ export default function AutoComplete({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
<Emoji
|
<div
|
||||||
emoji={
|
style={{
|
||||||
(emojiDictionary as Record<string, string>)[
|
display: "flex",
|
||||||
match
|
flexDirection: "row",
|
||||||
]
|
justifyContent: "center",
|
||||||
}
|
}}>
|
||||||
size={20}
|
{match instanceof CustomEmoji ? (
|
||||||
/>
|
<img
|
||||||
:{match}:
|
loading="lazy"
|
||||||
|
src={match.imageURL}
|
||||||
|
style={{
|
||||||
|
width: `20px`,
|
||||||
|
height: `20px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Emoji
|
||||||
|
emoji={
|
||||||
|
(
|
||||||
|
emojiDictionary as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>
|
||||||
|
)[match]
|
||||||
|
}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span style={{ paddingLeft: "4px" }}>{`:${
|
||||||
|
match instanceof CustomEmoji
|
||||||
|
? match.name
|
||||||
|
: match
|
||||||
|
}:`}</span>
|
||||||
|
</div>
|
||||||
|
{match instanceof CustomEmoji &&
|
||||||
|
match.parent.type == "Server" && (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
client.servers.get(
|
||||||
|
match.parent.id,
|
||||||
|
)?.name
|
||||||
|
}>
|
||||||
|
<Link
|
||||||
|
to={`/server/${match.parent.id}`}>
|
||||||
|
<ServerIcon
|
||||||
|
target={client.servers.get(
|
||||||
|
match.parent.id,
|
||||||
|
)}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{state.type === "user" &&
|
{state.type === "user" &&
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Channel } from "revolt.js";
|
import { Channel } from "revolt.js";
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import fallback from "./assets/group.png";
|
import fallback from "./assets/group.png";
|
||||||
|
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
||||||
|
|
||||||
interface Props extends IconBaseProps<Channel> {
|
interface Props extends IconBaseProps<Channel> {
|
||||||
@@ -22,7 +19,7 @@ export default observer(
|
|||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
|
||||||
export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji";
|
export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji";
|
||||||
|
|
||||||
let EMOJI_PACK: EmojiPack = "mutant";
|
let EMOJI_PACK: EmojiPack = "mutant";
|
||||||
@@ -40,7 +42,7 @@ function toCodePoint(rune: string) {
|
|||||||
.join("-");
|
.join("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEmoji(emoji: string) {
|
export function parseEmoji(emoji: string) {
|
||||||
if (emoji.startsWith("custom:")) {
|
if (emoji.startsWith("custom:")) {
|
||||||
return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
|
return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
|
||||||
7,
|
7,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Text } from "preact-i18n";
|
|||||||
|
|
||||||
import { IconButton } from "@revoltchat/ui";
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -60,6 +61,9 @@ const ServerBanner = styled.div<Omit<Props, "server">>`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -121,7 +125,13 @@ export default observer(({ server }: Props) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<div className="title">{server.name}</div>
|
<a
|
||||||
|
className="title"
|
||||||
|
onClick={() =>
|
||||||
|
modalController.push({ type: "server_info", server })
|
||||||
|
}>
|
||||||
|
{server.name}
|
||||||
|
</a>
|
||||||
{server.havePermission("ManageServer") && (
|
{server.havePermission("ManageServer") && (
|
||||||
<Link to={`/server/${server._id}/settings`}>
|
<Link to={`/server/${server._id}/settings`}>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import styled from "styled-components/macro";
|
|||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
|
||||||
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
||||||
|
|
||||||
interface Props extends IconBaseProps<Server> {
|
interface Props extends IconBaseProps<Server> {
|
||||||
@@ -34,7 +33,7 @@ export default observer(
|
|||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const { target, attachment, size, animate, server_name, ...imgProps } =
|
const { target, attachment, size, animate, server_name, ...imgProps } =
|
||||||
props;
|
props;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTriggerEvents } from "preact-context-menu";
|
|||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category } from "@revoltchat/ui";
|
import { Category, Button } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
@@ -13,9 +13,8 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|||||||
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { I18nError } from "../../../context/Locale";
|
import { I18nError } from "../../../context/Locale";
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import Markdown from "../../markdown/Markdown";
|
import Markdown from "../../markdown/Markdown";
|
||||||
import UserIcon from "../user/UserIcon";
|
import UserIcon from "../user/UserIcon";
|
||||||
import { Username } from "../user/UserShort";
|
import { Username } from "../user/UserShort";
|
||||||
@@ -26,6 +25,7 @@ import MessageBase, {
|
|||||||
} from "./MessageBase";
|
} from "./MessageBase";
|
||||||
import Attachment from "./attachments/Attachment";
|
import Attachment from "./attachments/Attachment";
|
||||||
import { MessageReply } from "./attachments/MessageReply";
|
import { MessageReply } from "./attachments/MessageReply";
|
||||||
|
import { Reactions } from "./attachments/Reactions";
|
||||||
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
|
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
|
||||||
import Embed from "./embed/Embed";
|
import Embed from "./embed/Embed";
|
||||||
import InviteList from "./embed/EmbedInvite";
|
import InviteList from "./embed/EmbedInvite";
|
||||||
@@ -52,11 +52,9 @@ const Message = observer(
|
|||||||
queued,
|
queued,
|
||||||
hideReply,
|
hideReply,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const client = useClient();
|
const client = message.client;
|
||||||
const user = message.author;
|
const user = message.author;
|
||||||
|
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
const head =
|
const head =
|
||||||
preferHead || (message.reply_ids && message.reply_ids.length > 0);
|
preferHead || (message.reply_ids && message.reply_ids.length > 0);
|
||||||
@@ -70,7 +68,10 @@ const Message = observer(
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const openProfile = () =>
|
const openProfile = () =>
|
||||||
openScreen({ id: "profile", user_id: message.author_id });
|
modalController.push({
|
||||||
|
type: "user_profile",
|
||||||
|
user_id: message.author_id,
|
||||||
|
});
|
||||||
|
|
||||||
const handleUserClick = (e: MouseEvent) => {
|
const handleUserClick = (e: MouseEvent) => {
|
||||||
if (e.shiftKey && user?._id) {
|
if (e.shiftKey && user?._id) {
|
||||||
@@ -87,6 +88,7 @@ const Message = observer(
|
|||||||
|
|
||||||
// ! FIXME(?): animate on hover
|
// ! FIXME(?): animate on hover
|
||||||
const [mouseHovering, setAnimate] = useState(false);
|
const [mouseHovering, setAnimate] = useState(false);
|
||||||
|
const [reactionsOpen, setReactionsOpen] = useState(false);
|
||||||
useEffect(() => setAnimate(false), [replacement]);
|
useEffect(() => setAnimate(false), [replacement]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +117,11 @@ const Message = observer(
|
|||||||
}
|
}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
sending={typeof queued !== "undefined"}
|
sending={typeof queued !== "undefined"}
|
||||||
mention={message.mention_ids?.includes(client.user!._id)}
|
mention={
|
||||||
|
message.mention_ids && client.user
|
||||||
|
? message.mention_ids.includes(client.user._id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
failed={typeof queued?.error !== "undefined"}
|
failed={typeof queued?.error !== "undefined"}
|
||||||
{...(attachContext
|
{...(attachContext
|
||||||
? useTriggerEvents("Menu", {
|
? useTriggerEvents("Menu", {
|
||||||
@@ -159,7 +165,8 @@ const Message = observer(
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{replacement ?? <Markdown content={content} />}
|
{replacement ??
|
||||||
|
(content && <Markdown content={content} />)}
|
||||||
{!queued && <InviteList message={message} />}
|
{!queued && <InviteList message={message} />}
|
||||||
{queued?.error && (
|
{queued?.error && (
|
||||||
<Category>
|
<Category>
|
||||||
@@ -179,10 +186,13 @@ const Message = observer(
|
|||||||
{message.embeds?.map((embed, index) => (
|
{message.embeds?.map((embed, index) => (
|
||||||
<Embed key={index} embed={embed} />
|
<Embed key={index} embed={embed} />
|
||||||
))}
|
))}
|
||||||
{mouseHovering &&
|
<Reactions message={message} />
|
||||||
|
{(mouseHovering || reactionsOpen) &&
|
||||||
!replacement &&
|
!replacement &&
|
||||||
!isTouchscreenDevice && (
|
!isTouchscreenDevice && (
|
||||||
<MessageOverlayBar
|
<MessageOverlayBar
|
||||||
|
reactionsOpen={reactionsOpen}
|
||||||
|
setReactionsOpen={setReactionsOpen}
|
||||||
message={message}
|
message={message}
|
||||||
queued={queued}
|
queued={queued}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
|
import { HappyBeaming, Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||||
import Axios, { CancelTokenSource } from "axios";
|
import Axios, { CancelTokenSource } from "axios";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Channel } from "revolt.js";
|
import { Channel } from "revolt.js";
|
||||||
@@ -6,9 +6,10 @@ import styled, { css } from "styled-components/macro";
|
|||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
import { memo } from "preact/compat";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { IconButton } from "@revoltchat/ui";
|
import { IconButton, Picker } from "@revoltchat/ui";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
import { debounce } from "../../../lib/debounce";
|
import { debounce } from "../../../lib/debounce";
|
||||||
@@ -21,18 +22,24 @@ import {
|
|||||||
SMOOTH_SCROLL_ON_RECEIVE,
|
SMOOTH_SCROLL_ON_RECEIVE,
|
||||||
} from "../../../lib/renderer/Singleton";
|
} from "../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { state, useApplicationState } from "../../../mobx/State";
|
||||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
import { dayjs } from "../../../context/Locale";
|
||||||
|
|
||||||
|
import { emojiDictionary } from "../../../assets/emojis";
|
||||||
|
import {
|
||||||
|
clientController,
|
||||||
|
useClient,
|
||||||
|
} from "../../../controllers/client/ClientController";
|
||||||
|
import { takeError } from "../../../controllers/client/jsx/error";
|
||||||
import {
|
import {
|
||||||
FileUploader,
|
FileUploader,
|
||||||
grabFiles,
|
grabFiles,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
} from "../../../context/revoltjs/FileUploads";
|
} from "../../../controllers/client/jsx/legacy/FileUploads";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
import { RenderEmoji } from "../../markdown/plugins/emoji";
|
||||||
|
|
||||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||||
import { PermissionTooltip } from "../Tooltip";
|
import { PermissionTooltip } from "../Tooltip";
|
||||||
import FilePreview from "./bars/FilePreview";
|
import FilePreview from "./bars/FilePreview";
|
||||||
@@ -127,6 +134,10 @@ const FileAction = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FloatingLayer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
|
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
`;
|
`;
|
||||||
@@ -137,6 +148,67 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
|
|||||||
// Tests for code block delimiters (``` at start of line)
|
// Tests for code block delimiters (``` at start of line)
|
||||||
const RE_CODE_DELIMITER = new RegExp("^```", "gm");
|
const RE_CODE_DELIMITER = new RegExp("^```", "gm");
|
||||||
|
|
||||||
|
export const HackAlertThisFileWillBeReplaced = observer(
|
||||||
|
({
|
||||||
|
onSelect,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
onSelect: (emoji: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const renderEmoji = useMemo(
|
||||||
|
() =>
|
||||||
|
memo(({ emoji }: { emoji: string }) => (
|
||||||
|
<RenderEmoji match={emoji} {...({} as any)} />
|
||||||
|
)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const emojis: Record<string, any> = {
|
||||||
|
default: Object.keys(emojiDictionary).map((id) => ({ id })),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ! FIXME: also expose typing from component
|
||||||
|
const categories: any[] = [];
|
||||||
|
|
||||||
|
for (const server of state.ordering.orderedServers) {
|
||||||
|
// ! FIXME: add a separate map on each server for emoji
|
||||||
|
const list = [...clientController.getReadyClient()!.emojis.values()]
|
||||||
|
.filter(
|
||||||
|
(emoji) =>
|
||||||
|
emoji.parent.type !== "Detached" &&
|
||||||
|
emoji.parent.id === server._id,
|
||||||
|
)
|
||||||
|
.map(({ _id, name }) => ({ id: _id, name }));
|
||||||
|
|
||||||
|
if (list.length > 0) {
|
||||||
|
emojis[server._id] = list;
|
||||||
|
categories.push({
|
||||||
|
id: server._id,
|
||||||
|
name: server.name,
|
||||||
|
iconURL: server.generateIconURL({ max_side: 256 }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.push({
|
||||||
|
id: "default",
|
||||||
|
name: "Default",
|
||||||
|
emoji: "smiley",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Picker
|
||||||
|
emojis={emojis}
|
||||||
|
categories={categories}
|
||||||
|
renderEmoji={renderEmoji}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ! FIXME: add to app config and load from app config
|
// ! FIXME: add to app config and load from app config
|
||||||
export const CAN_UPLOAD_AT_ONCE = 5;
|
export const CAN_UPLOAD_AT_ONCE = 5;
|
||||||
|
|
||||||
@@ -148,11 +220,42 @@ export default observer(({ channel }: Props) => {
|
|||||||
});
|
});
|
||||||
const [typing, setTyping] = useState<boolean | number>(false);
|
const [typing, setTyping] = useState<boolean | number>(false);
|
||||||
const [replies, setReplies] = useState<Reply[]>([]);
|
const [replies, setReplies] = useState<Reply[]>([]);
|
||||||
const client = useContext(AppContext);
|
const [picker, setPicker] = useState(false);
|
||||||
|
const client = useClient();
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
|
|
||||||
|
const closePicker = useCallback(() => setPicker(false), []);
|
||||||
|
|
||||||
const renderer = getRenderer(channel);
|
const renderer = getRenderer(channel);
|
||||||
|
|
||||||
|
if (channel.server?.member?.timeout) {
|
||||||
|
return (
|
||||||
|
<Base>
|
||||||
|
<Blocked>
|
||||||
|
<Action>
|
||||||
|
<PermissionTooltip
|
||||||
|
permission="SendMessages"
|
||||||
|
placement="top">
|
||||||
|
<ShieldX size={22} />
|
||||||
|
</PermissionTooltip>
|
||||||
|
</Action>
|
||||||
|
<div className="text">
|
||||||
|
<Text
|
||||||
|
id="app.main.channel.misc.timed_out"
|
||||||
|
fields={{
|
||||||
|
// TODO: make this reactive
|
||||||
|
time: dayjs().to(
|
||||||
|
channel.server.member.timeout,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Blocked>
|
||||||
|
</Base>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!channel.havePermission("SendMessage")) {
|
if (!channel.havePermission("SendMessage")) {
|
||||||
return (
|
return (
|
||||||
<Base>
|
<Base>
|
||||||
@@ -299,6 +402,7 @@ export default observer(({ channel }: Props) => {
|
|||||||
async function sendFile(content: string) {
|
async function sendFile(content: string) {
|
||||||
if (uploadState.type !== "attached") return;
|
if (uploadState.type !== "attached") return;
|
||||||
const attachments: string[] = [];
|
const attachments: string[] = [];
|
||||||
|
setMessage;
|
||||||
|
|
||||||
const cancel = Axios.CancelToken.source();
|
const cancel = Axios.CancelToken.source();
|
||||||
const files = uploadState.files;
|
const files = uploadState.files;
|
||||||
@@ -498,6 +602,20 @@ export default observer(({ channel }: Props) => {
|
|||||||
replies={replies}
|
replies={replies}
|
||||||
setReplies={setReplies}
|
setReplies={setReplies}
|
||||||
/>
|
/>
|
||||||
|
<FloatingLayer>
|
||||||
|
{picker && (
|
||||||
|
<HackAlertThisFileWillBeReplaced
|
||||||
|
onSelect={(emoji) => {
|
||||||
|
const v = state.draft.get(channel._id);
|
||||||
|
state.draft.set(
|
||||||
|
channel._id,
|
||||||
|
`${v ? `${v} ` : ""}:${emoji}:`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onClose={closePicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FloatingLayer>
|
||||||
<Base>
|
<Base>
|
||||||
{channel.havePermission("UploadFiles") ? (
|
{channel.havePermission("UploadFiles") ? (
|
||||||
<FileAction>
|
<FileAction>
|
||||||
@@ -618,16 +736,13 @@ export default observer(({ channel }: Props) => {
|
|||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
{/*<Action>
|
{state.experiments.isEnabled("picker") && (
|
||||||
<IconButton>
|
<Action>
|
||||||
<Box size={24} />
|
<IconButton onClick={() => setPicker(!picker)}>
|
||||||
</IconButton>
|
<HappyBeaming size={24} />
|
||||||
</Action>
|
</IconButton>
|
||||||
<Action>
|
</Action>
|
||||||
<IconButton>
|
)}
|
||||||
<HappyBeaming size={24} />
|
|
||||||
</IconButton>
|
|
||||||
</Action>*/}
|
|
||||||
<Action>
|
<Action>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={
|
className={
|
||||||
|
|||||||
@@ -9,15 +9,26 @@ import {
|
|||||||
EditAlt,
|
EditAlt,
|
||||||
Edit,
|
Edit,
|
||||||
MessageSquareEdit,
|
MessageSquareEdit,
|
||||||
|
Key,
|
||||||
} from "@styled-icons/boxicons-solid";
|
} from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Message, API } from "revolt.js";
|
import { Message, API } from "revolt.js";
|
||||||
import styled from "styled-components/macro";
|
import styled from "styled-components/macro";
|
||||||
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
import { useTriggerEvents } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Row } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { TextReact } from "../../../lib/i18n";
|
import { TextReact } from "../../../lib/i18n";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import { dayjs } from "../../../context/Locale";
|
||||||
|
|
||||||
|
import Markdown from "../../markdown/Markdown";
|
||||||
|
import Tooltip from "../Tooltip";
|
||||||
import UserShort from "../user/UserShort";
|
import UserShort from "../user/UserShort";
|
||||||
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
|
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
|
||||||
|
|
||||||
@@ -67,12 +78,17 @@ const iconDictionary = {
|
|||||||
channel_renamed: EditAlt,
|
channel_renamed: EditAlt,
|
||||||
channel_description_changed: Edit,
|
channel_description_changed: Edit,
|
||||||
channel_icon_changed: MessageSquareEdit,
|
channel_icon_changed: MessageSquareEdit,
|
||||||
|
channel_ownership_changed: Key,
|
||||||
text: InfoCircle,
|
text: InfoCircle,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SystemMessage = observer(
|
export const SystemMessage = observer(
|
||||||
({ attachContext, message, highlight, hideInfo }: Props) => {
|
({ attachContext, message, highlight, hideInfo }: Props) => {
|
||||||
const data = message.asSystemMessage;
|
const data = message.asSystemMessage;
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
const SystemMessageIcon =
|
const SystemMessageIcon =
|
||||||
iconDictionary[data.type as API.SystemMessage["type"]] ??
|
iconDictionary[data.type as API.SystemMessage["type"]] ??
|
||||||
InfoCircle;
|
InfoCircle;
|
||||||
@@ -98,16 +114,39 @@ export const SystemMessage = observer(
|
|||||||
case "user_joined":
|
case "user_joined":
|
||||||
case "user_left":
|
case "user_left":
|
||||||
case "user_kicked":
|
case "user_kicked":
|
||||||
case "user_banned":
|
case "user_banned": {
|
||||||
|
const createdAt = data.user ? decodeTime(data.user._id) : null;
|
||||||
children = (
|
children = (
|
||||||
<TextReact
|
<Row centred>
|
||||||
id={`app.main.channel.system.${data.type}`}
|
<TextReact
|
||||||
fields={{
|
id={`app.main.channel.system.${data.type}`}
|
||||||
user: <UserShort user={data.user} />,
|
fields={{
|
||||||
}}
|
user: <UserShort user={data.user} />,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
{data.type == "user_joined" &&
|
||||||
|
createdAt &&
|
||||||
|
(settings.get("appearance:show_account_age") ||
|
||||||
|
Date.now() - createdAt <
|
||||||
|
1000 * 60 * 60 * 24 * 7) && (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Text
|
||||||
|
id="app.main.channel.system.registered_at"
|
||||||
|
fields={{
|
||||||
|
time: dayjs(
|
||||||
|
createdAt,
|
||||||
|
).fromNow(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<InfoCircle size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "channel_renamed":
|
case "channel_renamed":
|
||||||
children = (
|
children = (
|
||||||
<TextReact
|
<TextReact
|
||||||
@@ -130,6 +169,22 @@ export const SystemMessage = observer(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "channel_ownership_changed":
|
||||||
|
children = (
|
||||||
|
<TextReact
|
||||||
|
id={`app.main.channel.system.channel_ownership_changed`}
|
||||||
|
fields={{
|
||||||
|
from: <UserShort user={data.from} />,
|
||||||
|
to: <UserShort user={data.to} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
if (message.system?.type === "text") {
|
||||||
|
children = <Markdown content={message.system?.content} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { API } from "revolt.js";
|
|||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTriggerEvents } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import AttachmentActions from "./AttachmentActions";
|
import AttachmentActions from "./AttachmentActions";
|
||||||
import { SizedGrid } from "./Grid";
|
import { SizedGrid } from "./Grid";
|
||||||
import ImageFile from "./ImageFile";
|
import ImageFile from "./ImageFile";
|
||||||
@@ -21,7 +20,7 @@ interface Props {
|
|||||||
const MAX_ATTACHMENT_WIDTH = 480;
|
const MAX_ATTACHMENT_WIDTH = 480;
|
||||||
|
|
||||||
export default function Attachment({ attachment, hasContent }: Props) {
|
export default function Attachment({ attachment, hasContent }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { filename, metadata } = attachment;
|
const { filename, metadata } = attachment;
|
||||||
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ import { IconButton } from "@revoltchat/ui";
|
|||||||
|
|
||||||
import { determineFileSize } from "../../../../lib/fileSize";
|
import { determineFileSize } from "../../../../lib/fileSize";
|
||||||
|
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: API.File;
|
attachment: API.File;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttachmentActions({ attachment }: Props) {
|
export default function AttachmentActions({ attachment }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { filename, metadata, size } = attachment;
|
const { filename, metadata, size } = attachment;
|
||||||
|
|
||||||
const url = client.generateFileURL(attachment);
|
const url = client.generateFileURL(attachment);
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { API } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
enum ImageLoadingState {
|
enum ImageLoadingState {
|
||||||
Loading,
|
Loading,
|
||||||
@@ -19,8 +19,7 @@ type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
|||||||
|
|
||||||
export default function ImageFile({ attachment, ...props }: Props) {
|
export default function ImageFile({ attachment, ...props }: Props) {
|
||||||
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +31,9 @@ export default function ImageFile({ attachment, ...props }: Props) {
|
|||||||
className={classNames(styles.image, {
|
className={classNames(styles.image, {
|
||||||
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
||||||
})}
|
})}
|
||||||
onClick={() => openScreen({ id: "image_viewer", attachment })}
|
onClick={() =>
|
||||||
|
modalController.push({ type: "image_viewer", attachment })
|
||||||
|
}
|
||||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||||
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
||||||
onError={() => setLoading(ImageLoadingState.Error)}
|
onError={() => setLoading(ImageLoadingState.Error)}
|
||||||
|
|||||||
@@ -221,13 +221,15 @@ export const MessageReply = observer(
|
|||||||
</em>
|
</em>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Markdown
|
{message.content && (
|
||||||
disallowBigEmoji
|
<Markdown
|
||||||
content={message.content?.replace(
|
disallowBigEmoji
|
||||||
/\n/g,
|
content={message.content.replace(
|
||||||
" ",
|
/\n/g,
|
||||||
)}
|
" ",
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
239
src/components/common/messaging/attachments/Reactions.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import {
|
||||||
|
autoPlacement,
|
||||||
|
offset,
|
||||||
|
shift,
|
||||||
|
useFloating,
|
||||||
|
} from "@floating-ui/react-dom-interactions";
|
||||||
|
import { Plus } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Message } from "revolt.js";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
import { createPortal } from "preact/compat";
|
||||||
|
import { useCallback, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { emojiDictionary } from "../../../../assets/emojis";
|
||||||
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { RenderEmoji } from "../../../markdown/plugins/emoji";
|
||||||
|
import { HackAlertThisFileWillBeReplaced } from "../MessageBox";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reaction list element
|
||||||
|
*/
|
||||||
|
const List = styled.div`
|
||||||
|
gap: 0.4em;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.add {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .add {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List divider
|
||||||
|
*/
|
||||||
|
const Divider = styled.div`
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--tertiary-foreground);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reaction styling
|
||||||
|
*/
|
||||||
|
const Reaction = styled.div<{ active: boolean }>`
|
||||||
|
padding: 0.4em;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: var(--secondary-background);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
filter: brightness(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.active &&
|
||||||
|
css`
|
||||||
|
border-color: var(--accent);
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render reactions on a message
|
||||||
|
*/
|
||||||
|
export const Reactions = observer(({ message }: Props) => {
|
||||||
|
const client = useClient();
|
||||||
|
const [showPicker, setPicker] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render individual reaction entries
|
||||||
|
*/
|
||||||
|
const Entry = useCallback(
|
||||||
|
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
|
||||||
|
const active = user_ids?.has(client.user!._id) || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reaction
|
||||||
|
active={active}
|
||||||
|
onClick={() =>
|
||||||
|
active ? message.unreact(id) : message.react(id)
|
||||||
|
}>
|
||||||
|
<RenderEmoji match={id} /> {user_ids?.size || 0}
|
||||||
|
</Reaction>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine two lists of 'required' and 'optional' reactions
|
||||||
|
*/
|
||||||
|
const { required, optional } = (() => {
|
||||||
|
const required = new Set<string>();
|
||||||
|
const optional = new Set<string>();
|
||||||
|
|
||||||
|
if (message.interactions?.reactions) {
|
||||||
|
for (const reaction of message.interactions.reactions) {
|
||||||
|
required.add(reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of message.reactions.keys()) {
|
||||||
|
if (!required.has(key)) {
|
||||||
|
optional.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
required,
|
||||||
|
optional,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Don't render list if nothing is going to show anyways
|
||||||
|
if (required.size === 0 && optional.size === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{Array.from(required, (id) => (
|
||||||
|
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
|
||||||
|
))}
|
||||||
|
{required.size !== 0 && optional.size !== 0 && <Divider />}
|
||||||
|
{Array.from(optional, (id) => (
|
||||||
|
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
|
||||||
|
))}
|
||||||
|
{message.channel?.havePermission("React") && (
|
||||||
|
<ReactionWrapper
|
||||||
|
message={message}
|
||||||
|
open={showPicker}
|
||||||
|
setOpen={setPicker}>
|
||||||
|
<IconButton className={showPicker ? "" : "add"}>
|
||||||
|
<Plus size={20} />
|
||||||
|
</IconButton>
|
||||||
|
</ReactionWrapper>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Base = styled.div`
|
||||||
|
> div {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! FIXME: rewrite
|
||||||
|
*/
|
||||||
|
export const ReactionWrapper: React.FC<{
|
||||||
|
message: Message;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (v: boolean) => void;
|
||||||
|
}> = ({ open, setOpen, message, children }) => {
|
||||||
|
const { x, y, reference, floating, strategy } = useFloating({
|
||||||
|
open,
|
||||||
|
middleware: [
|
||||||
|
offset(4),
|
||||||
|
shift({ mainAxis: true, crossAxis: true, padding: 4 }),
|
||||||
|
autoPlacement(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const skip = useRef();
|
||||||
|
const toggle = () => {
|
||||||
|
if (skip.current) {
|
||||||
|
skip.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(!open);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
skip.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={reference}
|
||||||
|
onClick={toggle}
|
||||||
|
style={{ width: "fit-content" }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{createPortal(
|
||||||
|
<div id="reaction">
|
||||||
|
{open && (
|
||||||
|
<Base
|
||||||
|
ref={floating}
|
||||||
|
style={{
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
}}>
|
||||||
|
<HackAlertThisFileWillBeReplaced
|
||||||
|
onSelect={(emoji) =>
|
||||||
|
message.react(
|
||||||
|
emojiDictionary[
|
||||||
|
emoji as keyof typeof emojiDictionary
|
||||||
|
] ?? emoji,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClose={toggle}
|
||||||
|
/>
|
||||||
|
</Base>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,15 +3,12 @@ import { API } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button, Preloader } from "@revoltchat/ui";
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import {
|
import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline";
|
||||||
AppContext,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: API.File;
|
attachment: API.File;
|
||||||
@@ -23,9 +20,8 @@ export default function TextFile({ attachment }: Props) {
|
|||||||
const [gated, setGated] = useState(attachment.size > 100_000);
|
const [gated, setGated] = useState(attachment.size > 100_000);
|
||||||
const [content, setContent] = useState<undefined | string>(undefined);
|
const [content, setContent] = useState<undefined | string>(undefined);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const status = useContext(StatusContext);
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
|
|
||||||
|
const client = useClient();
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -56,7 +52,7 @@ export default function TextFile({ attachment }: Props) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [content, loading, gated, status, attachment._id, attachment.size, url]);
|
}, [content, loading, gated, attachment._id, attachment.size, url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
Share,
|
Share,
|
||||||
InfoSquare,
|
InfoSquare,
|
||||||
Notification,
|
Notification,
|
||||||
|
HappyBeaming,
|
||||||
} from "@styled-icons/boxicons-solid";
|
} from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Permission } from "revolt.js";
|
|
||||||
import { Message as MessageObject } from "revolt.js";
|
import { Message as MessageObject } from "revolt.js";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
@@ -18,17 +18,16 @@ import { internalEmit } from "../../../../lib/eventEmitter";
|
|||||||
import { shiftKeyPressed } from "../../../../lib/modifiers";
|
import { shiftKeyPressed } from "../../../../lib/modifiers";
|
||||||
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
|
import { state } from "../../../../mobx/State";
|
||||||
import { QueuedMessage } from "../../../../mobx/stores/MessageQueue";
|
import { QueuedMessage } from "../../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import {
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
Screen,
|
|
||||||
useIntermediate,
|
|
||||||
} from "../../../../context/intermediate/Intermediate";
|
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import Tooltip from "../../../common/Tooltip";
|
import Tooltip from "../../../common/Tooltip";
|
||||||
|
import { ReactionWrapper } from "../attachments/Reactions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
reactionsOpen: boolean;
|
||||||
|
setReactionsOpen: (v: boolean) => void;
|
||||||
message: MessageObject;
|
message: MessageObject;
|
||||||
queued?: QueuedMessage;
|
queued?: QueuedMessage;
|
||||||
}
|
}
|
||||||
@@ -87,127 +86,152 @@ const Divider = styled.div`
|
|||||||
background: var(--tertiary-background);
|
background: var(--tertiary-background);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageOverlayBar = observer(({ message, queued }: Props) => {
|
export const MessageOverlayBar = observer(
|
||||||
const client = useClient();
|
({ reactionsOpen, setReactionsOpen, message, queued }: Props) => {
|
||||||
const { openScreen, writeClipboard } = useIntermediate();
|
const client = message.client;
|
||||||
const isAuthor = message.author_id === client.user!._id;
|
const isAuthor = message.author_id === client.user!._id;
|
||||||
|
|
||||||
const [copied, setCopied] = useState<"link" | "id">(null!);
|
const [copied, setCopied] = useState<"link" | "id">(null!);
|
||||||
const [extraActions, setExtra] = useState(shiftKeyPressed);
|
const [extraActions, setExtra] = useState(shiftKeyPressed);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey);
|
const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey);
|
||||||
|
|
||||||
document.addEventListener("keyup", handler);
|
document.addEventListener("keyup", handler);
|
||||||
document.addEventListener("keydown", handler);
|
document.addEventListener("keydown", handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keyup", handler);
|
document.removeEventListener("keyup", handler);
|
||||||
document.removeEventListener("keydown", handler);
|
document.removeEventListener("keydown", handler);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayBar>
|
<OverlayBar>
|
||||||
<Tooltip content="Reply">
|
{message.channel?.havePermission("SendMessage") && (
|
||||||
<Entry onClick={() => internalEmit("ReplyBar", "add", message)}>
|
<Tooltip content="Reply">
|
||||||
<Share size={18} />
|
<Entry
|
||||||
</Entry>
|
onClick={() =>
|
||||||
</Tooltip>
|
internalEmit("ReplyBar", "add", message)
|
||||||
|
}>
|
||||||
|
<Share size={18} />
|
||||||
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAuthor && (
|
{message.channel?.havePermission("React") &&
|
||||||
<Tooltip content="Edit">
|
state.experiments.isEnabled("picker") && (
|
||||||
|
<ReactionWrapper
|
||||||
|
open={reactionsOpen}
|
||||||
|
setOpen={setReactionsOpen}
|
||||||
|
message={message}>
|
||||||
|
<Tooltip content="React">
|
||||||
|
<Entry>
|
||||||
|
<HappyBeaming size={18} />
|
||||||
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
</ReactionWrapper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAuthor && (
|
||||||
|
<Tooltip content="Edit">
|
||||||
|
<Entry
|
||||||
|
onClick={() =>
|
||||||
|
internalEmit(
|
||||||
|
"MessageRenderer",
|
||||||
|
"edit_message",
|
||||||
|
message._id,
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<Pencil size={18} />
|
||||||
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isAuthor ||
|
||||||
|
(message.channel &&
|
||||||
|
message.channel.havePermission("ManageMessages")) ? (
|
||||||
|
<Tooltip content="Delete">
|
||||||
|
<Entry
|
||||||
|
onClick={(e) =>
|
||||||
|
e.shiftKey
|
||||||
|
? message.delete()
|
||||||
|
: modalController.push({
|
||||||
|
type: "delete_message",
|
||||||
|
target: message,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Trash size={18} color={"var(--error)"} />
|
||||||
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined}
|
||||||
|
<Tooltip content="More">
|
||||||
<Entry
|
<Entry
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
internalEmit(
|
openContextMenu("Menu", {
|
||||||
"MessageRenderer",
|
message,
|
||||||
"edit_message",
|
contextualChannel: message.channel_id,
|
||||||
message._id,
|
queued,
|
||||||
)
|
})
|
||||||
}>
|
}>
|
||||||
<Pencil size={18} />
|
<DotsVerticalRounded size={18} />
|
||||||
</Entry>
|
</Entry>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
{extraActions && (
|
||||||
{isAuthor ||
|
<>
|
||||||
(message.channel &&
|
<Divider />
|
||||||
message.channel.havePermission("ManageMessages")) ? (
|
<Tooltip content="Mark as Unread">
|
||||||
<Tooltip content="Delete">
|
<Entry
|
||||||
<Entry
|
onClick={() => {
|
||||||
onClick={(e) =>
|
// ! FIXME: deduplicate this code with ctx menu
|
||||||
e.shiftKey
|
const messages = getRenderer(
|
||||||
? message.delete()
|
message.channel!,
|
||||||
: openScreen({
|
).messages;
|
||||||
id: "special_prompt",
|
const index = messages.findIndex(
|
||||||
type: "delete_message",
|
(x) => x._id === message._id,
|
||||||
target: message,
|
);
|
||||||
} as unknown as Screen)
|
|
||||||
}>
|
|
||||||
<Trash size={18} color={"var(--error)"} />
|
|
||||||
</Entry>
|
|
||||||
</Tooltip>
|
|
||||||
) : undefined}
|
|
||||||
<Tooltip content="More">
|
|
||||||
<Entry
|
|
||||||
onClick={() =>
|
|
||||||
openContextMenu("Menu", {
|
|
||||||
message,
|
|
||||||
contextualChannel: message.channel_id,
|
|
||||||
queued,
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<DotsVerticalRounded size={18} />
|
|
||||||
</Entry>
|
|
||||||
</Tooltip>
|
|
||||||
{extraActions && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
<Tooltip content="Mark as Unread">
|
|
||||||
<Entry
|
|
||||||
onClick={() => {
|
|
||||||
// ! FIXME: deduplicate this code with ctx menu
|
|
||||||
const messages = getRenderer(
|
|
||||||
message.channel!,
|
|
||||||
).messages;
|
|
||||||
const index = messages.findIndex(
|
|
||||||
(x) => x._id === message._id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let unread_id = message._id;
|
let unread_id = message._id;
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
unread_id = messages[index - 1]._id;
|
unread_id = messages[index - 1]._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
internalEmit("NewMessages", "mark", unread_id);
|
internalEmit(
|
||||||
message.channel?.ack(unread_id, true);
|
"NewMessages",
|
||||||
}}>
|
"mark",
|
||||||
<Notification size={18} />
|
unread_id,
|
||||||
</Entry>
|
);
|
||||||
</Tooltip>
|
message.channel?.ack(unread_id, true);
|
||||||
<Tooltip
|
}}>
|
||||||
content={copied === "link" ? "Copied!" : "Copy Link"}
|
<Notification size={18} />
|
||||||
hideOnClick={false}>
|
</Entry>
|
||||||
<Entry
|
</Tooltip>
|
||||||
onClick={() => {
|
<Tooltip
|
||||||
setCopied("link");
|
content={
|
||||||
writeClipboard(message.url);
|
copied === "link" ? "Copied!" : "Copy Link"
|
||||||
}}>
|
}
|
||||||
<LinkAlt size={18} />
|
hideOnClick={false}>
|
||||||
</Entry>
|
<Entry
|
||||||
</Tooltip>
|
onClick={() => {
|
||||||
<Tooltip
|
setCopied("link");
|
||||||
content={copied === "id" ? "Copied!" : "Copy ID"}
|
modalController.writeText(message.url);
|
||||||
hideOnClick={false}>
|
}}>
|
||||||
<Entry
|
<LinkAlt size={18} />
|
||||||
onClick={() => {
|
</Entry>
|
||||||
setCopied("id");
|
</Tooltip>
|
||||||
writeClipboard(message._id);
|
<Tooltip
|
||||||
}}>
|
content={copied === "id" ? "Copied!" : "Copy ID"}
|
||||||
<InfoSquare size={18} />
|
hideOnClick={false}>
|
||||||
</Entry>
|
<Entry
|
||||||
</Tooltip>
|
onClick={() => {
|
||||||
</>
|
setCopied("id");
|
||||||
)}
|
modalController.writeText(message._id);
|
||||||
</OverlayBar>
|
}}>
|
||||||
);
|
<InfoSquare size={18} />
|
||||||
});
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</OverlayBar>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -185,13 +185,15 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
hideInfo
|
hideInfo
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Markdown
|
message.content && (
|
||||||
disallowBigEmoji
|
<Markdown
|
||||||
content={message.content?.replace(
|
disallowBigEmoji
|
||||||
/\n/g,
|
content={message.content.replace(
|
||||||
" ",
|
/\n/g,
|
||||||
)}
|
" ",
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
&.website {
|
&.website {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
> div:nth-child(1) {
|
> div:nth-child(1) {
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import styles from "./Embed.module.scss";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import Attachment from "../attachments/Attachment";
|
import Attachment from "../attachments/Attachment";
|
||||||
@@ -24,7 +23,6 @@ const MAX_PREVIEW_SIZE = 150;
|
|||||||
export default function Embed({ embed }: Props) {
|
export default function Embed({ embed }: Props) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const { openScreen, openLink } = useIntermediate();
|
|
||||||
const maxWidth = Math.min(
|
const maxWidth = Math.min(
|
||||||
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
||||||
MAX_EMBED_WIDTH,
|
MAX_EMBED_WIDTH,
|
||||||
@@ -143,7 +141,7 @@ export default function Embed({ embed }: Props) {
|
|||||||
<a
|
<a
|
||||||
onMouseDown={(ev) =>
|
onMouseDown={(ev) =>
|
||||||
(ev.button === 0 || ev.button === 1) &&
|
(ev.button === 0 || ev.button === 1) &&
|
||||||
openLink(embed.url!)
|
modalController.openLink(embed.url!)
|
||||||
}
|
}
|
||||||
className={styles.title}>
|
className={styles.title}>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
@@ -191,8 +189,12 @@ export default function Embed({ embed }: Props) {
|
|||||||
type="text/html"
|
type="text/html"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onClick={() => openScreen({ id: "image_viewer", embed })}
|
onClick={() =>
|
||||||
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
modalController.push({ type: "image_viewer", embed })
|
||||||
|
}
|
||||||
|
onMouseDown={(ev) =>
|
||||||
|
ev.button === 1 && modalController.openLink(embed.url)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Group } from "@styled-icons/boxicons-solid";
|
import { Group } from "@styled-icons/boxicons-solid";
|
||||||
import { reaction } from "mobx";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Message, API } from "revolt.js";
|
import { Message, API } from "revolt.js";
|
||||||
@@ -12,14 +11,13 @@ import { Button, Category, Preloader } from "@revoltchat/ui";
|
|||||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { I18nError } from "../../../../context/Locale";
|
import { I18nError } from "../../../../context/Locale";
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../../../context/revoltjs/util";
|
|
||||||
|
|
||||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||||
|
import {
|
||||||
|
useClient,
|
||||||
|
useSession,
|
||||||
|
} from "../../../../controllers/client/ClientController";
|
||||||
|
import { takeError } from "../../../../controllers/client/jsx/error";
|
||||||
|
|
||||||
const EmbedInviteBase = styled.div`
|
const EmbedInviteBase = styled.div`
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@@ -78,8 +76,8 @@ type Props = {
|
|||||||
|
|
||||||
export function EmbedInvite({ code }: Props) {
|
export function EmbedInvite({ code }: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useContext(AppContext);
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
||||||
@@ -90,7 +88,7 @@ export function EmbedInvite({ code }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
typeof invite === "undefined" &&
|
typeof invite === "undefined" &&
|
||||||
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
|
(session.state === "Online" || session.state === "Ready")
|
||||||
) {
|
) {
|
||||||
client
|
client
|
||||||
.fetchInvite(code)
|
.fetchInvite(code)
|
||||||
@@ -99,7 +97,7 @@ export function EmbedInvite({ code }: Props) {
|
|||||||
)
|
)
|
||||||
.catch((err) => setError(takeError(err)));
|
.catch((err) => setError(takeError(err)));
|
||||||
}
|
}
|
||||||
}, [client, code, invite, status]);
|
}, [client, code, invite, session.state]);
|
||||||
|
|
||||||
if (typeof invite === "undefined") {
|
if (typeof invite === "undefined") {
|
||||||
return error ? (
|
return error ? (
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { API } from "revolt.js";
|
|||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: API.Embed;
|
embed: API.Embed;
|
||||||
@@ -14,7 +14,6 @@ interface Props {
|
|||||||
|
|
||||||
export default function EmbedMedia({ embed, width, height }: Props) {
|
export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
if (embed.type !== "Website") return null;
|
if (embed.type !== "Website") return null;
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
switch (embed.special?.type) {
|
switch (embed.special?.type) {
|
||||||
@@ -50,7 +49,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
|||||||
case "Lightspeed":
|
case "Lightspeed":
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://next.lightspeed.tv/embed/${embed.special.id}`}
|
src={`https://new.lightspeed.tv/embed/${embed.special.id}/stream`}
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
@@ -115,10 +114,10 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
|||||||
className={styles.image}
|
className={styles.image}
|
||||||
src={client.proxyFile(url)}
|
src={client.proxyFile(url)}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width, height }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "image_viewer",
|
type: "image_viewer",
|
||||||
embed: embed.image!,
|
embed: embed.image!,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ enum Badges {
|
|||||||
Paw = 128,
|
Paw = 128,
|
||||||
EarlyAdopter = 256,
|
EarlyAdopter = 256,
|
||||||
ReservedRelevantJokeBadge1 = 512,
|
ReservedRelevantJokeBadge1 = 512,
|
||||||
|
ReservedRelevantJokeBadge2 = 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BadgesBase = styled.div`
|
const BadgesBase = styled.div`
|
||||||
@@ -135,6 +136,13 @@ export default function UserBadges({ badges, uid }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
{badges & Badges.ReservedRelevantJokeBadge2 ? (
|
||||||
|
<Tooltip content="It's Morbin Time">
|
||||||
|
<img src="/assets/badges/amorbus.svg" />
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
{badges & Badges.Paw ? (
|
{badges & Badges.Paw ? (
|
||||||
<Tooltip content="🦊">
|
<Tooltip content="🦊">
|
||||||
<img src="/assets/badges/paw.svg" />
|
<img src="/assets/badges/paw.svg" />
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { Header, IconButton } from "@revoltchat/ui";
|
|||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import UserStatus from "./UserStatus";
|
import UserStatus from "./UserStatus";
|
||||||
|
|
||||||
@@ -48,8 +47,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default observer(({ user }: Props) => {
|
export default observer(({ user }: Props) => {
|
||||||
const { writeClipboard } = useIntermediate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header topBorder palette="secondary">
|
<Header topBorder palette="secondary">
|
||||||
<HeaderBase>
|
<HeaderBase>
|
||||||
@@ -57,7 +54,9 @@ export default observer(({ user }: Props) => {
|
|||||||
<Tooltip content={<Text id="app.special.copy_username" />}>
|
<Tooltip content={<Text id="app.special.copy_username" />}>
|
||||||
<span
|
<span
|
||||||
className="username"
|
className="username"
|
||||||
onClick={() => writeClipboard(user.username)}>
|
onClick={() =>
|
||||||
|
modalController.writeText(user.username)
|
||||||
|
}>
|
||||||
@{user.username}
|
@{user.username}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import styled, { css } from "styled-components/macro";
|
|||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import fallback from "../assets/user.png";
|
import fallback from "../assets/user.png";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import IconBase, { IconBaseProps } from "../IconBase";
|
import IconBase, { IconBaseProps } from "../IconBase";
|
||||||
|
|
||||||
type VoiceStatus = "muted" | "deaf";
|
type VoiceStatus = "muted" | "deaf";
|
||||||
@@ -26,6 +25,8 @@ export function useStatusColour(user?: User) {
|
|||||||
return user?.online && user?.status?.presence !== "Invisible"
|
return user?.online && user?.status?.presence !== "Invisible"
|
||||||
? user?.status?.presence === "Idle"
|
? user?.status?.presence === "Idle"
|
||||||
? theme.getVariable("status-away")
|
? theme.getVariable("status-away")
|
||||||
|
: user?.status?.presence === "Focus"
|
||||||
|
? theme.getVariable("status-focus")
|
||||||
: user?.status?.presence === "Busy"
|
: user?.status?.presence === "Busy"
|
||||||
? theme.getVariable("status-busy")
|
? theme.getVariable("status-busy")
|
||||||
: theme.getVariable("status-online")
|
: theme.getVariable("status-online")
|
||||||
@@ -56,7 +57,7 @@ export default observer(
|
|||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useApplicationState().client!;
|
const client = useClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
target,
|
target,
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import { TimeFive } from "@styled-icons/boxicons-regular";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { User, API } from "revolt.js";
|
import { User, API } from "revolt.js";
|
||||||
import styled from "styled-components/macro";
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
import { Ref } from "preact";
|
import { Ref } from "preact";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { dayjs } from "../../../context/Locale";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
import Tooltip from "../Tooltip";
|
||||||
import UserIcon from "./UserIcon";
|
import UserIcon from "./UserIcon";
|
||||||
|
|
||||||
const BotBadge = styled.div`
|
const BotBadge = styled.div`
|
||||||
@@ -27,7 +30,10 @@ const BotBadge = styled.div`
|
|||||||
border-radius: calc(var(--border-radius) / 2);
|
border-radius: calc(var(--border-radius) / 2);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
|
type UsernameProps = Omit<
|
||||||
|
JSX.HTMLAttributes<HTMLElement>,
|
||||||
|
"children" | "as"
|
||||||
|
> & {
|
||||||
user?: User;
|
user?: User;
|
||||||
prefixAt?: boolean;
|
prefixAt?: boolean;
|
||||||
masquerade?: API.Masquerade;
|
masquerade?: API.Masquerade;
|
||||||
@@ -36,6 +42,21 @@ type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
|
|||||||
innerRef?: Ref<any>;
|
innerRef?: Ref<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Name = styled.span<{ colour?: string | null }>`
|
||||||
|
${(props) =>
|
||||||
|
props.colour &&
|
||||||
|
(props.colour.includes("gradient")
|
||||||
|
? css`
|
||||||
|
background: ${props.colour};
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
color: ${props.colour};
|
||||||
|
`)}
|
||||||
|
`;
|
||||||
|
|
||||||
export const Username = observer(
|
export const Username = observer(
|
||||||
({
|
({
|
||||||
user,
|
user,
|
||||||
@@ -46,7 +67,8 @@ export const Username = observer(
|
|||||||
...otherProps
|
...otherProps
|
||||||
}: UsernameProps) => {
|
}: UsernameProps) => {
|
||||||
let username = user?.username;
|
let username = user?.username;
|
||||||
let color;
|
let color = masquerade?.colour;
|
||||||
|
let timed_out: Date | undefined;
|
||||||
|
|
||||||
if (user && showServerIdentity) {
|
if (user && showServerIdentity) {
|
||||||
const { server } = useParams<{ server?: string }>();
|
const { server } = useParams<{ server?: string }>();
|
||||||
@@ -66,15 +88,14 @@ export const Username = observer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.roles && member.roles.length > 0) {
|
if (member.timeout) {
|
||||||
const srv = client.servers.get(member._id.server);
|
timed_out = member.timeout;
|
||||||
if (srv?.roles) {
|
}
|
||||||
for (const role of member.roles) {
|
|
||||||
const c = srv.roles[role]?.colour;
|
if (!color) {
|
||||||
if (c) {
|
for (const [_, { colour }] of member.orderedRoles) {
|
||||||
color = c;
|
if (colour) {
|
||||||
continue;
|
color = colour;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,14 +103,38 @@ export const Username = observer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const el = (
|
||||||
|
<>
|
||||||
|
<Name {...otherProps} ref={innerRef} colour={color}>
|
||||||
|
{prefixAt ? "@" : undefined}
|
||||||
|
{masquerade?.name ?? username ?? (
|
||||||
|
<Text id="app.main.channel.unknown_user" />
|
||||||
|
)}
|
||||||
|
</Name>
|
||||||
|
|
||||||
|
{timed_out && (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Text
|
||||||
|
id="app.main.channel.user_timed_out"
|
||||||
|
fields={{
|
||||||
|
time: dayjs(timed_out).fromNow(true),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<TimeFive
|
||||||
|
size={16}
|
||||||
|
color="var(--secondary-foreground)"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
if (user?.bot) {
|
if (user?.bot) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span {...otherProps} ref={innerRef} style={{ color }}>
|
{el}
|
||||||
{masquerade?.name ?? username ?? (
|
|
||||||
<Text id="app.main.channel.unknown_user" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<BotBadge>
|
<BotBadge>
|
||||||
{masquerade ? (
|
{masquerade ? (
|
||||||
<Text id="app.main.channel.bridge" />
|
<Text id="app.main.channel.bridge" />
|
||||||
@@ -101,14 +146,7 @@ export const Username = observer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return el;
|
||||||
<span {...otherProps} ref={innerRef} style={{ color }}>
|
|
||||||
{prefixAt ? "@" : undefined}
|
|
||||||
{masquerade?.name ?? username ?? (
|
|
||||||
<Text id="app.main.channel.unknown_user" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,9 +163,9 @@ export default function UserShort({
|
|||||||
masquerade?: API.Masquerade;
|
masquerade?: API.Masquerade;
|
||||||
showServerIdentity?: boolean;
|
showServerIdentity?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const openProfile = () =>
|
const openProfile = () =>
|
||||||
user && openScreen({ id: "profile", user_id: user._id });
|
user &&
|
||||||
|
modalController.push({ type: "user_profile", user_id: user._id });
|
||||||
|
|
||||||
const handleUserClick = (e: MouseEvent) => {
|
const handleUserClick = (e: MouseEvent) => {
|
||||||
if (e.shiftKey && user?._id) {
|
if (e.shiftKey && user?._id) {
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export default observer(({ user, tooltip }: Props) => {
|
|||||||
return <Text id="app.status.idle" />;
|
return <Text id="app.status.idle" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.status?.presence === "Focus") {
|
||||||
|
return <Text id="app.status.focus" />;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.status?.presence === "Invisible") {
|
if (user.status?.presence === "Invisible") {
|
||||||
return <Text id="app.status.offline" />;
|
return <Text id="app.status.offline" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
.markdown {
|
|
||||||
:global(.emoji) {
|
|
||||||
object-fit: contain;
|
|
||||||
|
|
||||||
height: 1.25em;
|
|
||||||
width: 1.25em;
|
|
||||||
margin: 0 0.05em 0 0.1em;
|
|
||||||
vertical-align: -0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-large-emojis="true"] :global(.emoji) {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-top: 1px;
|
|
||||||
margin-right: 2px;
|
|
||||||
vertical-align: -0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&[data-type="mention"] {
|
|
||||||
padding: 0 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--secondary-background);
|
|
||||||
border-radius: calc(var(--border-radius) * 2);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
ul,
|
|
||||||
ol,
|
|
||||||
blockquote {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
margin: 2px 0;
|
|
||||||
padding: 2px 0;
|
|
||||||
background: var(--hover);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border-inline-start: 4px solid var(--tertiary-background);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 1em;
|
|
||||||
overflow-x: scroll;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: var(--block) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
p > code {
|
|
||||||
padding: 1px 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
color: white;
|
|
||||||
font-size: 90%;
|
|
||||||
background: var(--block);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-family: var(--monospace-font), monospace;
|
|
||||||
border-radius: 3px;
|
|
||||||
-webkit-box-decoration-break: clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
margin-right: 4px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: 6px;
|
|
||||||
border: 1px solid var(--tertiary-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.katex-block) {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.spoiler) {
|
|
||||||
padding: 0 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
color: transparent;
|
|
||||||
background: #151515;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:global(.shown) {
|
|
||||||
cursor: auto;
|
|
||||||
user-select: all;
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--secondary-background);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.code) {
|
|
||||||
font-family: var(--monospace-font), monospace;
|
|
||||||
|
|
||||||
:global(.lang) {
|
|
||||||
width: fit-content;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
|
|
||||||
div {
|
|
||||||
color: #111;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--accent);
|
|
||||||
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
box-shadow: 0 2px #787676;
|
|
||||||
border-radius: calc(var(--border-radius) / 3);
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
box-shadow: 0 1px #787676;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
width: 0;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] + label:before {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
content: "a";
|
|
||||||
font-size: 10px;
|
|
||||||
margin-right: 6px;
|
|
||||||
line-height: 12px;
|
|
||||||
background: white;
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"][checked="true"] + label:before {
|
|
||||||
content: "✓";
|
|
||||||
align-items: center;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] + label {
|
|
||||||
line-height: 12px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Suspense, lazy } from "preact/compat";
|
import { Suspense, lazy } from "preact/compat";
|
||||||
|
|
||||||
const Renderer = lazy(() => import("./Renderer"));
|
const Renderer = lazy(() => import("./RemarkRenderer"));
|
||||||
|
|
||||||
export interface MarkdownProps {
|
export interface MarkdownProps {
|
||||||
content?: string | null;
|
content: string;
|
||||||
disallowBigEmoji?: boolean;
|
disallowBigEmoji?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Markdown(props: MarkdownProps) {
|
export default function Markdown(props: MarkdownProps) {
|
||||||
|
if (!props.content) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error Typings mis-match.
|
// @ts-expect-error Typings mis-match.
|
||||||
<Suspense fallback={props.content}>
|
<Suspense fallback={props.content}>
|
||||||
|
|||||||
258
src/components/markdown/RemarkRenderer.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import rehypePrism from "rehype-prism";
|
||||||
|
import rehypeReact from "rehype-react";
|
||||||
|
import remarkBreaks from "remark-breaks";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkRehype from "remark-rehype";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
import { unified } from "unified";
|
||||||
|
|
||||||
|
import { createElement } from "preact";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
|
import { useLayoutEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { MarkdownProps } from "./Markdown";
|
||||||
|
import { handlers } from "./hast";
|
||||||
|
import { RenderCodeblock } from "./plugins/Codeblock";
|
||||||
|
import { RenderAnchor } from "./plugins/anchors";
|
||||||
|
import { remarkChannels, RenderChannel } from "./plugins/channels";
|
||||||
|
import { isOnlyEmoji, remarkEmoji, RenderEmoji } from "./plugins/emoji";
|
||||||
|
import { remarkHtmlToText } from "./plugins/htmlToText";
|
||||||
|
import { remarkMention, RenderMention } from "./plugins/mentions";
|
||||||
|
import { remarkSpoiler, RenderSpoiler } from "./plugins/spoiler";
|
||||||
|
import { remarkTimestamps } from "./plugins/timestamps";
|
||||||
|
import "./prism";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null element
|
||||||
|
*/
|
||||||
|
const Null: React.FC = () => null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Markdown components
|
||||||
|
*/
|
||||||
|
const components = {
|
||||||
|
emoji: RenderEmoji,
|
||||||
|
mention: RenderMention,
|
||||||
|
spoiler: RenderSpoiler,
|
||||||
|
channel: RenderChannel,
|
||||||
|
a: RenderAnchor,
|
||||||
|
p: styled.p`
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> code {
|
||||||
|
padding: 1px 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
h1: styled.h1`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h2: styled.h2`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h3: styled.h3`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h4: styled.h4`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h5: styled.h5`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h6: styled.h6`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
pre: RenderCodeblock,
|
||||||
|
code: styled.code`
|
||||||
|
color: white;
|
||||||
|
background: var(--block);
|
||||||
|
|
||||||
|
font-size: 90%;
|
||||||
|
font-family: var(--monospace-font), monospace;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
`,
|
||||||
|
table: styled.table`
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--tertiary-foreground);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
ul: styled.ul`
|
||||||
|
list-style-position: inside;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
ol: styled.ol`
|
||||||
|
list-style-position: inside;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
li: styled.li`
|
||||||
|
${(props) =>
|
||||||
|
props.class === "task-list-item" &&
|
||||||
|
css`
|
||||||
|
list-style-type: none;
|
||||||
|
`}
|
||||||
|
`,
|
||||||
|
blockquote: styled.blockquote`
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 2px 0;
|
||||||
|
background: var(--hover);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border-inline-start: 4px solid var(--tertiary-background);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
// Block image elements
|
||||||
|
img: Null,
|
||||||
|
// Catch literally everything else just in case
|
||||||
|
video: Null,
|
||||||
|
figure: Null,
|
||||||
|
picture: Null,
|
||||||
|
source: Null,
|
||||||
|
audio: Null,
|
||||||
|
script: Null,
|
||||||
|
style: Null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified Markdown renderer
|
||||||
|
*/
|
||||||
|
const render = unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkBreaks)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(remarkMath)
|
||||||
|
.use(remarkSpoiler)
|
||||||
|
.use(remarkChannels)
|
||||||
|
.use(remarkTimestamps)
|
||||||
|
.use(remarkEmoji)
|
||||||
|
.use(remarkMention)
|
||||||
|
.use(remarkHtmlToText)
|
||||||
|
.use(remarkRehype, {
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
.use(rehypeKatex, {
|
||||||
|
maxSize: 10,
|
||||||
|
maxExpand: 0,
|
||||||
|
trust: false,
|
||||||
|
strict: false,
|
||||||
|
output: "html",
|
||||||
|
throwOnError: false,
|
||||||
|
errorColor: "var(--error)",
|
||||||
|
})
|
||||||
|
.use(rehypePrism)
|
||||||
|
// @ts-expect-error typings do not
|
||||||
|
// match between Preact and React
|
||||||
|
.use(rehypeReact, {
|
||||||
|
createElement,
|
||||||
|
Fragment,
|
||||||
|
components,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown parent container
|
||||||
|
*/
|
||||||
|
const Container = styled.div<{ largeEmoji: boolean }>`
|
||||||
|
// Allow scrolling block math
|
||||||
|
.math-display {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set emoji size
|
||||||
|
--emoji-size: ${(props) => (props.largeEmoji ? "3em" : "1.25em")};
|
||||||
|
|
||||||
|
// Underline link hover
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching execessive recursion of blockquotes and lists
|
||||||
|
*/
|
||||||
|
const RE_RECURSIVE = /(^(?:[>*+-][^\S\r\n]*){5})(?:[>*+-][^\S\r\n]*)+(.*$)/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching multi-line blockquotes
|
||||||
|
*/
|
||||||
|
const RE_BLOCKQUOTE = /^([^\S\r\n]*>[^\n]+\n?)+/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching HTML tags
|
||||||
|
*/
|
||||||
|
const RE_HTML_TAGS = /^(<\/?[a-zA-Z0-9]+>)(.*$)/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching empty lines
|
||||||
|
*/
|
||||||
|
const RE_EMPTY_LINE = /^\s*?$/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching line starting with plus
|
||||||
|
*/
|
||||||
|
const RE_PLUS = /^\s*\+(?:$|[^+])/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise Markdown input before rendering
|
||||||
|
* @param content Input string
|
||||||
|
* @returns Sanitised string
|
||||||
|
*/
|
||||||
|
function sanitise(content: string) {
|
||||||
|
return (
|
||||||
|
content
|
||||||
|
// Strip excessive blockquote or list indentation
|
||||||
|
.replace(RE_RECURSIVE, (_, m0, m1) => m0 + m1)
|
||||||
|
|
||||||
|
// Append empty character if string starts with html tag
|
||||||
|
// This is to avoid inconsistencies in rendering Markdown inside/after HTML tags
|
||||||
|
// https://github.com/revoltchat/revite/issues/733
|
||||||
|
.replace(RE_HTML_TAGS, (match) => `\u200E${match}`)
|
||||||
|
|
||||||
|
// Append empty character if line starts with a plus
|
||||||
|
// which would usually open a new list but we want
|
||||||
|
// to avoid that behaviour in our case.
|
||||||
|
.replace(RE_PLUS, (match) => `\u200E${match}`)
|
||||||
|
|
||||||
|
// Replace empty lines with non-breaking space
|
||||||
|
// because remark renderer is collapsing empty
|
||||||
|
// or otherwise whitespace-only lines of text
|
||||||
|
.replace(RE_EMPTY_LINE, "")
|
||||||
|
|
||||||
|
// Ensure empty line after blockquotes for correct rendering
|
||||||
|
.replace(RE_BLOCKQUOTE, (match) => `${match}\n`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remark renderer component
|
||||||
|
*/
|
||||||
|
export default memo(({ content, disallowBigEmoji }: MarkdownProps) => {
|
||||||
|
const sanitisedContent = useMemo(() => sanitise(content), [content]);
|
||||||
|
|
||||||
|
const [Content, setContent] = useState<React.ReactElement>(null!);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
render
|
||||||
|
.process(sanitisedContent)
|
||||||
|
.then((file) => setContent(file.result));
|
||||||
|
}, [sanitisedContent]);
|
||||||
|
|
||||||
|
const largeEmoji = useMemo(
|
||||||
|
() => !disallowBigEmoji && isOnlyEmoji(content!),
|
||||||
|
[content, disallowBigEmoji],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Container largeEmoji={largeEmoji}>{Content}</Container>;
|
||||||
|
});
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import MarkdownKatex from "@traptitech/markdown-it-katex";
|
|
||||||
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
|
|
||||||
import "katex/dist/katex.min.css";
|
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
// @ts-expect-error No typings.
|
|
||||||
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
|
|
||||||
import { RE_MENTIONS } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./Markdown.module.scss";
|
|
||||||
import { useCallback, useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { internalEmit } from "../../lib/eventEmitter";
|
|
||||||
import { determineLink } from "../../lib/links";
|
|
||||||
|
|
||||||
import { dayjs } from "../../context/Locale";
|
|
||||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
|
||||||
import { generateEmoji } from "../common/Emoji";
|
|
||||||
import { MarkdownProps } from "./Markdown";
|
|
||||||
import Prism from "./prism";
|
|
||||||
|
|
||||||
// TODO: global.d.ts file for defining globals
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
copycode: (element: HTMLDivElement) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for code block copy.
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.copycode = function (element: HTMLDivElement) {
|
|
||||||
try {
|
|
||||||
const code = element.parentElement?.parentElement?.children[1];
|
|
||||||
if (code) {
|
|
||||||
navigator.clipboard.writeText(code.textContent?.trim() ?? "");
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const md: MarkdownIt = MarkdownIt({
|
|
||||||
breaks: true,
|
|
||||||
linkify: true,
|
|
||||||
highlight: (str, lang) => {
|
|
||||||
const v = Prism.languages[lang];
|
|
||||||
if (v) {
|
|
||||||
const out = Prism.highlight(str, v, lang);
|
|
||||||
return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<pre class="code"><code>${md.utils.escapeHtml(
|
|
||||||
str,
|
|
||||||
)}</code></pre>`;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.disable("image")
|
|
||||||
.use(MarkdownEmoji, { defs: emojiDictionary })
|
|
||||||
.use(MarkdownSpoilers)
|
|
||||||
.use(MarkdownKatex, {
|
|
||||||
throwOnError: false,
|
|
||||||
maxExpand: 0,
|
|
||||||
maxSize: 10,
|
|
||||||
strict: false,
|
|
||||||
errorColor: "var(--error)",
|
|
||||||
});
|
|
||||||
|
|
||||||
md.linkify.set({ fuzzyLink: false });
|
|
||||||
|
|
||||||
// TODO: global.d.ts file for defining globals
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
internalHandleURL: (element: HTMLAnchorElement) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include emojis.
|
|
||||||
md.renderer.rules.emoji = function (token, idx) {
|
|
||||||
return generateEmoji(token[idx].content);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Force line breaks.
|
|
||||||
// https://github.com/markdown-it/markdown-it/issues/211#issuecomment-508380611
|
|
||||||
const defaultParagraphRenderer =
|
|
||||||
md.renderer.rules.paragraph_open ||
|
|
||||||
((tokens, idx, options, env, self) =>
|
|
||||||
self.renderToken(tokens, idx, options));
|
|
||||||
|
|
||||||
md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) {
|
|
||||||
let result = "";
|
|
||||||
if (idx > 1) {
|
|
||||||
const inline = tokens[idx - 2];
|
|
||||||
const paragraph = tokens[idx];
|
|
||||||
if (
|
|
||||||
inline.type === "inline" &&
|
|
||||||
inline.map &&
|
|
||||||
inline.map[1] &&
|
|
||||||
paragraph.map &&
|
|
||||||
paragraph.map[0]
|
|
||||||
) {
|
|
||||||
const diff = paragraph.map[0] - inline.map[1];
|
|
||||||
if (diff > 0) {
|
|
||||||
result = "<br>".repeat(diff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result + defaultParagraphRenderer(tokens, idx, options, env, self);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RE_TWEMOJI = /:(\w+):/g;
|
|
||||||
|
|
||||||
// ! FIXME: Move to library
|
|
||||||
const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
|
|
||||||
|
|
||||||
const RE_TIME = /<t:([0-9]+):(\w)>/g;
|
|
||||||
|
|
||||||
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const { openLink } = useIntermediate();
|
|
||||||
|
|
||||||
if (typeof content === "undefined") return null;
|
|
||||||
if (!content || content.length === 0) return null;
|
|
||||||
|
|
||||||
// We replace the message with the mention at the time of render.
|
|
||||||
// We don't care if the mention changes.
|
|
||||||
const newContent = content
|
|
||||||
.replace(RE_TIME, (sub: string, ...args: unknown[]) => {
|
|
||||||
if (isNaN(args[0] as number)) return sub;
|
|
||||||
const date = dayjs.unix(args[0] as number);
|
|
||||||
const format = args[1] as string;
|
|
||||||
let final = "";
|
|
||||||
switch (format) {
|
|
||||||
case "t":
|
|
||||||
final = date.format("hh:mm");
|
|
||||||
break;
|
|
||||||
case "T":
|
|
||||||
final = date.format("hh:mm:ss");
|
|
||||||
break;
|
|
||||||
case "R":
|
|
||||||
final = date.fromNow();
|
|
||||||
break;
|
|
||||||
case "D":
|
|
||||||
final = date.format("DD MMMM YYYY");
|
|
||||||
break;
|
|
||||||
case "F":
|
|
||||||
final = date.format("dddd, DD MMMM YYYY hh:mm");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
final = date.format("DD MMMM YYYY hh:mm");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return `\`${final}\``;
|
|
||||||
})
|
|
||||||
.replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
|
|
||||||
const id = args[0] as string,
|
|
||||||
user = client.users.get(id);
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
return `[@${user.username}](/@${id})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
})
|
|
||||||
.replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
|
|
||||||
const id = args[0] as string,
|
|
||||||
channel = client.channels.get(id);
|
|
||||||
|
|
||||||
if (
|
|
||||||
channel?.channel_type === "TextChannel" ||
|
|
||||||
channel?.channel_type === "VoiceChannel"
|
|
||||||
) {
|
|
||||||
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
});
|
|
||||||
|
|
||||||
const useLargeEmojis = disallowBigEmoji
|
|
||||||
? false
|
|
||||||
: content.replace(RE_TWEMOJI, "").trim().length === 0;
|
|
||||||
|
|
||||||
const toggle = useCallback((ev: MouseEvent) => {
|
|
||||||
if (ev.currentTarget) {
|
|
||||||
const element = ev.currentTarget as HTMLDivElement;
|
|
||||||
if (element.classList.contains("spoiler")) {
|
|
||||||
element.classList.add("shown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLink = useCallback(
|
|
||||||
(ev: MouseEvent) => {
|
|
||||||
if (ev.currentTarget) {
|
|
||||||
const element = ev.currentTarget as HTMLAnchorElement;
|
|
||||||
|
|
||||||
if (ev.shiftKey) {
|
|
||||||
switch (element.dataset.type) {
|
|
||||||
case "mention": {
|
|
||||||
internalEmit(
|
|
||||||
"MessageBox",
|
|
||||||
"append",
|
|
||||||
`<@${element.dataset.mentionId}>`,
|
|
||||||
"mention",
|
|
||||||
);
|
|
||||||
ev.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case "channel_mention": {
|
|
||||||
internalEmit(
|
|
||||||
"MessageBox",
|
|
||||||
"append",
|
|
||||||
`<#${element.dataset.mentionId}>`,
|
|
||||||
"channel_mention",
|
|
||||||
);
|
|
||||||
ev.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openLink(element.href)) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[openLink],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
ref={(el) => {
|
|
||||||
if (el) {
|
|
||||||
el.querySelectorAll<HTMLDivElement>(".spoiler").forEach(
|
|
||||||
(element) => {
|
|
||||||
element.removeEventListener("click", toggle);
|
|
||||||
element.addEventListener("click", toggle);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
el.querySelectorAll<HTMLAnchorElement>("a").forEach(
|
|
||||||
(element) => {
|
|
||||||
element.removeEventListener("click", handleLink);
|
|
||||||
element.addEventListener("click", handleLink);
|
|
||||||
element.removeAttribute("data-type");
|
|
||||||
element.removeAttribute("data-mention-id");
|
|
||||||
element.removeAttribute("target");
|
|
||||||
|
|
||||||
const link = determineLink(element.href);
|
|
||||||
switch (link.type) {
|
|
||||||
case "profile": {
|
|
||||||
element.setAttribute(
|
|
||||||
"data-type",
|
|
||||||
"mention",
|
|
||||||
);
|
|
||||||
element.setAttribute(
|
|
||||||
"data-mention-id",
|
|
||||||
link.id,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "navigate": {
|
|
||||||
if (link.navigation_type === "channel") {
|
|
||||||
element.setAttribute(
|
|
||||||
"data-type",
|
|
||||||
"channel_mention",
|
|
||||||
);
|
|
||||||
element.setAttribute(
|
|
||||||
"data-mention-id",
|
|
||||||
link.channel_id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "external": {
|
|
||||||
element.setAttribute("target", "_blank");
|
|
||||||
element.setAttribute("rel", "noreferrer");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={styles.markdown}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: md.render(newContent),
|
|
||||||
}}
|
|
||||||
data-large-emojis={useLargeEmojis}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
7
src/components/markdown/hast.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { passThroughComponents } from "./plugins/remarkRegexComponent";
|
||||||
|
import { timestampHandler } from "./plugins/timestamps";
|
||||||
|
|
||||||
|
export const handlers = {
|
||||||
|
...passThroughComponents("emoji", "spoiler", "mention", "channel"),
|
||||||
|
timestamp: timestampHandler,
|
||||||
|
};
|
||||||
79
src/components/markdown/plugins/Codeblock.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { useCallback, useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Tooltip } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base codeblock styles
|
||||||
|
*/
|
||||||
|
const Base = styled.pre`
|
||||||
|
padding: 1em;
|
||||||
|
overflow-x: scroll;
|
||||||
|
background: var(--block);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy codeblock contents button styles
|
||||||
|
*/
|
||||||
|
const Lang = styled.div`
|
||||||
|
font-family: var(--monospace-font);
|
||||||
|
width: fit-content;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #111;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--accent);
|
||||||
|
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: 0 2px #787676;
|
||||||
|
border-radius: calc(var(--border-radius) / 3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow: 0 1px #787676;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a codeblock with copy text button
|
||||||
|
*/
|
||||||
|
export const RenderCodeblock: React.FC<{ class: string }> = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
|
|
||||||
|
let text = "text";
|
||||||
|
if (props.class) {
|
||||||
|
text = props.class.split("-")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCopy = useCallback(() => {
|
||||||
|
const text = ref.current?.querySelector("code")?.innerText;
|
||||||
|
text && modalController.writeText(text);
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base ref={ref}>
|
||||||
|
<Lang>
|
||||||
|
<Tooltip content="Copy to Clipboard" placement="top">
|
||||||
|
{/**
|
||||||
|
// @ts-expect-error Preact-React */}
|
||||||
|
<a onClick={onCopy}>{text}</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Lang>
|
||||||
|
{children}
|
||||||
|
</Base>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/components/markdown/plugins/anchors.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { determineLink } from "../../../lib/links";
|
||||||
|
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
|
export function RenderAnchor({
|
||||||
|
href,
|
||||||
|
...props
|
||||||
|
}: JSX.HTMLAttributes<HTMLAnchorElement>) {
|
||||||
|
// Pass-through no href or if anchor
|
||||||
|
if (!href || href.startsWith("#")) return <a href={href} {...props} />;
|
||||||
|
|
||||||
|
// Determine type of link
|
||||||
|
const link = determineLink(href);
|
||||||
|
if (link.type === "none") return <a {...props} />;
|
||||||
|
|
||||||
|
// Render direct link if internal
|
||||||
|
if (link.type === "navigate") {
|
||||||
|
return <Link to={link.path} children={props.children} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
onClick={(ev) =>
|
||||||
|
modalController.openLink(href) && ev.preventDefault()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/markdown/plugins/channels.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { clientController } from "../../../controllers/client/ClientController";
|
||||||
|
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
|
||||||
|
|
||||||
|
export function RenderChannel({ match }: CustomComponentProps) {
|
||||||
|
const channel = clientController.getAvailableClient().channels.get(match)!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`${
|
||||||
|
channel.server_id ? `/server/${channel.server_id}` : ""
|
||||||
|
}/channel/${match}`}>{`#${channel.name}`}</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remarkChannels = createComponent(
|
||||||
|
"channel",
|
||||||
|
/<#([A-z0-9]{26})>/g,
|
||||||
|
(match) => clientController.getAvailableClient().channels.has(match),
|
||||||
|
);
|
||||||
66
src/components/markdown/plugins/emoji.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { emojiDictionary } from "../../../assets/emojis";
|
||||||
|
import { clientController } from "../../../controllers/client/ClientController";
|
||||||
|
import { parseEmoji } from "../../common/Emoji";
|
||||||
|
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
|
||||||
|
|
||||||
|
const Emoji = styled.img`
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
height: var(--emoji-size);
|
||||||
|
width: var(--emoji-size);
|
||||||
|
margin: 0 0.05em 0 0.1em;
|
||||||
|
vertical-align: -0.2em;
|
||||||
|
|
||||||
|
img:before {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
background-image: url(ishere.jpg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RE_EMOJI = /:([a-zA-Z0-9_+]+):/g;
|
||||||
|
const RE_ULID = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
|
||||||
|
|
||||||
|
export function RenderEmoji({ match }: CustomComponentProps) {
|
||||||
|
const [fail, setFail] = useState(false);
|
||||||
|
const url = RE_ULID.test(match)
|
||||||
|
? `${
|
||||||
|
clientController.getAvailableClient().configuration?.features
|
||||||
|
.autumn.url
|
||||||
|
}/emojis/${match}`
|
||||||
|
: parseEmoji(
|
||||||
|
match in emojiDictionary
|
||||||
|
? emojiDictionary[match as keyof typeof emojiDictionary]
|
||||||
|
: match,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fail) return <span>{`:${match}:`}</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Emoji
|
||||||
|
alt={`:${match}:`}
|
||||||
|
loading="lazy"
|
||||||
|
className="emoji"
|
||||||
|
draggable={false}
|
||||||
|
src={url}
|
||||||
|
onError={() => setFail(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remarkEmoji = createComponent(
|
||||||
|
"emoji",
|
||||||
|
RE_EMOJI,
|
||||||
|
(match) => match in emojiDictionary || RE_ULID.test(match),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isOnlyEmoji(text: string) {
|
||||||
|
return text.replaceAll(RE_EMOJI, "").trim().length === 0;
|
||||||
|
}
|
||||||
10
src/components/markdown/plugins/htmlToText.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Plugin } from "unified";
|
||||||
|
import { visit } from "unist-util-visit";
|
||||||
|
|
||||||
|
export const remarkHtmlToText: Plugin = () => {
|
||||||
|
return (tree) => {
|
||||||
|
visit(tree, "html", (node: { type: string; value: string }) => {
|
||||||
|
node.type = "text";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
53
src/components/markdown/plugins/mentions.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { RE_MENTIONS } from "revolt.js";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { clientController } from "../../../controllers/client/ClientController";
|
||||||
|
import UserShort from "../../common/user/UserShort";
|
||||||
|
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
|
||||||
|
|
||||||
|
const Mention = styled.a`
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 6px;
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none !important;
|
||||||
|
background: var(--secondary-background);
|
||||||
|
border-radius: calc(var(--border-radius) * 2);
|
||||||
|
|
||||||
|
transition: 0.1s ease filter;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
filter: brightness(0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function RenderMention({ match }: CustomComponentProps) {
|
||||||
|
return (
|
||||||
|
<Mention>
|
||||||
|
<UserShort
|
||||||
|
showServerIdentity
|
||||||
|
user={clientController.getAvailableClient().users.get(match)}
|
||||||
|
/>
|
||||||
|
</Mention>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remarkMention = createComponent("mention", RE_MENTIONS, (match) =>
|
||||||
|
clientController.getAvailableClient().users.has(match),
|
||||||
|
);
|
||||||
108
src/components/markdown/plugins/remarkRegexComponent.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import type { Handler } from "mdast-util-to-hast";
|
||||||
|
import type { Plugin } from "unified";
|
||||||
|
import { visit } from "unist-util-visit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props given to custom components
|
||||||
|
*/
|
||||||
|
export interface CustomComponentProps {
|
||||||
|
type?: string;
|
||||||
|
match: string;
|
||||||
|
arg1?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new custom component matched by a given RegExp
|
||||||
|
* @param type hast node type
|
||||||
|
* @param regex Regex to match (must have one capture group)
|
||||||
|
* @returns Unified Plugin
|
||||||
|
*/
|
||||||
|
export function createComponent(
|
||||||
|
type: string,
|
||||||
|
regex: RegExp,
|
||||||
|
validator?: (match: string) => boolean,
|
||||||
|
): Plugin {
|
||||||
|
/**
|
||||||
|
* Plugin which transforms a given RegExp into a custom component with given name.
|
||||||
|
*/
|
||||||
|
return () => {
|
||||||
|
return (tree) => {
|
||||||
|
visit(
|
||||||
|
tree,
|
||||||
|
"text",
|
||||||
|
(
|
||||||
|
node: { value: string },
|
||||||
|
index: number,
|
||||||
|
parent: { children: any[] },
|
||||||
|
) => {
|
||||||
|
const result = [];
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
regex.lastIndex = 0;
|
||||||
|
|
||||||
|
let match = regex.exec(node.value);
|
||||||
|
|
||||||
|
while (match) {
|
||||||
|
if (!validator || validator(match[1])) {
|
||||||
|
const position = match.index;
|
||||||
|
|
||||||
|
if (start !== position) {
|
||||||
|
result.push({
|
||||||
|
type: "text",
|
||||||
|
value: node.value.slice(start, position),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
type,
|
||||||
|
match: match[1],
|
||||||
|
arg1: match[2],
|
||||||
|
});
|
||||||
|
start = position + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
match = regex.exec(node.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.length > 0 &&
|
||||||
|
parent &&
|
||||||
|
typeof index === "number"
|
||||||
|
) {
|
||||||
|
if (start < node.value.length) {
|
||||||
|
result.push({
|
||||||
|
type: "text",
|
||||||
|
value: node.value.slice(start),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.splice(index, 1, ...result);
|
||||||
|
return index + result.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass-through a component as-is from remark to rehype
|
||||||
|
* @param name Tag name
|
||||||
|
* @returns Handler
|
||||||
|
*/
|
||||||
|
export const passThroughRehype: (name: string) => Handler =
|
||||||
|
(name: string) => (h, node) =>
|
||||||
|
h(node, name, node);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass-through multiple components at once
|
||||||
|
* @param keys Tags
|
||||||
|
* @returns Handlers
|
||||||
|
*/
|
||||||
|
export const passThroughComponents = (...keys: string[]) => {
|
||||||
|
const obj: Record<string, Handler> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
obj[key] = passThroughRehype(key);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
45
src/components/markdown/plugins/spoiler.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
|
||||||
|
|
||||||
|
const Spoiler = styled.span<{ shown: boolean }>`
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: transparent;
|
||||||
|
background: #151515;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.shown &&
|
||||||
|
css`
|
||||||
|
cursor: auto;
|
||||||
|
user-select: all;
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--secondary-background);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: unset;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function RenderSpoiler({ match }: CustomComponentProps) {
|
||||||
|
const [shown, setShown] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spoiler shown={shown} onClick={() => setShown(true)}>
|
||||||
|
{match}
|
||||||
|
</Spoiler>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remarkSpoiler = createComponent("spoiler", /!!([^!]+)!!/g);
|
||||||
39
src/components/markdown/plugins/timestamps.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Handler } from "mdast-util-to-hast";
|
||||||
|
|
||||||
|
import { dayjs } from "../../../context/Locale";
|
||||||
|
|
||||||
|
import { createComponent } from "./remarkRegexComponent";
|
||||||
|
|
||||||
|
export const timestampHandler: Handler = (h, { match, arg1 }) => {
|
||||||
|
if (isNaN(match)) return { type: "text", value: match };
|
||||||
|
const date = dayjs.unix(match);
|
||||||
|
|
||||||
|
let value = "";
|
||||||
|
switch (arg1) {
|
||||||
|
case "t":
|
||||||
|
value = date.format("hh:mm");
|
||||||
|
break;
|
||||||
|
case "T":
|
||||||
|
value = date.format("hh:mm:ss");
|
||||||
|
break;
|
||||||
|
case "R":
|
||||||
|
value = date.fromNow();
|
||||||
|
break;
|
||||||
|
case "D":
|
||||||
|
value = date.format("DD MMMM YYYY");
|
||||||
|
break;
|
||||||
|
case "F":
|
||||||
|
value = date.format("dddd, DD MMMM YYYY hh:mm");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = date.format("DD MMMM YYYY hh:mm");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(null, "code", {}, [{ type: "text", value }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remarkTimestamps = createComponent(
|
||||||
|
"timestamp",
|
||||||
|
/<t:([0-9]+)(?::(\w))?>/g,
|
||||||
|
);
|
||||||
@@ -34,7 +34,6 @@ import "prismjs/components/prism-r";
|
|||||||
import "prismjs/components/prism-sql";
|
import "prismjs/components/prism-sql";
|
||||||
import "prismjs/components/prism-graphql";
|
import "prismjs/components/prism-graphql";
|
||||||
import "prismjs/components/prism-shell-session";
|
import "prismjs/components/prism-shell-session";
|
||||||
import "prismjs/components/prism-java";
|
|
||||||
import "prismjs/components/prism-powershell";
|
import "prismjs/components/prism-powershell";
|
||||||
import "prismjs/components/prism-swift";
|
import "prismjs/components/prism-swift";
|
||||||
import "prismjs/components/prism-yaml";
|
import "prismjs/components/prism-yaml";
|
||||||
@@ -87,7 +86,6 @@ import "prismjs/components/prism-moonscript";
|
|||||||
import "prismjs/components/prism-qml";
|
import "prismjs/components/prism-qml";
|
||||||
import "prismjs/components/prism-vim";
|
import "prismjs/components/prism-vim";
|
||||||
import "prismjs/components/prism-nim";
|
import "prismjs/components/prism-nim";
|
||||||
import "prismjs/components/prism-swift";
|
|
||||||
import "prismjs/components/prism-haml";
|
import "prismjs/components/prism-haml";
|
||||||
import "prismjs/components/prism-ada";
|
import "prismjs/components/prism-ada";
|
||||||
import "prismjs/components/prism-arduino";
|
import "prismjs/components/prism-arduino";
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import ConditionalLink from "../../lib/ConditionalLink";
|
|||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
|
||||||
import UserIcon from "../common/user/UserIcon";
|
import UserIcon from "../common/user/UserIcon";
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import { IconButton } from "@revoltchat/ui";
|
|||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import ChannelIcon from "../../common/ChannelIcon";
|
import ChannelIcon from "../../common/ChannelIcon";
|
||||||
import Tooltip from "../../common/Tooltip";
|
import Tooltip from "../../common/Tooltip";
|
||||||
import UserIcon from "../../common/user/UserIcon";
|
import UserIcon from "../../common/user/UserIcon";
|
||||||
@@ -50,7 +49,6 @@ export const UserButton = observer((props: UserProps) => {
|
|||||||
channel,
|
channel,
|
||||||
...divProps
|
...divProps
|
||||||
} = props;
|
} = props;
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -111,8 +109,7 @@ export const UserButton = observer((props: UserProps) => {
|
|||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
stopPropagation(e) &&
|
stopPropagation(e) &&
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "special_prompt",
|
|
||||||
type: "close_dm",
|
type: "close_dm",
|
||||||
target: channel,
|
target: channel,
|
||||||
})
|
})
|
||||||
@@ -149,7 +146,6 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||||||
return <UserButton {...{ active, alert, channel, user }} />;
|
return <UserButton {...{ active, alert, channel, user }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const alerting = alert && !muted && !active;
|
const alerting = alert && !muted && !active;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -159,7 +155,9 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||||||
data-alert={alerting}
|
data-alert={alerting}
|
||||||
data-muted={muted}
|
data-muted={muted}
|
||||||
aria-label={channel.name}
|
aria-label={channel.name}
|
||||||
className={classNames(styles.item, { [styles.compact]: compact })}
|
className={classNames(styles.item, {
|
||||||
|
[styles.compact]: compact,
|
||||||
|
})}
|
||||||
{...useTriggerEvents("Menu", {
|
{...useTriggerEvents("Menu", {
|
||||||
channel: channel._id,
|
channel: channel._id,
|
||||||
unread: !!alert,
|
unread: !!alert,
|
||||||
@@ -179,7 +177,9 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||||||
<Text
|
<Text
|
||||||
id="quantities.members"
|
id="quantities.members"
|
||||||
plural={channel.recipients!.length}
|
plural={channel.recipients!.length}
|
||||||
fields={{ count: channel.recipients!.length }}
|
fields={{
|
||||||
|
count: channel.recipients!.length,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -195,8 +195,7 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "special_prompt",
|
|
||||||
type: "leave_group",
|
type: "leave_group",
|
||||||
target: channel,
|
target: channel,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Banner } from "@revoltchat/ui";
|
import { Banner, Button, Column } from "@revoltchat/ui";
|
||||||
|
|
||||||
import {
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
useClient,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export default function ConnectionStatus() {
|
function ConnectionStatus() {
|
||||||
const status = useContext(StatusContext);
|
const session = useSession()!;
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
if (status === ClientStatus.OFFLINE) {
|
if (session.state === "Offline") {
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<Text id="app.special.status.offline" />
|
<Text id="app.special.status.offline" />
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
} else if (status === ClientStatus.DISCONNECTED) {
|
} else if (session.state === "Disconnected") {
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<Text id="app.special.status.disconnected" /> <br />
|
<Column centred>
|
||||||
<a onClick={() => client.websocket.connect()}>
|
<Text id="app.special.status.disconnected" />
|
||||||
<Text id="app.special.status.reconnect" />
|
<Button
|
||||||
</a>
|
compact
|
||||||
|
palette="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
session.emit({
|
||||||
|
action: "RETRY",
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Text id="app.status.reconnect" />
|
||||||
|
</Button>
|
||||||
|
</Column>
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
} else if (status === ClientStatus.CONNECTING) {
|
} else if (session.state === "Connecting") {
|
||||||
return (
|
|
||||||
<Banner>
|
|
||||||
<Text id="app.special.status.connecting" />
|
|
||||||
</Banner>
|
|
||||||
);
|
|
||||||
} else if (status === ClientStatus.RECONNECTING) {
|
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<Text id="app.special.status.reconnecting" />
|
<Text id="app.special.status.reconnecting" />
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default observer(ConnectionStatus);
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import placeholderSVG from "../items/placeholder.svg";
|
import placeholderSVG from "../items/placeholder.svg";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
||||||
import ConnectionStatus from "../items/ConnectionStatus";
|
import ConnectionStatus from "../items/ConnectionStatus";
|
||||||
@@ -46,10 +45,9 @@ const Navbar = styled.div`
|
|||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const state = useApplicationState();
|
const state = useApplicationState();
|
||||||
const { channel: channel_id } = useParams<{ channel: string }>();
|
const { channel: channel_id } = useParams<{ channel: string }>();
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
const channels = [...client.channels.values()].filter(
|
const channels = [...client.channels.values()].filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
@@ -131,8 +129,7 @@ export default observer(() => {
|
|||||||
<Text id="app.main.categories.conversations" />
|
<Text id="app.main.categories.conversations" />
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "special_input",
|
|
||||||
type: "create_group",
|
type: "create_group",
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import { ServerList } from "@revoltchat/ui";
|
|||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
import { IS_REVOLT } from "../../../version";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server list sidebar shim component
|
* Server list sidebar shim component
|
||||||
@@ -16,13 +17,11 @@ import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const state = useApplicationState();
|
const state = useApplicationState();
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const { server: server_id } = useParams<{ server?: string }>();
|
const { server: server_id } = useParams<{ server?: string }>();
|
||||||
|
|
||||||
const createServer = useCallback(
|
const createServer = useCallback(
|
||||||
() =>
|
() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "special_input",
|
|
||||||
type: "create_server",
|
type: "create_server",
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
@@ -37,6 +36,7 @@ export default observer(() => {
|
|||||||
home={state.layout.getLastHomePath}
|
home={state.layout.getLastHomePath}
|
||||||
servers={state.ordering.orderedServers}
|
servers={state.ordering.orderedServers}
|
||||||
reorder={state.ordering.reorderServer}
|
reorder={state.ordering.reorderServer}
|
||||||
|
showDiscovery={IS_REVOLT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import CollapsibleSection from "../../common/CollapsibleSection";
|
import CollapsibleSection from "../../common/CollapsibleSection";
|
||||||
import ServerHeader from "../../common/ServerHeader";
|
import ServerHeader from "../../common/ServerHeader";
|
||||||
import { ChannelButton } from "../items/ButtonItem";
|
import { ChannelButton } from "../items/ButtonItem";
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import { memo } from "preact/compat";
|
|||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
|
||||||
import {
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
Screen,
|
|
||||||
useIntermediate,
|
|
||||||
} from "../../../context/intermediate/Intermediate";
|
|
||||||
|
|
||||||
import { UserButton } from "../items/ButtonItem";
|
import { UserButton } from "../items/ButtonItem";
|
||||||
|
|
||||||
export type MemberListGroup = {
|
export type MemberListGroup = {
|
||||||
@@ -55,15 +51,7 @@ const NoOomfie = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ItemContent = memo(
|
const ItemContent = memo(
|
||||||
({
|
({ item, context }: { item: User; context: Channel }) => (
|
||||||
item,
|
|
||||||
context,
|
|
||||||
openScreen,
|
|
||||||
}: {
|
|
||||||
item: User;
|
|
||||||
context: Channel;
|
|
||||||
openScreen: (screen: Screen) => void;
|
|
||||||
}) => (
|
|
||||||
<UserButton
|
<UserButton
|
||||||
key={item._id}
|
key={item._id}
|
||||||
user={item}
|
user={item}
|
||||||
@@ -77,13 +65,12 @@ const ItemContent = memo(
|
|||||||
`<@${item._id}>`,
|
`<@${item._id}>`,
|
||||||
"mention",
|
"mention",
|
||||||
);
|
);
|
||||||
} else
|
} else {
|
||||||
[
|
modalController.push({
|
||||||
openScreen({
|
type: "user_profile",
|
||||||
id: "profile",
|
user_id: item._id,
|
||||||
user_id: item._id,
|
});
|
||||||
}),
|
}
|
||||||
];
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -96,8 +83,6 @@ export default function MemberList({
|
|||||||
entries: MemberListGroup[];
|
entries: MemberListGroup[];
|
||||||
context: Channel;
|
context: Channel;
|
||||||
}) {
|
}) {
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupedVirtuoso
|
<GroupedVirtuoso
|
||||||
groupCounts={entries.map((x) => x.users.length)}
|
groupCounts={entries.map((x) => x.users.length)}
|
||||||
@@ -114,7 +99,7 @@ export default function MemberList({
|
|||||||
)}
|
)}
|
||||||
{entry.type !== "no_offline" && (
|
{entry.type !== "no_offline" && (
|
||||||
<>
|
<>
|
||||||
{" - "}
|
{" – "}
|
||||||
{entry.users.length}
|
{entry.users.length}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -133,19 +118,20 @@ export default function MemberList({
|
|||||||
return (
|
return (
|
||||||
<NoOomfie>
|
<NoOomfie>
|
||||||
<div>
|
<div>
|
||||||
Offline users temporarily disabled for this
|
Offline users have temporarily been disabled for
|
||||||
server, see issue{" "}
|
larger servers - see{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/revoltchat/delta/issues/128"
|
href="https://github.com/revoltchat/backend/issues/178"
|
||||||
target="_blank" rel="noreferrer">
|
target="_blank"
|
||||||
#128
|
rel="noreferrer">
|
||||||
|
issue #178
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
for when this will be resolved.
|
for when this will be resolved.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
You may re-enable them in{" "}
|
You may re-enable them{" "}
|
||||||
<Link to="/settings/experiments">
|
<Link to="/settings/experiments">
|
||||||
<a>experiments</a>
|
<a>here</a>
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
@@ -158,11 +144,7 @@ export default function MemberList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ItemContent
|
<ItemContent item={item} context={context} />
|
||||||
item={item}
|
|
||||||
context={context}
|
|
||||||
openScreen={openScreen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Channel, Server, User, API } from "revolt.js";
|
import { Channel, Server, User, API } from "revolt.js";
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClientStatus,
|
useSession,
|
||||||
StatusContext,
|
|
||||||
useClient,
|
useClient,
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
} from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import { GenericSidebarBase } from "../SidebarBase";
|
import { GenericSidebarBase } from "../SidebarBase";
|
||||||
import MemberList, { MemberListGroup } from "./MemberList";
|
import MemberList, { MemberListGroup } from "./MemberList";
|
||||||
|
|
||||||
@@ -205,18 +203,18 @@ function shouldSkipOffline(id: string) {
|
|||||||
|
|
||||||
export const ServerMemberSidebar = observer(
|
export const ServerMemberSidebar = observer(
|
||||||
({ channel }: { channel: Channel }) => {
|
({ channel }: { channel: Channel }) => {
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const server_id = channel.server_id!;
|
const server_id = channel.server_id!;
|
||||||
if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) {
|
if (session.state === "Online" && !FETCHED.has(server_id)) {
|
||||||
FETCHED.add(server_id);
|
FETCHED.add(server_id);
|
||||||
channel
|
channel
|
||||||
.server!.syncMembers(shouldSkipOffline(server_id))
|
.server!.syncMembers(shouldSkipOffline(server_id))
|
||||||
.catch(() => FETCHED.delete(server_id));
|
.catch(() => FETCHED.delete(server_id));
|
||||||
}
|
}
|
||||||
}, [status, channel]);
|
}, [session.state, channel]);
|
||||||
|
|
||||||
const entries = useEntries(
|
const entries = useEntries(
|
||||||
channel,
|
channel,
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { useEffect, useState } from "preact/hooks";
|
|||||||
|
|
||||||
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import Message from "../../common/messaging/Message";
|
import Message from "../../common/messaging/Message";
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ import { Block } from "@styled-icons/boxicons-regular";
|
|||||||
import { Trash } from "@styled-icons/boxicons-solid";
|
import { Trash } from "@styled-icons/boxicons-solid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { CategoryButton } from "@revoltchat/ui";
|
import { CategoryButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
|
||||||
import {
|
import {
|
||||||
LogOutContext,
|
clientController,
|
||||||
useClient,
|
useClient,
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
} from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
export default function AccountManagement() {
|
export default function AccountManagement() {
|
||||||
const logOut = useContext(LogOutContext);
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const callback = (route: "disable" | "delete") => () =>
|
const callback = (route: "disable" | "delete") => () =>
|
||||||
@@ -26,7 +24,7 @@ export default function AccountManagement() {
|
|||||||
"X-MFA-Ticket": ticket.token,
|
"X-MFA-Ticket": ticket.token,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => logOut(true)),
|
.then(clientController.logoutCurrent),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AccountDetail,
|
AccountDetail,
|
||||||
@@ -12,26 +12,22 @@ import {
|
|||||||
HiddenValue,
|
HiddenValue,
|
||||||
} from "@revoltchat/ui";
|
} from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
import {
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
useClient,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
|
|
||||||
const [email, setEmail] = useState("...");
|
const [email, setEmail] = useState("...");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
if (email === "..." && session.state === "Online") {
|
||||||
client.api
|
client.api
|
||||||
.get("/auth/account/")
|
.get("/auth/account/")
|
||||||
.then((account) => setEmail(account.email));
|
.then((account) => setEmail(account.email));
|
||||||
}
|
}
|
||||||
}, [client, email, status]);
|
}, [client, email, session.state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import { Lock } from "@styled-icons/boxicons-solid";
|
|||||||
import { API } from "revolt.js";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
import {
|
import { takeError } from "../../../controllers/client/jsx/error";
|
||||||
ClientStatus,
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
StatusContext,
|
|
||||||
useClient,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary helper function for Axios config
|
* Temporary helper function for Axios config
|
||||||
@@ -33,8 +29,8 @@ export function toConfig(token: string) {
|
|||||||
*/
|
*/
|
||||||
export default function MultiFactorAuthentication() {
|
export default function MultiFactorAuthentication() {
|
||||||
// Pull in prerequisites
|
// Pull in prerequisites
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
|
|
||||||
// Keep track of MFA state
|
// Keep track of MFA state
|
||||||
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
||||||
@@ -42,13 +38,13 @@ export default function MultiFactorAuthentication() {
|
|||||||
|
|
||||||
// Fetch the current MFA status on account
|
// Fetch the current MFA status on account
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mfa && status === ClientStatus.ONLINE) {
|
if (!mfa && session.state === "Online") {
|
||||||
client.api
|
client!.api
|
||||||
.get("/auth/mfa/")
|
.get("/auth/mfa/")
|
||||||
.then(setMFA)
|
.then(setMFA)
|
||||||
.catch((err) => setError(takeError(err)));
|
.catch((err) => setError(takeError(err)));
|
||||||
}
|
}
|
||||||
}, [client, mfa, status]);
|
}, [mfa, client, session.state]);
|
||||||
|
|
||||||
// Action called when recovery code button is pressed
|
// Action called when recovery code button is pressed
|
||||||
const recoveryAction = useCallback(async () => {
|
const recoveryAction = useCallback(async () => {
|
||||||
@@ -104,7 +100,11 @@ export default function MultiFactorAuthentication() {
|
|||||||
// Decide whether to disable or enable.
|
// Decide whether to disable or enable.
|
||||||
if (mfa!.totp_mfa) {
|
if (mfa!.totp_mfa) {
|
||||||
// Disable TOTP authentication
|
// Disable TOTP authentication
|
||||||
await client.api.delete("/auth/mfa/totp", toConfig(ticket.token));
|
await client.api.delete(
|
||||||
|
"/auth/mfa/totp",
|
||||||
|
{},
|
||||||
|
toConfig(ticket.token),
|
||||||
|
);
|
||||||
|
|
||||||
setMFA({
|
setMFA({
|
||||||
...mfa!,
|
...mfa!,
|
||||||
|
|||||||
63
src/components/settings/appearance/AdvancedOptions.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { ObservedInputElement } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MonospaceFonts,
|
||||||
|
MONOSPACE_FONTS,
|
||||||
|
MONOSPACE_FONT_KEYS,
|
||||||
|
} from "../../../context/Theme";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! LEGACY
|
||||||
|
* Component providing a way to edit custom CSS.
|
||||||
|
*/
|
||||||
|
export const ShimThemeCustomCSS = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.custom_css" />
|
||||||
|
</h3>
|
||||||
|
<TextAreaAutoSize
|
||||||
|
maxRows={20}
|
||||||
|
minHeight={480}
|
||||||
|
code
|
||||||
|
value={theme.getCSS() ?? ""}
|
||||||
|
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AdvancedOptions() {
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/** Combo box of available monospaced fonts */}
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.mono_font" />
|
||||||
|
</h3>
|
||||||
|
<ObservedInputElement
|
||||||
|
type="combo"
|
||||||
|
value={() => settings.theme.getMonospaceFont()}
|
||||||
|
onChange={(value) =>
|
||||||
|
settings.theme.setMonospaceFont(value as MonospaceFonts)
|
||||||
|
}
|
||||||
|
options={MONOSPACE_FONT_KEYS.map((value) => ({
|
||||||
|
value,
|
||||||
|
name: MONOSPACE_FONTS[value as keyof typeof MONOSPACE_FONTS]
|
||||||
|
.name,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
{/** Custom CSS */}
|
||||||
|
<ShimThemeCustomCSS />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/components/settings/appearance/AppearanceOptions.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Column, ObservedInputElement } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
export default function AppearanceOptions() {
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.appearance_options.title" />
|
||||||
|
</h3>
|
||||||
|
{/* Option to toggle "send message" button on desktop. */}
|
||||||
|
<ObservedInputElement
|
||||||
|
type="checkbox"
|
||||||
|
value={() =>
|
||||||
|
settings.get("appearance:show_send_button") ?? false
|
||||||
|
}
|
||||||
|
onChange={(v) => settings.set("appearance:show_send_button", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* Option to always show the account creation age next to join system messages. */}
|
||||||
|
<ObservedInputElement
|
||||||
|
type="checkbox"
|
||||||
|
value={() =>
|
||||||
|
settings.get("appearance:show_account_age") ?? false
|
||||||
|
}
|
||||||
|
onChange={(v) => settings.set("appearance:show_account_age", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.appearance_options.show_account_age" />
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.appearance_options.show_account_age_desc" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.title" />
|
||||||
|
</h3>
|
||||||
|
<Column>
|
||||||
|
{/* Option to toggle transparency effects in-app. */}
|
||||||
|
<ObservedInputElement
|
||||||
|
type="checkbox"
|
||||||
|
value={() =>
|
||||||
|
settings.get("appearance:transparency") ?? true
|
||||||
|
}
|
||||||
|
onChange={(v) => settings.set("appearance:transparency", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.transparency" />
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* Option to toggle seasonal effects. */}
|
||||||
|
<ObservedInputElement
|
||||||
|
type="checkbox"
|
||||||
|
value={() => settings.get("appearance:seasonal") ?? true}
|
||||||
|
onChange={(v) => settings.set("appearance:seasonal", v)}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/settings/appearance/ChatOptions.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Column, ObservedInputElement } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import { FONTS, Fonts, FONT_KEYS } from "../../../context/Theme";
|
||||||
|
|
||||||
|
import { EmojiSelector } from "./legacy/EmojiSelector";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! LEGACY
|
||||||
|
* Component providing a way to change emoji pack.
|
||||||
|
*/
|
||||||
|
export const ShimDisplayEmoji = observer(() => {
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
return (
|
||||||
|
<EmojiSelector
|
||||||
|
value={settings.get("appearance:emoji")}
|
||||||
|
setValue={(v) => settings.set("appearance:emoji", v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default observer(() => {
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Column>
|
||||||
|
{/* Combo box of available fonts. */}
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.font" />
|
||||||
|
</h3>
|
||||||
|
<ObservedInputElement
|
||||||
|
type="combo"
|
||||||
|
value={() => settings.theme.getFont()}
|
||||||
|
onChange={(value) => settings.theme.setFont(value as Fonts)}
|
||||||
|
options={FONT_KEYS.map((value) => ({
|
||||||
|
value,
|
||||||
|
name: FONTS[value as keyof typeof FONTS].name,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
{/* Option to toggle liagures for supported fonts. */}
|
||||||
|
{settings.theme.getFont() === "Inter" && (
|
||||||
|
<ObservedInputElement
|
||||||
|
type="checkbox"
|
||||||
|
value={() =>
|
||||||
|
settings.get("appearance:ligatures") ?? true
|
||||||
|
}
|
||||||
|
onChange={(v) =>
|
||||||
|
settings.set("appearance:ligatures", v)
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<Text id="app.settings.pages.appearance.ligatures" />
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
<hr />
|
||||||
|
{/* Emoji pack selector */}
|
||||||
|
<ShimDisplayEmoji />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import { Brush } from "@styled-icons/boxicons-solid";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
// @ts-expect-error shade-blend-color does not have typings.
|
|
||||||
import pSBC from "shade-blend-color";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CategoryButton,
|
|
||||||
Checkbox,
|
|
||||||
ColourSwatches,
|
|
||||||
ComboBox,
|
|
||||||
Radio,
|
|
||||||
} from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Fonts,
|
|
||||||
FONTS,
|
|
||||||
FONT_KEYS,
|
|
||||||
MonospaceFonts,
|
|
||||||
MONOSPACE_FONTS,
|
|
||||||
MONOSPACE_FONT_KEYS,
|
|
||||||
} from "../../../context/Theme";
|
|
||||||
|
|
||||||
import { EmojiSelector } from "./EmojiSelector";
|
|
||||||
import { ThemeBaseSelector } from "./ThemeBaseSelector";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to switch the base theme being used.
|
|
||||||
*/
|
|
||||||
export const ShimThemeBaseSelector = observer(() => {
|
|
||||||
const theme = useApplicationState().settings.theme;
|
|
||||||
return (
|
|
||||||
<ThemeBaseSelector
|
|
||||||
value={theme.isModified() ? undefined : theme.getBase()}
|
|
||||||
setValue={(base) => {
|
|
||||||
theme.setBase(base);
|
|
||||||
theme.reset();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a link to the theme shop.
|
|
||||||
* Only appears if experiment is enabled.
|
|
||||||
*/
|
|
||||||
export const ShimThemeShop = () => {
|
|
||||||
return (
|
|
||||||
<Link to="/discover/themes" replace>
|
|
||||||
<CategoryButton
|
|
||||||
icon={<Brush size={24} />}
|
|
||||||
action="chevron"
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.discover.description" />
|
|
||||||
}>
|
|
||||||
<Text id="app.settings.pages.appearance.discover.title" />
|
|
||||||
</CategoryButton>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to change current accent colour.
|
|
||||||
*/
|
|
||||||
export const ShimThemeAccent = observer(() => {
|
|
||||||
const theme = useApplicationState().settings.theme;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
|
||||||
</h3>
|
|
||||||
<ColourSwatches
|
|
||||||
value={theme.getVariable("accent")}
|
|
||||||
onChange={(colour) => {
|
|
||||||
theme.setVariable("accent", colour as string);
|
|
||||||
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to edit custom CSS.
|
|
||||||
*/
|
|
||||||
export const ShimThemeCustomCSS = observer(() => {
|
|
||||||
const theme = useApplicationState().settings.theme;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.custom_css" />
|
|
||||||
</h3>
|
|
||||||
<TextAreaAutoSize
|
|
||||||
maxRows={20}
|
|
||||||
minHeight={480}
|
|
||||||
code
|
|
||||||
value={theme.getCSS() ?? ""}
|
|
||||||
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to switch between compact and normal message view.
|
|
||||||
*/
|
|
||||||
export const ShimDisplayCompact = () => {
|
|
||||||
// TODO: WIP feature
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.message_display" />
|
|
||||||
</h3>
|
|
||||||
<div /* className={styles.display} */>
|
|
||||||
<Radio
|
|
||||||
title={
|
|
||||||
<Text id="app.settings.pages.appearance.display.default" />
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.display.default_description" />
|
|
||||||
}
|
|
||||||
value={true}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
title={
|
|
||||||
<Text id="app.settings.pages.appearance.display.compact" />
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.display.compact_description" />
|
|
||||||
}
|
|
||||||
value={false}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to change primary text font.
|
|
||||||
*/
|
|
||||||
export const ShimDisplayFont = observer(() => {
|
|
||||||
const theme = useApplicationState().settings.theme;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.font" />
|
|
||||||
</h3>
|
|
||||||
<ComboBox
|
|
||||||
value={theme.getFont()}
|
|
||||||
onChange={(e) => theme.setFont(e.currentTarget.value as Fonts)}>
|
|
||||||
{FONT_KEYS.map((key) => (
|
|
||||||
<option value={key} key={key}>
|
|
||||||
{FONTS[key as keyof typeof FONTS].name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</ComboBox>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to change secondary, monospace text font.
|
|
||||||
*/
|
|
||||||
export const ShimDisplayMonospaceFont = observer(() => {
|
|
||||||
const theme = useApplicationState().settings.theme;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.mono_font" />
|
|
||||||
</h3>
|
|
||||||
<ComboBox
|
|
||||||
value={theme.getMonospaceFont()}
|
|
||||||
onChange={(e) =>
|
|
||||||
theme.setMonospaceFont(
|
|
||||||
e.currentTarget.value as MonospaceFonts,
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
{MONOSPACE_FONT_KEYS.map((key) => (
|
|
||||||
<option value={key} key={key}>
|
|
||||||
{
|
|
||||||
MONOSPACE_FONTS[key as keyof typeof MONOSPACE_FONTS]
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</ComboBox>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to toggle font ligatures.
|
|
||||||
*/
|
|
||||||
export const ShimDisplayLigatures = observer(() => {
|
|
||||||
const settings = useApplicationState().settings;
|
|
||||||
if (settings.theme.getFont() !== "Inter") return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Checkbox
|
|
||||||
value={settings.get("appearance:ligatures") ?? false}
|
|
||||||
onChange={(v) => settings.set("appearance:ligatures", v)}
|
|
||||||
title={<Text id="app.settings.pages.appearance.ligatures" />}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to toggle showing the send button on desktop.
|
|
||||||
*/
|
|
||||||
export const ShimShowSendButton = observer(() => {
|
|
||||||
const settings = useApplicationState().settings;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
value={settings.get("appearance:show_send_button") ?? false}
|
|
||||||
onChange={(v) => settings.set("appearance:show_send_button", v)}
|
|
||||||
title={
|
|
||||||
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to toggle seasonal themes.
|
|
||||||
*/
|
|
||||||
export const ShimDisplaySeasonal = observer(() => {
|
|
||||||
const settings = useApplicationState().settings;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
value={settings.get("appearance:seasonal") ?? true}
|
|
||||||
onChange={(v) => settings.set("appearance:seasonal", v)}
|
|
||||||
title={
|
|
||||||
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to toggle transparency effects.
|
|
||||||
*/
|
|
||||||
export const ShimDisplayTransparency = observer(() => {
|
|
||||||
const settings = useApplicationState().settings;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
value={settings.get("appearance:transparency") ?? true}
|
|
||||||
onChange={(v) => settings.set("appearance:transparency", v)}
|
|
||||||
title={
|
|
||||||
<Text id="app.settings.pages.appearance.theme_options.transparency" />
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component providing a way to change emoji pack.
|
|
||||||
*/
|
|
||||||
export const ShimDisplayEmoji = observer(() => {
|
|
||||||
const settings = useApplicationState().settings;
|
|
||||||
return (
|
|
||||||
<EmojiSelector
|
|
||||||
value={settings.get("appearance:emoji")}
|
|
||||||
setValue={(v) => settings.set("appearance:emoji", v)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,170 +1,11 @@
|
|||||||
import { Pencil } from "@styled-icons/boxicons-regular";
|
import Overrides from "./legacy/ThemeOverrides";
|
||||||
import { observer } from "mobx-react-lite";
|
import ThemeTools from "./legacy/ThemeTools";
|
||||||
import styled from "styled-components/macro";
|
|
||||||
|
|
||||||
import { InputBox } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import { useDebounceCallback } from "../../../lib/debounce";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
|
||||||
|
|
||||||
import { Variables } from "../../../context/Theme";
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
row-gap: 8px;
|
|
||||||
display: grid;
|
|
||||||
column-gap: 16px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.entry {
|
|
||||||
padding: 12px;
|
|
||||||
margin-top: 8px;
|
|
||||||
border: 1px solid black;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
span {
|
|
||||||
flex: 1;
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
|
|
||||||
background: inherit;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.override {
|
|
||||||
gap: 8px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.picker {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
display: grid;
|
|
||||||
cursor: pointer;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: var(--primary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
width: 0;
|
|
||||||
min-width: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
input {
|
|
||||||
opacity: 0;
|
|
||||||
border: none;
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
top: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default observer(() => {
|
|
||||||
const theme = useApplicationState().settings.theme;
|
|
||||||
const setVariable = useDebounceCallback(
|
|
||||||
(data) => {
|
|
||||||
const { key, value } = data as { key: Variables; value: string };
|
|
||||||
theme.setVariable(key, value);
|
|
||||||
},
|
|
||||||
[theme],
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export default function ThemeOverrides() {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<>
|
||||||
{(
|
<ThemeTools />
|
||||||
[
|
<Overrides />
|
||||||
"accent",
|
</>
|
||||||
"background",
|
|
||||||
"foreground",
|
|
||||||
"primary-background",
|
|
||||||
"primary-header",
|
|
||||||
"secondary-background",
|
|
||||||
"secondary-foreground",
|
|
||||||
"secondary-header",
|
|
||||||
"tertiary-background",
|
|
||||||
"tertiary-foreground",
|
|
||||||
"block",
|
|
||||||
"message-box",
|
|
||||||
"mention",
|
|
||||||
"scrollbar-thumb",
|
|
||||||
"scrollbar-track",
|
|
||||||
"status-online",
|
|
||||||
"status-away",
|
|
||||||
"status-busy",
|
|
||||||
"status-streaming",
|
|
||||||
"status-invisible",
|
|
||||||
"success",
|
|
||||||
"warning",
|
|
||||||
"error",
|
|
||||||
"hover",
|
|
||||||
] as const
|
|
||||||
).map((key) => (
|
|
||||||
<div
|
|
||||||
className="entry"
|
|
||||||
key={key}
|
|
||||||
style={{ backgroundColor: theme.getVariable(key) }}>
|
|
||||||
<div className="input">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={theme.getVariable(key)}
|
|
||||||
onChange={(el) =>
|
|
||||||
setVariable({
|
|
||||||
key,
|
|
||||||
value: el.currentTarget.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: theme.getContrastingVariable(
|
|
||||||
key,
|
|
||||||
theme.getVariable("primary-background"),
|
|
||||||
),
|
|
||||||
}}>
|
|
||||||
{key}
|
|
||||||
</span>
|
|
||||||
<div className="override">
|
|
||||||
<div
|
|
||||||
className="picker"
|
|
||||||
onClick={(e) =>
|
|
||||||
e.currentTarget.parentElement?.parentElement
|
|
||||||
?.querySelector("input")
|
|
||||||
?.click()
|
|
||||||
}>
|
|
||||||
<Pencil size={24} />
|
|
||||||
</div>
|
|
||||||
<InputBox
|
|
||||||
type="text"
|
|
||||||
className="text"
|
|
||||||
value={theme.getVariable(key)}
|
|
||||||
onChange={(el) =>
|
|
||||||
setVariable({
|
|
||||||
key,
|
|
||||||
value: el.currentTarget.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
64
src/components/settings/appearance/ThemeSelection.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Brush } from "@styled-icons/boxicons-solid";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
// @ts-expect-error shade-blend-color does not have typings.
|
||||||
|
import pSBC from "shade-blend-color";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { CategoryButton, ObservedInputElement } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import { ThemeBaseSelector } from "./legacy/ThemeBaseSelector";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! LEGACY
|
||||||
|
* Component providing a way to switch the base theme being used.
|
||||||
|
*/
|
||||||
|
export const ShimThemeBaseSelector = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<ThemeBaseSelector
|
||||||
|
value={theme.isModified() ? undefined : theme.getBase()}
|
||||||
|
setValue={(base) => {
|
||||||
|
theme.setBase(base);
|
||||||
|
theme.reset();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ThemeSelection() {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/** Allow users to change base theme */}
|
||||||
|
<ShimThemeBaseSelector />
|
||||||
|
{/** Provide a link to the theme shop */}
|
||||||
|
<Link to="/discover/themes" replace>
|
||||||
|
<CategoryButton
|
||||||
|
icon={<Brush size={24} />}
|
||||||
|
action="chevron"
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.discover.description" />
|
||||||
|
}>
|
||||||
|
<Text id="app.settings.pages.appearance.discover.title" />
|
||||||
|
</CategoryButton>
|
||||||
|
</Link>
|
||||||
|
<hr />
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||||
|
</h3>
|
||||||
|
<ObservedInputElement
|
||||||
|
type="colour"
|
||||||
|
value={theme.getVariable("accent")}
|
||||||
|
onChange={(colour) => {
|
||||||
|
theme.setVariable("accent", colour as string);
|
||||||
|
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import notoSVG from "./assets/noto_emoji.svg";
|
|||||||
import openmojiSVG from "./assets/openmoji_emoji.svg";
|
import openmojiSVG from "./assets/openmoji_emoji.svg";
|
||||||
import twemojiSVG from "./assets/twemoji_emoji.svg";
|
import twemojiSVG from "./assets/twemoji_emoji.svg";
|
||||||
|
|
||||||
import { EmojiPack } from "../../common/Emoji";
|
import { EmojiPack } from "../../../common/Emoji";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
1
src/components/settings/appearance/legacy/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
These components need to be ported to @revoltchat/ui.
|
||||||
170
src/components/settings/appearance/legacy/ThemeOverrides.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Pencil } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
|
import { InputBox } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { useDebounceCallback } from "../../../../lib/debounce";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../../mobx/State";
|
||||||
|
|
||||||
|
import { Variables } from "../../../../context/Theme";
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
row-gap: 8px;
|
||||||
|
display: grid;
|
||||||
|
column-gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
background: inherit;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.override {
|
||||||
|
gap: 8px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
display: grid;
|
||||||
|
cursor: pointer;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: var(--primary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
input {
|
||||||
|
opacity: 0;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
top: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
const setVariable = useDebounceCallback(
|
||||||
|
(data) => {
|
||||||
|
const { key, value } = data as { key: Variables; value: string };
|
||||||
|
theme.setVariable(key, value);
|
||||||
|
},
|
||||||
|
[theme],
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
"accent",
|
||||||
|
"background",
|
||||||
|
"foreground",
|
||||||
|
"primary-background",
|
||||||
|
"primary-header",
|
||||||
|
"secondary-background",
|
||||||
|
"secondary-foreground",
|
||||||
|
"secondary-header",
|
||||||
|
"tertiary-background",
|
||||||
|
"tertiary-foreground",
|
||||||
|
"block",
|
||||||
|
"message-box",
|
||||||
|
"mention",
|
||||||
|
"scrollbar-thumb",
|
||||||
|
"scrollbar-track",
|
||||||
|
"status-online",
|
||||||
|
"status-away",
|
||||||
|
"status-busy",
|
||||||
|
"status-streaming",
|
||||||
|
"status-invisible",
|
||||||
|
"success",
|
||||||
|
"warning",
|
||||||
|
"error",
|
||||||
|
"hover",
|
||||||
|
] as const
|
||||||
|
).map((key) => (
|
||||||
|
<div
|
||||||
|
className="entry"
|
||||||
|
key={key}
|
||||||
|
style={{ backgroundColor: theme.getVariable(key) }}>
|
||||||
|
<div className="input">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={theme.getVariable(key)}
|
||||||
|
onChange={(el) =>
|
||||||
|
setVariable({
|
||||||
|
key,
|
||||||
|
value: el.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.getContrastingVariable(
|
||||||
|
key,
|
||||||
|
theme.getVariable("primary-background"),
|
||||||
|
),
|
||||||
|
}}>
|
||||||
|
{key}
|
||||||
|
</span>
|
||||||
|
<div className="override">
|
||||||
|
<div
|
||||||
|
className="picker"
|
||||||
|
onClick={(e) =>
|
||||||
|
e.currentTarget.parentElement?.parentElement
|
||||||
|
?.querySelector("input")
|
||||||
|
?.click()
|
||||||
|
}>
|
||||||
|
<Pencil size={24} />
|
||||||
|
</div>
|
||||||
|
<InputBox
|
||||||
|
type="text"
|
||||||
|
className="text"
|
||||||
|
value={theme.getVariable(key)}
|
||||||
|
onChange={(el) =>
|
||||||
|
setVariable({
|
||||||
|
key,
|
||||||
|
value: el.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -5,11 +5,10 @@ import { Text } from "preact-i18n";
|
|||||||
|
|
||||||
import { Button } from "@revoltchat/ui";
|
import { Button } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../../mobx/State";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
import Tooltip from "../../../common/Tooltip";
|
||||||
import Tooltip from "../../common/Tooltip";
|
|
||||||
|
|
||||||
const Actions = styled.div`
|
const Actions = styled.div`
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -38,7 +37,6 @@ const Actions = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function ThemeTools() {
|
export default function ThemeTools() {
|
||||||
const { writeClipboard, openScreen } = useIntermediate();
|
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +54,9 @@ export default function ThemeTools() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div
|
<div
|
||||||
className="code"
|
className="code"
|
||||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
onClick={() =>
|
||||||
|
modalController.writeText(JSON.stringify(theme))
|
||||||
|
}>
|
||||||
<Tooltip content={<Text id="app.special.copy" />}>
|
<Tooltip content={<Text id="app.special.copy" />}>
|
||||||
{" "}
|
{" "}
|
||||||
{JSON.stringify(theme)}
|
{JSON.stringify(theme)}
|
||||||
@@ -72,16 +72,8 @@ export default function ThemeTools() {
|
|||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
theme.hydrate(JSON.parse(text));
|
theme.hydrate(JSON.parse(text));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "_input",
|
type: "import_theme",
|
||||||
question: (
|
|
||||||
<Text id="app.settings.pages.appearance.import_theme" />
|
|
||||||
),
|
|
||||||
field: (
|
|
||||||
<Text id="app.settings.pages.appearance.theme_data" />
|
|
||||||
),
|
|
||||||
callback: async (text) =>
|
|
||||||
theme.hydrate(JSON.parse(text)),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
71
src/components/settings/customisation/EmojiUploader.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Server } from "revolt.js";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Button, Column, Form, FormElement, Row } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
server: Server;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmojiUploader({ server }: Props) {
|
||||||
|
const [fileId, setFileId] = useState<string>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.server_pages.emojis.upload" />
|
||||||
|
</h3>
|
||||||
|
<Form
|
||||||
|
schema={{
|
||||||
|
name: "text",
|
||||||
|
file: "custom",
|
||||||
|
}}
|
||||||
|
data={{
|
||||||
|
name: {
|
||||||
|
field: "Name",
|
||||||
|
palette: "secondary",
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
element: (
|
||||||
|
<FileUploader
|
||||||
|
style="icon"
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
fileType="emojis"
|
||||||
|
behaviour="upload"
|
||||||
|
previewAfterUpload
|
||||||
|
maxFileSize={500000}
|
||||||
|
remove={async () => void setFileId("")}
|
||||||
|
onUpload={async (id) => void setFileId(id)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onSubmit={async ({ name }) => {
|
||||||
|
await server.client.api.put(`/custom/emoji/${fileId}`, {
|
||||||
|
name,
|
||||||
|
parent: { type: "Server", id: server._id },
|
||||||
|
});
|
||||||
|
|
||||||
|
setFileId("");
|
||||||
|
}}>
|
||||||
|
<Row>
|
||||||
|
<FormElement id="file" />
|
||||||
|
<Column>
|
||||||
|
<FormElement id="name" />
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
palette="secondary"
|
||||||
|
disabled={!fileId}>
|
||||||
|
<Text id="app.special.modals.actions.save" />
|
||||||
|
</Button>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { API, Channel, Member, Server } from "revolt.js";
|
import { API, Channel, Permission, Server } from "revolt.js";
|
||||||
import { Permission } from "revolt.js";
|
|
||||||
|
|
||||||
import { PermissionSelect } from "./PermissionSelect";
|
import { PermissionSelect } from "./PermissionSelect";
|
||||||
|
|
||||||
@@ -19,7 +18,6 @@ export function PermissionList({ value, onChange, filter, target }: Props) {
|
|||||||
(key) =>
|
(key) =>
|
||||||
![
|
![
|
||||||
"GrantAllSafe",
|
"GrantAllSafe",
|
||||||
"TimeoutMembers",
|
|
||||||
"ReadMessageHistory",
|
"ReadMessageHistory",
|
||||||
"Speak",
|
"Speak",
|
||||||
"Video",
|
"Video",
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const PermissionEntry = styled.label<{ disabled?: boolean }>`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 1.1em;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type Variables =
|
|||||||
| "tooltip"
|
| "tooltip"
|
||||||
| "status-online"
|
| "status-online"
|
||||||
| "status-away"
|
| "status-away"
|
||||||
|
| "status-focus"
|
||||||
| "status-busy"
|
| "status-busy"
|
||||||
| "status-streaming"
|
| "status-streaming"
|
||||||
| "status-invisible";
|
| "status-invisible";
|
||||||
@@ -283,6 +284,7 @@ export const PRESETS: Record<string, Theme> = {
|
|||||||
"tertiary-foreground": "#3a3a3a",
|
"tertiary-foreground": "#3a3a3a",
|
||||||
"status-online": "#3ABF7E",
|
"status-online": "#3ABF7E",
|
||||||
"status-away": "#F39F00",
|
"status-away": "#F39F00",
|
||||||
|
"status-focus": "#4799F0",
|
||||||
"status-busy": "#F84848",
|
"status-busy": "#F84848",
|
||||||
"status-streaming": "#977EFF",
|
"status-streaming": "#977EFF",
|
||||||
"status-invisible": "#A5A5A5",
|
"status-invisible": "#A5A5A5",
|
||||||
@@ -310,6 +312,7 @@ export const PRESETS: Record<string, Theme> = {
|
|||||||
"tertiary-foreground": "#848484",
|
"tertiary-foreground": "#848484",
|
||||||
"status-online": "#3ABF7E",
|
"status-online": "#3ABF7E",
|
||||||
"status-away": "#F39F00",
|
"status-away": "#F39F00",
|
||||||
|
"status-focus": "#4799F0",
|
||||||
"status-busy": "#F84848",
|
"status-busy": "#F84848",
|
||||||
"status-streaming": "#977EFF",
|
"status-streaming": "#977EFF",
|
||||||
"status-invisible": "#A5A5A5",
|
"status-invisible": "#A5A5A5",
|
||||||
@@ -337,8 +340,7 @@ export const generateVariables = (theme: Theme) => {
|
|||||||
const [r, g, b] = colour;
|
const [r, g, b] = colour;
|
||||||
return `--${key}: ${theme[key]}; --${key}-rgb: ${r}, ${g}, ${b};`;
|
return `--${key}: ${theme[key]}; --${key}-rgb: ${r}, ${g}, ${b};`;
|
||||||
}
|
}
|
||||||
return `--${key}: ${theme[key]};`;
|
return `--${key}: ${theme[key]};`;
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,14 @@ import { createBrowserHistory } from "history";
|
|||||||
export const history = createBrowserHistory({
|
export const history = createBrowserHistory({
|
||||||
basename: import.meta.env.BASE_URL,
|
basename: import.meta.env.BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const routeInformation = {
|
||||||
|
getServer: () =>
|
||||||
|
/server\/([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})/.exec(
|
||||||
|
history.location.pathname,
|
||||||
|
)?.[1],
|
||||||
|
getChannel: () =>
|
||||||
|
/channel\/([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})/.exec(
|
||||||
|
history.location.pathname,
|
||||||
|
)?.[1],
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,21 +6,19 @@ import { useEffect, useState } from "preact/hooks";
|
|||||||
|
|
||||||
import { Preloader, UIProvider } from "@revoltchat/ui";
|
import { Preloader, UIProvider } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { hydrateState } from "../mobx/State";
|
import { state } from "../mobx/State";
|
||||||
|
|
||||||
|
import Binder from "../controllers/client/jsx/Binder";
|
||||||
|
import ModalRenderer from "../controllers/modals/ModalRenderer";
|
||||||
import Locale from "./Locale";
|
import Locale from "./Locale";
|
||||||
import Theme from "./Theme";
|
import Theme from "./Theme";
|
||||||
import { history } from "./history";
|
import { history } from "./history";
|
||||||
import Intermediate from "./intermediate/Intermediate";
|
|
||||||
import ModalRenderer from "./modals/ModalRenderer";
|
|
||||||
import Client from "./revoltjs/RevoltClient";
|
|
||||||
import SyncManager from "./revoltjs/SyncManager";
|
|
||||||
|
|
||||||
const uiContext = {
|
const uiContext = {
|
||||||
Link,
|
Link,
|
||||||
Text: Text as any,
|
Text: Text as any,
|
||||||
Trigger: ContextMenuTrigger,
|
Trigger: ContextMenuTrigger,
|
||||||
emitAction: () => {},
|
emitAction: () => void {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +29,7 @@ export default function Context({ children }: { children: Children }) {
|
|||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hydrateState().then(() => setReady(true));
|
state.hydrate().then(() => setReady(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!ready) return <Preloader type="spinner" />;
|
if (!ready) return <Preloader type="spinner" />;
|
||||||
@@ -40,12 +38,8 @@ export default function Context({ children }: { children: Children }) {
|
|||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<UIProvider value={uiContext}>
|
<UIProvider value={uiContext}>
|
||||||
<Locale>
|
<Locale>
|
||||||
<Intermediate>
|
<>{children}</>
|
||||||
<Client>
|
<Binder />
|
||||||
{children}
|
|
||||||
<SyncManager />
|
|
||||||
</Client>
|
|
||||||
</Intermediate>
|
|
||||||
<ModalRenderer />
|
<ModalRenderer />
|
||||||
</Locale>
|
</Locale>
|
||||||
</UIProvider>
|
</UIProvider>
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
import { Prompt } from "react-router";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { API, Channel, Message, Server, User } from "revolt.js";
|
|
||||||
|
|
||||||
import { createContext } from "preact";
|
|
||||||
import {
|
|
||||||
StateUpdater,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "preact/hooks";
|
|
||||||
|
|
||||||
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
|
||||||
|
|
||||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
|
||||||
import { determineLink } from "../../lib/links";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
|
||||||
|
|
||||||
import { modalController } from "../modals";
|
|
||||||
import Modals from "./Modals";
|
|
||||||
|
|
||||||
export type Screen =
|
|
||||||
| { id: "none" }
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
| { id: "signed_out" }
|
|
||||||
| { id: "error"; error: string }
|
|
||||||
| { id: "clipboard"; text: string }
|
|
||||||
| { id: "token_reveal"; token: string; username: string }
|
|
||||||
| { id: "external_link_prompt"; link: string }
|
|
||||||
| { id: "sessions"; confirm: () => void }
|
|
||||||
| {
|
|
||||||
id: "_prompt";
|
|
||||||
question: Children;
|
|
||||||
content?: Children;
|
|
||||||
actions: Action[];
|
|
||||||
}
|
|
||||||
| ({ id: "special_prompt" } & (
|
|
||||||
| { type: "leave_group"; target: Channel }
|
|
||||||
| { type: "close_dm"; target: Channel }
|
|
||||||
| { type: "leave_server"; target: Server }
|
|
||||||
| { type: "delete_server"; target: Server }
|
|
||||||
| { type: "delete_channel"; target: Channel }
|
|
||||||
| {
|
|
||||||
type: "delete_bot";
|
|
||||||
target: string;
|
|
||||||
name: string;
|
|
||||||
cb?: () => void;
|
|
||||||
}
|
|
||||||
| { type: "delete_message"; target: Message }
|
|
||||||
| {
|
|
||||||
type: "create_invite";
|
|
||||||
target: Channel;
|
|
||||||
}
|
|
||||||
| { type: "kick_member"; target: Server; user: User }
|
|
||||||
| { type: "ban_member"; target: Server; user: User }
|
|
||||||
| { type: "unfriend_user"; target: User }
|
|
||||||
| { type: "block_user"; target: User }
|
|
||||||
| {
|
|
||||||
type: "create_channel";
|
|
||||||
target: Server;
|
|
||||||
cb?: (
|
|
||||||
channel: Channel & {
|
|
||||||
channel_type: "TextChannel" | "VoiceChannel";
|
|
||||||
},
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
| { type: "create_category"; target: Server }
|
|
||||||
))
|
|
||||||
| ({ id: "special_input" } & (
|
|
||||||
| {
|
|
||||||
type:
|
|
||||||
| "create_group"
|
|
||||||
| "create_server"
|
|
||||||
| "set_custom_status"
|
|
||||||
| "add_friend";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "create_role";
|
|
||||||
server: Server;
|
|
||||||
callback: (id: string) => void;
|
|
||||||
}
|
|
||||||
))
|
|
||||||
| {
|
|
||||||
id: "_input";
|
|
||||||
question: Children;
|
|
||||||
field: Children;
|
|
||||||
defaultValue?: string;
|
|
||||||
callback: (value: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: "onboarding";
|
|
||||||
callback: (
|
|
||||||
username: string,
|
|
||||||
loginAfterSuccess?: true,
|
|
||||||
) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop-overs
|
|
||||||
| { id: "profile"; user_id: string }
|
|
||||||
| {
|
|
||||||
id: "user_picker";
|
|
||||||
omit?: string[];
|
|
||||||
callback: (users: string[]) => Promise<void>;
|
|
||||||
}
|
|
||||||
| { id: "image_viewer"; attachment?: API.File; embed?: API.Image }
|
|
||||||
| { id: "channel_info"; channel: Channel }
|
|
||||||
| { id: "pending_requests"; users: User[] }
|
|
||||||
| { id: "modify_account"; field: "username" | "email" | "password" }
|
|
||||||
| { id: "create_bot"; onCreate: (bot: API.Bot) => void }
|
|
||||||
| {
|
|
||||||
id: "server_identity";
|
|
||||||
server: Server;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IntermediateContext = createContext({
|
|
||||||
screen: { id: "none" },
|
|
||||||
focusTaken: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const IntermediateActionsContext = createContext<{
|
|
||||||
openLink: (href?: string, trusted?: boolean) => boolean;
|
|
||||||
openScreen: (screen: Screen) => void;
|
|
||||||
writeClipboard: (text: string) => void;
|
|
||||||
}>({
|
|
||||||
openLink: null!,
|
|
||||||
openScreen: null!,
|
|
||||||
writeClipboard: null!,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: Children;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let __thisIsAHack: StateUpdater<Screen>;
|
|
||||||
|
|
||||||
export default function Intermediate(props: Props) {
|
|
||||||
const [screen, openScreen] = useState<Screen>({ id: "none" });
|
|
||||||
__thisIsAHack = openScreen;
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
screen,
|
|
||||||
focusTaken: screen.id !== "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = useMemo(() => {
|
|
||||||
return {
|
|
||||||
openLink: (href?: string, trusted?: boolean) => {
|
|
||||||
return modalController.openLink(href, trusted);
|
|
||||||
},
|
|
||||||
openScreen: (screen: Screen) => openScreen(screen),
|
|
||||||
writeClipboard: (a: string) => modalController.writeText(a),
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const openProfile = (user_id: string) =>
|
|
||||||
openScreen({ id: "profile", user_id });
|
|
||||||
const navigate = (path: string) => history.push(path);
|
|
||||||
|
|
||||||
const subs = [
|
|
||||||
internalSubscribe(
|
|
||||||
"Intermediate",
|
|
||||||
"openProfile",
|
|
||||||
openProfile as (...args: unknown[]) => void,
|
|
||||||
),
|
|
||||||
internalSubscribe(
|
|
||||||
"Intermediate",
|
|
||||||
"navigate",
|
|
||||||
navigate as (...args: unknown[]) => void,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return () => subs.map((unsub) => unsub());
|
|
||||||
}, [history]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntermediateContext.Provider value={value}>
|
|
||||||
<IntermediateActionsContext.Provider value={actions}>
|
|
||||||
{screen.id !== "onboarding" && props.children}
|
|
||||||
<Modals
|
|
||||||
{...value}
|
|
||||||
{...actions}
|
|
||||||
key={
|
|
||||||
screen.id
|
|
||||||
} /** By specifying a key, we reset state whenever switching screen. */
|
|
||||||
/>
|
|
||||||
<Prompt
|
|
||||||
when={[
|
|
||||||
"modify_account",
|
|
||||||
"special_prompt",
|
|
||||||
"special_input",
|
|
||||||
"image_viewer",
|
|
||||||
"profile",
|
|
||||||
"channel_info",
|
|
||||||
"pending_requests",
|
|
||||||
"user_picker",
|
|
||||||
].includes(screen.id)}
|
|
||||||
message={(_, action) => {
|
|
||||||
if (action === "POP") {
|
|
||||||
openScreen({ id: "none" });
|
|
||||||
setTimeout(() => history.push(history.location), 0);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IntermediateActionsContext.Provider>
|
|
||||||
</IntermediateContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useIntermediate = () => useContext(IntermediateActionsContext);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//import { isModalClosing } from "../../components/ui/Modal";
|
|
||||||
import { Screen } from "./Intermediate";
|
|
||||||
import { InputModal } from "./modals/Input";
|
|
||||||
import { OnboardingModal } from "./modals/Onboarding";
|
|
||||||
import { PromptModal } from "./modals/Prompt";
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
screen: Screen;
|
|
||||||
openScreen: (screen: Screen) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Modals({ screen, openScreen }: Props) {
|
|
||||||
const onClose = () =>
|
|
||||||
//isModalClosing || screen.id === "onboarding"
|
|
||||||
openScreen({ id: "none" });
|
|
||||||
// : internalEmit("Modal", "close");
|
|
||||||
|
|
||||||
switch (screen.id) {
|
|
||||||
case "_prompt":
|
|
||||||
return <PromptModal onClose={onClose} {...screen} />;
|
|
||||||
case "_input":
|
|
||||||
return <InputModal onClose={onClose} {...screen} />;
|
|
||||||
case "onboarding":
|
|
||||||
return <OnboardingModal onClose={onClose} {...screen} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
|
||||||
import { SpecialInputModal } from "./modals/Input";
|
|
||||||
import { SpecialPromptModal } from "./modals/Prompt";
|
|
||||||
import { ChannelInfo } from "./popovers/ChannelInfo";
|
|
||||||
import { CreateBotModal } from "./popovers/CreateBot";
|
|
||||||
import { ImageViewer } from "./popovers/ImageViewer";
|
|
||||||
import { UserPicker } from "./popovers/UserPicker";
|
|
||||||
import { UserProfile } from "./popovers/UserProfile";
|
|
||||||
|
|
||||||
export default function Popovers() {
|
|
||||||
const { screen } = useContext(IntermediateContext);
|
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
const onClose = () =>
|
|
||||||
//isModalClosing
|
|
||||||
openScreen({ id: "none" });
|
|
||||||
//: internalEmit("Modal", "close");
|
|
||||||
|
|
||||||
switch (screen.id) {
|
|
||||||
case "profile":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <UserProfile {...screen} onClose={onClose} />;
|
|
||||||
case "user_picker":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <UserPicker {...screen} onClose={onClose} />;
|
|
||||||
case "image_viewer":
|
|
||||||
return <ImageViewer {...screen} onClose={onClose} />;
|
|
||||||
case "channel_info":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <ChannelInfo {...screen} onClose={onClose} />;
|
|
||||||
case "create_bot":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <CreateBotModal onClose={onClose} {...screen} />;
|
|
||||||
case "special_prompt":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
|
||||||
case "special_input":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <SpecialInputModal onClose={onClose} {...screen} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import { useHistory } from "react-router";
|
|
||||||
import { Server } from "revolt.js";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
import { useContext, useState } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Category, InputBox, Modal } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import { I18nError } from "../../Locale";
|
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
question: Children;
|
|
||||||
field?: Children;
|
|
||||||
description?: Children;
|
|
||||||
defaultValue?: string;
|
|
||||||
callback: (value: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InputModal({
|
|
||||||
onClose,
|
|
||||||
question,
|
|
||||||
field,
|
|
||||||
description,
|
|
||||||
defaultValue,
|
|
||||||
callback,
|
|
||||||
}: Props) {
|
|
||||||
const [processing, setProcessing] = useState(false);
|
|
||||||
const [value, setValue] = useState(defaultValue ?? "");
|
|
||||||
const [error, setError] = useState<undefined | string>(undefined);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={question}
|
|
||||||
description={description}
|
|
||||||
disabled={processing}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
confirmation: true,
|
|
||||||
children: <Text id="app.special.modals.actions.ok" />,
|
|
||||||
onClick: () => {
|
|
||||||
setProcessing(true);
|
|
||||||
callback(value)
|
|
||||||
.then(onClose)
|
|
||||||
.catch((err) => {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: <Text id="app.special.modals.actions.cancel" />,
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClose={onClose}>
|
|
||||||
{field ? (
|
|
||||||
<Category>
|
|
||||||
<I18nError error={error}>{field}</I18nError>
|
|
||||||
</Category>
|
|
||||||
) : (
|
|
||||||
error && (
|
|
||||||
<Category>
|
|
||||||
<I18nError error={error} />
|
|
||||||
</Category>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<InputBox
|
|
||||||
value={value}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
onChange={(e) => setValue(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SpecialProps = { onClose: () => void } & (
|
|
||||||
| {
|
|
||||||
type:
|
|
||||||
| "create_group"
|
|
||||||
| "create_server"
|
|
||||||
| "set_custom_status"
|
|
||||||
| "add_friend";
|
|
||||||
}
|
|
||||||
| { type: "create_role"; server: Server; callback: (id: string) => void }
|
|
||||||
);
|
|
||||||
|
|
||||||
export function SpecialInputModal(props: SpecialProps) {
|
|
||||||
const history = useHistory();
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
|
|
||||||
const { onClose } = props;
|
|
||||||
switch (props.type) {
|
|
||||||
case "create_group": {
|
|
||||||
return (
|
|
||||||
<InputModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id="app.main.groups.create" />}
|
|
||||||
field={<Text id="app.main.groups.name" />}
|
|
||||||
callback={async (name) => {
|
|
||||||
const group = await client.channels.createGroup({
|
|
||||||
name,
|
|
||||||
users: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
history.push(`/channel/${group._id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "create_server": {
|
|
||||||
return (
|
|
||||||
<InputModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id="app.main.servers.create" />}
|
|
||||||
field={<Text id="app.main.servers.name" />}
|
|
||||||
description={
|
|
||||||
<div>
|
|
||||||
By creating this server, you agree to the{" "}
|
|
||||||
<a
|
|
||||||
href="https://revolt.chat/aup"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">
|
|
||||||
Acceptable Use Policy.
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
callback={async (name) => {
|
|
||||||
const server = await client.servers.createServer({
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
history.push(`/server/${server._id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "create_role": {
|
|
||||||
return (
|
|
||||||
<InputModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={
|
|
||||||
<Text id="app.settings.permissions.create_role" />
|
|
||||||
}
|
|
||||||
field={<Text id="app.settings.permissions.role_name" />}
|
|
||||||
callback={async (name) => {
|
|
||||||
const role = await props.server.createRole(name);
|
|
||||||
props.callback(role.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "set_custom_status": {
|
|
||||||
return (
|
|
||||||
<InputModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id="app.context_menu.set_custom_status" />}
|
|
||||||
field={<Text id="app.context_menu.custom_status" />}
|
|
||||||
defaultValue={client.user?.status?.text ?? undefined}
|
|
||||||
callback={(text) =>
|
|
||||||
client.users.edit({
|
|
||||||
status: {
|
|
||||||
...client.user?.status,
|
|
||||||
text: text.trim().length > 0 ? text : undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "add_friend": {
|
|
||||||
return (
|
|
||||||
<InputModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={"Add Friend"}
|
|
||||||
callback={(username) =>
|
|
||||||
client.api
|
|
||||||
.post(`/users/friend`, { username })
|
|
||||||
.then(undefined)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import styles from "./Onboarding.module.scss";
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Button, Preloader } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import wideSVG from "/assets/wide.svg";
|
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormInputs {
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OnboardingModal({ onClose, callback }: Props) {
|
|
||||||
const { handleSubmit, register } = useForm<FormInputs>();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
|
||||||
setLoading(true);
|
|
||||||
callback(username, true)
|
|
||||||
.then(() => onClose())
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
setError(takeError(err));
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.onboarding}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h1>
|
|
||||||
<Text id="app.special.modals.onboarding.welcome" />
|
|
||||||
<br />
|
|
||||||
<img src={wideSVG} loading="eager" />
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className={styles.form}>
|
|
||||||
{loading ? (
|
|
||||||
<Preloader type="spinner" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
<Text id="app.special.modals.onboarding.pick" />
|
|
||||||
</p>
|
|
||||||
<form
|
|
||||||
onSubmit={
|
|
||||||
handleSubmit(
|
|
||||||
onSubmit,
|
|
||||||
) as unknown as JSX.GenericEventHandler<HTMLFormElement>
|
|
||||||
}>
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
type="username"
|
|
||||||
register={register}
|
|
||||||
showOverline
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button type="submit">
|
|
||||||
<Text id="app.special.modals.actions.continue" />
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.invite {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding: 1em;
|
|
||||||
user-select: all;
|
|
||||||
font-size: 1.4em;
|
|
||||||
text-align: center;
|
|
||||||
font-family: var(--monospace-font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { Channel, Message as MessageI, Server, User } from "revolt.js";
|
|
||||||
import { ulid } from "ulid";
|
|
||||||
|
|
||||||
import styles from "./Prompt.module.scss";
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Category, Modal, InputBox, Radio } from "@revoltchat/ui";
|
|
||||||
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
|
||||||
|
|
||||||
import { TextReact } from "../../../lib/i18n";
|
|
||||||
|
|
||||||
import Message from "../../../components/common/messaging/Message";
|
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
|
||||||
import { I18nError } from "../../Locale";
|
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
|
||||||
import { useIntermediate } from "../Intermediate";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
question: Children;
|
|
||||||
content?: Children;
|
|
||||||
disabled?: boolean;
|
|
||||||
actions: Action[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PromptModal({
|
|
||||||
onClose,
|
|
||||||
question,
|
|
||||||
content,
|
|
||||||
actions,
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={question}
|
|
||||||
actions={actions}
|
|
||||||
onClose={onClose}
|
|
||||||
disabled={disabled}>
|
|
||||||
{error && (
|
|
||||||
<Category>
|
|
||||||
<I18nError error={error} />
|
|
||||||
</Category>
|
|
||||||
)}
|
|
||||||
{content}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SpecialProps = { onClose: () => void } & (
|
|
||||||
| { type: "leave_group"; target: Channel }
|
|
||||||
| { type: "close_dm"; target: Channel }
|
|
||||||
| { type: "leave_server"; target: Server }
|
|
||||||
| { type: "delete_server"; target: Server }
|
|
||||||
| { type: "delete_channel"; target: Channel }
|
|
||||||
| { type: "delete_bot"; target: string; name: string; cb?: () => void }
|
|
||||||
| { type: "delete_message"; target: MessageI }
|
|
||||||
| {
|
|
||||||
type: "create_invite";
|
|
||||||
target: Channel;
|
|
||||||
}
|
|
||||||
| { type: "kick_member"; target: Server; user: User }
|
|
||||||
| { type: "ban_member"; target: Server; user: User }
|
|
||||||
| { type: "unfriend_user"; target: User }
|
|
||||||
| { type: "block_user"; target: User }
|
|
||||||
| {
|
|
||||||
type: "create_channel";
|
|
||||||
target: Server;
|
|
||||||
cb?: (
|
|
||||||
channel: Channel & {
|
|
||||||
channel_type: "TextChannel" | "VoiceChannel";
|
|
||||||
},
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
| { type: "create_category"; target: Server }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SpecialPromptModal = observer((props: SpecialProps) => {
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const history = useHistory();
|
|
||||||
const [processing, setProcessing] = useState(false);
|
|
||||||
const [error, setError] = useState<undefined | string>(undefined);
|
|
||||||
|
|
||||||
const { onClose } = props;
|
|
||||||
switch (props.type) {
|
|
||||||
case "leave_group":
|
|
||||||
case "close_dm":
|
|
||||||
case "leave_server":
|
|
||||||
case "delete_server":
|
|
||||||
case "delete_channel":
|
|
||||||
case "delete_bot":
|
|
||||||
case "unfriend_user":
|
|
||||||
case "block_user": {
|
|
||||||
const EVENTS = {
|
|
||||||
close_dm: ["confirm_close_dm", "close"],
|
|
||||||
delete_server: ["confirm_delete", "delete"],
|
|
||||||
delete_channel: ["confirm_delete", "delete"],
|
|
||||||
delete_bot: ["confirm_delete", "delete"],
|
|
||||||
leave_group: ["confirm_leave", "leave"],
|
|
||||||
leave_server: ["confirm_leave", "leave"],
|
|
||||||
unfriend_user: ["unfriend_user", "remove"],
|
|
||||||
block_user: ["block_user", "block"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const event = EVENTS[props.type];
|
|
||||||
let name;
|
|
||||||
switch (props.type) {
|
|
||||||
case "unfriend_user":
|
|
||||||
case "block_user":
|
|
||||||
name = props.target.username;
|
|
||||||
break;
|
|
||||||
case "close_dm":
|
|
||||||
name = props.target.recipient?.username;
|
|
||||||
break;
|
|
||||||
case "delete_bot":
|
|
||||||
name = props.name;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
name = props.target.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={
|
|
||||||
<Text
|
|
||||||
id={`app.special.modals.prompt.${event[0]}`}
|
|
||||||
fields={{ name }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
confirmation: true,
|
|
||||||
palette: "error",
|
|
||||||
children: (
|
|
||||||
<Text
|
|
||||||
id={`app.special.modals.actions.${event[1]}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: async () => {
|
|
||||||
setProcessing(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (props.type) {
|
|
||||||
case "unfriend_user":
|
|
||||||
await props.target.removeFriend();
|
|
||||||
break;
|
|
||||||
case "block_user":
|
|
||||||
await props.target.blockUser();
|
|
||||||
break;
|
|
||||||
case "leave_group":
|
|
||||||
case "close_dm":
|
|
||||||
case "delete_channel":
|
|
||||||
case "leave_server":
|
|
||||||
case "delete_server":
|
|
||||||
if (props.type != "delete_channel")
|
|
||||||
history.push("/");
|
|
||||||
props.target.delete();
|
|
||||||
break;
|
|
||||||
case "delete_bot":
|
|
||||||
client.bots.delete(props.target);
|
|
||||||
props.cb?.();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.cancel" />
|
|
||||||
),
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
<TextReact
|
|
||||||
id={`app.special.modals.prompt.${event[0]}_long`}
|
|
||||||
fields={{ name: <b>{name}</b> }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "delete_message": {
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id={"app.context_menu.delete_message"} />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
confirmation: true,
|
|
||||||
palette: "error",
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.delete" />
|
|
||||||
),
|
|
||||||
onClick: async () => {
|
|
||||||
setProcessing(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
props.target.delete();
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.cancel" />
|
|
||||||
),
|
|
||||||
onClick: onClose,
|
|
||||||
palette: "plain",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<h5>
|
|
||||||
<Text
|
|
||||||
id={`app.special.modals.prompt.confirm_delete_message_long`}
|
|
||||||
/>
|
|
||||||
</h5>
|
|
||||||
<Message
|
|
||||||
message={props.target}
|
|
||||||
head={true}
|
|
||||||
contrast
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "create_invite": {
|
|
||||||
const [code, setCode] = useState("abcdef");
|
|
||||||
const { writeClipboard } = useIntermediate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setProcessing(true);
|
|
||||||
|
|
||||||
props.target
|
|
||||||
.createInvite()
|
|
||||||
.then(({ _id }) => setCode(_id))
|
|
||||||
.catch((err) => setError(takeError(err)))
|
|
||||||
.finally(() => setProcessing(false));
|
|
||||||
}, [props.target]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id={`app.context_menu.create_invite`} />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.ok" />
|
|
||||||
),
|
|
||||||
confirmation: true,
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: <Text id="app.context_menu.copy_link" />,
|
|
||||||
onClick: () =>
|
|
||||||
writeClipboard(
|
|
||||||
`${window.location.protocol}//${window.location.host}/invite/${code}`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
processing ? (
|
|
||||||
<Text id="app.special.modals.prompt.create_invite_generate" />
|
|
||||||
) : (
|
|
||||||
<div className={styles.invite}>
|
|
||||||
<Text id="app.special.modals.prompt.create_invite_created" />
|
|
||||||
<code>{code}</code>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "kick_member": {
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id={`app.context_menu.kick_member`} />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.kick" />
|
|
||||||
),
|
|
||||||
palette: "error",
|
|
||||||
confirmation: true,
|
|
||||||
onClick: async () => {
|
|
||||||
setProcessing(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
client.members
|
|
||||||
.getKey({
|
|
||||||
server: props.target._id,
|
|
||||||
user: props.user._id,
|
|
||||||
})
|
|
||||||
?.kick();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.cancel" />
|
|
||||||
),
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
<div className={styles.column}>
|
|
||||||
<UserIcon target={props.user} size={64} />
|
|
||||||
<Text
|
|
||||||
id="app.special.modals.prompt.confirm_kick"
|
|
||||||
fields={{ name: props.user?.username }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "ban_member": {
|
|
||||||
const [reason, setReason] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id={`app.context_menu.ban_member`} />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.ban" />
|
|
||||||
),
|
|
||||||
palette: "error",
|
|
||||||
|
|
||||||
confirmation: true,
|
|
||||||
onClick: async () => {
|
|
||||||
setProcessing(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await props.target.banUser(props.user._id, {
|
|
||||||
reason,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.cancel" />
|
|
||||||
),
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
<div className={styles.column}>
|
|
||||||
<UserIcon target={props.user} size={64} />
|
|
||||||
<Text
|
|
||||||
id="app.special.modals.prompt.confirm_ban"
|
|
||||||
fields={{ name: props.user?.username }}
|
|
||||||
/>
|
|
||||||
<Category>
|
|
||||||
<Text id="app.special.modals.prompt.confirm_ban_reason" />
|
|
||||||
</Category>
|
|
||||||
<InputBox
|
|
||||||
value={reason ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setReason(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "create_channel": {
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [type, setType] = useState<"Text" | "Voice">("Text");
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id="app.context_menu.create_channel" />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
confirmation: true,
|
|
||||||
palette: "secondary",
|
|
||||||
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.create" />
|
|
||||||
),
|
|
||||||
onClick: async () => {
|
|
||||||
setProcessing(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const channel =
|
|
||||||
await props.target.createChannel({
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (props.cb) {
|
|
||||||
props.cb(channel as any);
|
|
||||||
} else {
|
|
||||||
history.push(
|
|
||||||
`/server/${props.target._id}/channel/${channel._id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.cancel" />
|
|
||||||
),
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<Category>
|
|
||||||
<Text id="app.main.servers.channel_type" />
|
|
||||||
</Category>
|
|
||||||
<Radio
|
|
||||||
title={
|
|
||||||
<Text id="app.main.servers.text_channel" />
|
|
||||||
}
|
|
||||||
value={type === "Text"}
|
|
||||||
onSelect={() => setType("Text")}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
title={
|
|
||||||
<Text id="app.main.servers.voice_channel" />
|
|
||||||
}
|
|
||||||
value={type === "Voice"}
|
|
||||||
onSelect={() => setType("Voice")}
|
|
||||||
/>
|
|
||||||
<Category>
|
|
||||||
<Text id="app.main.servers.channel_name" />
|
|
||||||
</Category>
|
|
||||||
<InputBox
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "create_category": {
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PromptModal
|
|
||||||
onClose={onClose}
|
|
||||||
question={<Text id="app.context_menu.create_category" />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
confirmation: true,
|
|
||||||
palette: "secondary",
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.create" />
|
|
||||||
),
|
|
||||||
onClick: async () => {
|
|
||||||
setProcessing(true);
|
|
||||||
try {
|
|
||||||
props.target.edit({
|
|
||||||
categories: [
|
|
||||||
...(props.target.categories ?? []),
|
|
||||||
{
|
|
||||||
id: ulid(),
|
|
||||||
title: name,
|
|
||||||
channels: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
setProcessing(false);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
setProcessing(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: (
|
|
||||||
<Text id="app.special.modals.actions.cancel" />
|
|
||||||
),
|
|
||||||
onClick: onClose,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<Category>
|
|
||||||
<Text id="app.main.servers.category_name" />
|
|
||||||
</Category>
|
|
||||||
<InputBox
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
disabled={processing}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
.info {
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { X } from "@styled-icons/boxicons-regular";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Channel } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./ChannelInfo.module.scss";
|
|
||||||
|
|
||||||
import { Modal } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
|
||||||
import { getChannelName } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
channel: Channel;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChannelInfo = observer(({ channel, onClose }: Props) => {
|
|
||||||
if (
|
|
||||||
channel.channel_type === "DirectMessage" ||
|
|
||||||
channel.channel_type === "SavedMessages"
|
|
||||||
) {
|
|
||||||
onClose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal onClose={onClose}>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h1>{getChannelName(channel, true)}</h1>
|
|
||||||
<div onClick={onClose}>
|
|
||||||
<X size={36} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<Markdown content={channel.description!} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
|
||||||
import { API } from "revolt.js";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
|
||||||
import { useContext, useState } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Category, Modal } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
|
||||||
import { I18nError } from "../../Locale";
|
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
onCreate: (bot: API.Bot) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormInputs {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateBotModal({ onClose, onCreate }: Props) {
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<FormInputs> = async ({ name }) => {
|
|
||||||
try {
|
|
||||||
const { bot } = await client.bots.create({ name });
|
|
||||||
onCreate(bot);
|
|
||||||
onClose();
|
|
||||||
} catch (err) {
|
|
||||||
setError(takeError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
onClose={onClose}
|
|
||||||
title={<Text id="app.special.popovers.create_bot.title" />}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
confirmation: true,
|
|
||||||
palette: "accent",
|
|
||||||
onClick: async () => {
|
|
||||||
await handleSubmit(onSubmit)();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
children: <Text id="app.special.modals.actions.create" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
palette: "plain",
|
|
||||||
onClick: onClose,
|
|
||||||
children: <Text id="app.special.modals.actions.cancel" />,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{/* Preact / React typing incompatabilities */}
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit(
|
|
||||||
onSubmit,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
)(e as any);
|
|
||||||
}}>
|
|
||||||
<FormField
|
|
||||||
type="username"
|
|
||||||
name="name"
|
|
||||||
register={register}
|
|
||||||
showOverline
|
|
||||||
error={errors.name?.message}
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<Category>
|
|
||||||
<Text id="app.special.popovers.create_bot.failed" />{" "}
|
|
||||||
· <I18nError error={error} />
|
|
||||||
</Category>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.viewer {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
border-end-end-radius: 4px;
|
|
||||||
border-end-start-radius: 4px;
|
|
||||||
|
|
||||||
max-width: 100vw;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 75vh;
|
|
||||||
object-fit: contain;
|
|
||||||
border-bottom: thin solid var(--tertiary-foreground);
|
|
||||||
|
|
||||||
-webkit-touch-callout: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.list {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 360px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
import { useEffect } from "preact/hooks";
|
|
||||||
|
|
||||||
import { modalController } from ".";
|
|
||||||
|
|
||||||
export default observer(() => {
|
|
||||||
useEffect(() => {
|
|
||||||
function keyUp(event: KeyboardEvent) {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
modalController.pop("close");
|
|
||||||
} else if (event.key === "Enter") {
|
|
||||||
modalController.pop("confirm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keyup", keyUp);
|
|
||||||
return () => document.removeEventListener("keyup", keyUp);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return modalController.rendered;
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { API, Client, User, Member } from "revolt.js";
|
|
||||||
|
|
||||||
export type Modal = {
|
|
||||||
key?: string;
|
|
||||||
} & (
|
|
||||||
| ({
|
|
||||||
type: "mfa_flow";
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
state: "known";
|
|
||||||
client: Client;
|
|
||||||
callback: (ticket?: API.MFATicket) => void;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
state: "unknown";
|
|
||||||
available_methods: API.MFAMethod[];
|
|
||||||
callback: (response?: API.MFAResponse) => void;
|
|
||||||
}
|
|
||||||
))
|
|
||||||
| { type: "mfa_recovery"; codes: string[]; client: Client }
|
|
||||||
| {
|
|
||||||
type: "mfa_enable_totp";
|
|
||||||
identifier: string;
|
|
||||||
secret: string;
|
|
||||||
callback: (code?: string) => void;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "out_of_date";
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "changelog";
|
|
||||||
initial?: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "sign_out_sessions";
|
|
||||||
client: Client;
|
|
||||||
onDelete: () => void;
|
|
||||||
onDeleting: () => void;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "show_token";
|
|
||||||
name: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "error";
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "clipboard";
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "link_warning";
|
|
||||||
link: string;
|
|
||||||
callback: () => true;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "pending_friend_requests";
|
|
||||||
users: User[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "modify_account";
|
|
||||||
client: Client;
|
|
||||||
field: "username" | "email" | "password";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "server_identity";
|
|
||||||
member: Member;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "signed_out";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
|
||||||
onClose: () => void;
|
|
||||||
signal?: "close" | "confirm";
|
|
||||||
};
|
|
||||||