forked from abner/for-legacy-web
Compare commits
387 Commits
feat/dragg
...
handmade
| Author | SHA1 | Date |
|---|---|---|
|
|
1b2a980a47 | |
|
|
1ce522579e | |
|
|
41f47a1a3f | |
|
|
c6130a19bf | |
|
|
3c7cb2a222 | |
|
|
2625b37f70 | |
|
|
ca3613725a | |
|
|
707d0a2d7d | |
|
|
33e686ab1b | |
|
|
f22e6fa9fe | |
|
|
2e2886f01b | |
|
|
cbdeb1ba6d | |
|
|
608b0a3ef2 | |
|
|
59226959ba | |
|
|
9d82873d2a | |
|
|
56af4b9423 | |
|
|
cce94d2e0e | |
|
|
8ba7a769b3 | |
|
|
92e84309ec | |
|
|
dd5548c5e6 | |
|
|
e7d7420d5b | |
|
|
ee3b54b373 | |
|
|
3dc9f6b045 | |
|
|
2a1f7cb8bb | |
|
|
1136e8e31b | |
|
|
5337031359 | |
|
|
670c4e5f6e | |
|
|
20c8dde197 | |
|
|
ddaca7b0c4 | |
|
|
94fab0852f | |
|
|
2c41c5789c | |
|
|
184754ca0f | |
|
|
6bacbfbb56 | |
|
|
9e293e0a30 | |
|
|
d957fbd5dc | |
|
|
0a8b70c8ed | |
|
|
07d74a4ef4 | |
|
|
38ab8a7801 | |
|
|
bccf1eebae | |
|
|
135dbe2da4 | |
|
|
f95d3d27d8 | |
|
|
3d41a016cf | |
|
|
f9c2fcec56 | |
|
|
18243f0207 | |
|
|
7750276554 | |
|
|
97a6c7d399 | |
|
|
ac2beaf549 | |
|
|
bf951e59ee | |
|
|
03f9f7673d | |
|
|
478d375125 | |
|
|
cb6296a96e | |
|
|
48a9d7d370 | |
|
|
5de18192b2 | |
|
|
ef4218d12b | |
|
|
df4f6578f7 | |
|
|
28c897ac3d | |
|
|
86e8424e46 | |
|
|
5e66bc8dc4 | |
|
|
00e6ead9bd | |
|
|
0f0808aa56 | |
|
|
c972e6813f | |
|
|
61304f18c2 | |
|
|
2722d0a854 | |
|
|
dfd96c449a | |
|
|
9eca58dda1 | |
|
|
6cf0ef95ad | |
|
|
d3661170a4 | |
|
|
5b6546b761 | |
|
|
c25ecc12b2 | |
|
|
60fdb5c091 | |
|
|
472e6e07b5 | |
|
|
15e8e10151 | |
|
|
782a9b54ba | |
|
|
f52ede1c21 | |
|
|
7fef354fe6 | |
|
|
d4e0f19f95 | |
|
|
fe63c6633f | |
|
|
be3565871c | |
|
|
bc604c0157 | |
|
|
8c183b530b | |
|
|
9bbc22b091 | |
|
|
0d5ffb4df8 | |
|
|
9d83d7cc1c | |
|
|
534b9227de | |
|
|
5363ff2572 | |
|
|
3f1c2be709 | |
|
|
04027658e6 | |
|
|
f1594a7b36 | |
|
|
fa0fcedce5 | |
|
|
70f5e6fc7e | |
|
|
9b9ec867da | |
|
|
fead4ca879 | |
|
|
449eee006d | |
|
|
cf691a8462 | |
|
|
c5ca579c95 | |
|
|
636367faea | |
|
|
f20ada7c49 | |
|
|
75d07ffe0f | |
|
|
5b600bec20 | |
|
|
86218a7272 | |
|
|
9bc052ea87 | |
|
|
1862db89a3 | |
|
|
00708bb8f4 | |
|
|
7e87265ccb | |
|
|
d55d86aa3c | |
|
|
c426b6c184 | |
|
|
4d58ff06f3 | |
|
|
9f9902ffb1 | |
|
|
e8989fcffa | |
|
|
06dea9300c | |
|
|
c13896244c | |
|
|
e17e556ee6 | |
|
|
7441569cfa | |
|
|
eff1de7c78 | |
|
|
b733e13ec2 | |
|
|
e68ceac4f6 | |
|
|
3f536589e5 | |
|
|
21175dffda | |
|
|
5b31ce494e | |
|
|
056ec623cb | |
|
|
ff0330ec1b | |
|
|
58f35a2813 | |
|
|
a00d554f80 | |
|
|
7f911f5d2c | |
|
|
122f047c85 | |
|
|
7d544c82ab | |
|
|
46eec5a132 | |
|
|
de85527cd8 | |
|
|
6062a361f1 | |
|
|
95d4f61d7f | |
|
|
34ce1d1a86 | |
|
|
a9f23fe0e3 | |
|
|
6c08ccff52 | |
|
|
2f43a2c32d | |
|
|
e34c5c99fe | |
|
|
89b3c9c098 | |
|
|
dadf0b6329 | |
|
|
fcf6812151 | |
|
|
09be8c9e4f | |
|
|
6767ea1853 | |
|
|
9be4afe241 | |
|
|
7e89dcfb13 | |
|
|
3836156f3d | |
|
|
9f3e47d327 | |
|
|
e29678a6a3 | |
|
|
40d3356c1b | |
|
|
eb4670ec43 | |
|
|
df20ab7407 | |
|
|
22b2bc0a5e | |
|
|
2a12b79ef7 | |
|
|
9b6abe374a | |
|
|
b649b2a923 | |
|
|
b64c316dc9 | |
|
|
bfeefb4c73 | |
|
|
734fa06425 | |
|
|
5a738b7c68 | |
|
|
f03a88bd78 | |
|
|
e53059ee08 | |
|
|
099f7a3116 | |
|
|
d2264a2a43 | |
|
|
ef26f67c8e | |
|
|
5b7ec655eb | |
|
|
a381ba6320 | |
|
|
4f2fc267f9 | |
|
|
2687f98952 | |
|
|
0a27632f05 | |
|
|
1621e3b17d | |
|
|
6b9106c975 | |
|
|
e6ad6a552e | |
|
|
7ba2388de4 | |
|
|
47bfaad508 | |
|
|
1a9a2786bb | |
|
|
61a06c3f1a | |
|
|
b42c24295f | |
|
|
8fd6963f38 | |
|
|
71e689ece3 | |
|
|
7bc806ec63 | |
|
|
1016d80e56 | |
|
|
eab5ed033f | |
|
|
263d97853a | |
|
|
ff57dbddba | |
|
|
de207f0fa7 | |
|
|
1c69756383 | |
|
|
02eb7d83f6 | |
|
|
14037ef0c7 | |
|
|
2c50d9be6b | |
|
|
594ef00d09 | |
|
|
8608257066 | |
|
|
7626a1f461 | |
|
|
83ca6f489e | |
|
|
b7a10bb9ab | |
|
|
f6be6d7cf8 | |
|
|
dfbba41be4 | |
|
|
88bc8f93b6 | |
|
|
024fc6853b | |
|
|
5bd2d24c56 | |
|
|
5c50bed33d | |
|
|
07fd598bf9 | |
|
|
a234e3a582 | |
|
|
a77f2f9b4d | |
|
|
787d5840d2 | |
|
|
353507e17a | |
|
|
1b41ca03d9 | |
|
|
3ca2b12dfc | |
|
|
ff41219cf4 | |
|
|
7cf7402cea | |
|
|
c2a729a5e0 | |
|
|
e9977b2a76 | |
|
|
f76e53649b | |
|
|
a4b8fb5fc2 | |
|
|
c9264e1a49 | |
|
|
9a5653bc02 | |
|
|
50fd3b2d11 | |
|
|
58f294b790 | |
|
|
e1d3ad1675 | |
|
|
b2e0b5a035 | |
|
|
11a17feaae | |
|
|
ca69a0b4c5 | |
|
|
11e8cd652d | |
|
|
6e70b55d02 | |
|
|
dedc1e0666 | |
|
|
9a70bb0eff | |
|
|
791f4dfd89 | |
|
|
3fbf315587 | |
|
|
9807ef9c9a | |
|
|
96017c5f33 | |
|
|
084c90613f | |
|
|
dbe8a64ffc | |
|
|
176c7883c8 | |
|
|
73fd35bf46 | |
|
|
1a0b4b5703 | |
|
|
62a427b7a7 | |
|
|
64e7038532 | |
|
|
7e88e733d5 | |
|
|
70bda88383 | |
|
|
f3822b625d | |
|
|
df1b39256d | |
|
|
c9066aba2d | |
|
|
fc8cfa5419 | |
|
|
3204d176de | |
|
|
e3a526e2d7 | |
|
|
57887dc86f | |
|
|
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 | |
|
|
95ebd935ed | |
|
|
3b7c1cbe20 | |
|
|
cb0a521473 | |
|
|
aa9974149c | |
|
|
569864167e | |
|
|
f185dec461 | |
|
|
03e177f865 | |
|
|
f685352963 | |
|
|
6755217ad2 | |
|
|
211ff2058a | |
|
|
b7be9f8c03 | |
|
|
241b9cd27b | |
|
|
d10bd96900 | |
|
|
374be319c4 | |
|
|
0ee7b73d61 | |
|
|
63d5f6bb7d | |
|
|
34e6995d86 | |
|
|
a1ef1dce5e | |
|
|
a190a51d0b | |
|
|
c9127d6cf3 | |
|
|
f0d2e31b17 | |
|
|
220a28a151 | |
|
|
b44779c89a | |
|
|
5835064219 | |
|
|
b9da79bc11 | |
|
|
ba99cbaf2a | |
|
|
fc0c7611d4 | |
|
|
2e9c013ed8 | |
|
|
3e40a61624 | |
|
|
4e22ccb2f7 | |
|
|
00718245f9 | |
|
|
cd8ab6739b | |
|
|
5eabd2861f | |
|
|
a404ff7fe0 | |
|
|
7680931f5f | |
|
|
c1324108e3 | |
|
|
56770d40df | |
|
|
64f19ec2c0 | |
|
|
dbb1c1e8fa | |
|
|
c686e85d37 | |
|
|
8eefc87b05 | |
|
|
645e1af6db | |
|
|
8103cc03cf | |
|
|
bdc527ebbe | |
|
|
8a2826da91 | |
|
|
bd50378234 | |
|
|
71f8fc86a4 | |
|
|
277eaa685d | |
|
|
e81b8ed472 | |
|
|
6be0807433 | |
|
|
e0ca1681bd | |
|
|
ebcbe4bd4b | |
|
|
ec8b51f559 | |
|
|
72e60b7528 | |
|
|
3399991824 | |
|
|
6497e11fb0 | |
|
|
7544c78360 | |
|
|
040c4367f7 | |
|
|
ee80dfd3c8 | |
|
|
67c8418c31 | |
|
|
2056232759 | |
|
|
81bf325990 | |
|
|
906f15f103 | |
|
|
ea106a3902 | |
|
|
41e533ab59 | |
|
|
68b9d5ea79 | |
|
|
673efc0586 | |
|
|
74f8c552ed | |
|
|
f3bdbe52d9 | |
|
|
b4777e9816 | |
|
|
ab77d4a812 | |
|
|
1d243d4762 | |
|
|
2f9bfbf83f | |
|
|
c2547b3ead | |
|
|
a64fe61199 | |
|
|
1bd138d6ef | |
|
|
4bcfa601a5 | |
|
|
20d31babce | |
|
|
12b9716043 | |
|
|
e2d9e41a58 | |
|
|
bdf741e0ee | |
|
|
588cb7c019 | |
|
|
94dd4b464a | |
|
|
4541a34cef | |
|
|
83a3585940 |
5
.env
5
.env
|
|
@ -1,2 +1,5 @@
|
|||
VITE_API_URL=https://api.revolt.chat
|
||||
# VITE_API_URL=https://api.revolt.chat
|
||||
# VITE_API_URL=https://app.revolt.chat/api
|
||||
# VITE_API_URL=http://local.revolt.chat:8000
|
||||
VITE_API_URL=https://app.revolt.chat/api
|
||||
VITE_THEMES_URL=https://themes.revolt.chat
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## Please make sure to check the following tasks before opening and submitting a PR
|
||||
|
||||
* [ ] I understand and have followed the [contribution guide](https://github.com/revoltchat/revolt/discussions/282)
|
||||
* [ ] I understand and have followed the [contribution guide](https://developers.revolt.chat/contrib.html)
|
||||
* [ ] I have tested my changes locally and they are working as intended
|
||||
* [ ] These changes do not have any notable side effects on other Revolt projects
|
||||
* [ ] (optional) I have opened a pull request on [the translation repository](https://github.com/revoltchat/translations)
|
||||
|
|
|
|||
|
|
@ -3,109 +3,66 @@ name: Docker
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "handmade"
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- "!.github/workflows/preview_*.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- ".gitlab-ci.yml"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
- "*"
|
||||
# TODO: Bring back once gitea is updated past 1.21
|
||||
# paths-ignore:
|
||||
# - ".github/**"
|
||||
# - "!.github/workflows/docker.yml"
|
||||
# - ".vscode/**"
|
||||
# - ".gitignore"
|
||||
# - ".gitlab-ci.yml"
|
||||
# - "LICENSE"
|
||||
# - "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- "!.github/workflows/preview_*.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- ".gitlab-ci.yml"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
- "handmade"
|
||||
# TODO: Bring back once gitea is updated past 1.21
|
||||
# paths-ignore:
|
||||
# - ".github/**"
|
||||
# - "!.github/workflows/docker.yml"
|
||||
# - "!.github/workflows/preview_*.yml"
|
||||
# - ".vscode/**"
|
||||
# - ".gitignore"
|
||||
# - ".gitlab-ci.yml"
|
||||
# - "LICENSE"
|
||||
# - "README"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [linux/amd64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.architecture }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
|
||||
publish:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
# NOTE: Running on pull requests for now, but without pushing.
|
||||
# if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache amd64 Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/linux/amd64
|
||||
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: revoltchat/client, ghcr.io/revoltchat/client
|
||||
images: handmadecities/handmade-revolt-web-client
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
name: Mirroring
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "production"
|
||||
|
||||
jobs:
|
||||
to_gitlab:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url: git@gitlab.com:insert/revolt-vite.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
|
|
@ -15,3 +15,5 @@ public/assets_*
|
|||
!public/assets_default
|
||||
|
||||
.vscode/chrome_data
|
||||
|
||||
.direnv
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
image: node:16-buster
|
||||
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
# Fetch dependencies and setup project for compilation.
|
||||
install:
|
||||
stage: prepare
|
||||
script:
|
||||
- yarn
|
||||
|
||||
# Type check the project
|
||||
typecheck:
|
||||
stage: test
|
||||
needs:
|
||||
- install
|
||||
dependencies:
|
||||
- install
|
||||
script:
|
||||
- yarn typecheck
|
||||
|
||||
# Lint the project and check prettier output.
|
||||
lint:
|
||||
stage: test
|
||||
allow_failure: true
|
||||
needs:
|
||||
- install
|
||||
dependencies:
|
||||
- install
|
||||
script:
|
||||
- yarn lint
|
||||
- yarn --check 'src/**/*.{js,jsx,ts,tsx}'
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- test
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
[submodule "external/lang"]
|
||||
path = external/lang
|
||||
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
|
||||
|
|
|
|||
12
Dockerfile
12
Dockerfile
|
|
@ -1,17 +1,21 @@
|
|||
FROM node:16-buster AS builder
|
||||
# syntax=docker.io/docker/dockerfile:1.7-labs
|
||||
FROM --platform=$BUILDPLATFORM node:16-buster AS builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY . .
|
||||
COPY .env.build .env
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn typecheck
|
||||
RUN yarn build:deps
|
||||
# RUN yarn typecheck # lol no
|
||||
RUN yarn build:highmem
|
||||
RUN yarn workspaces focus --production --all
|
||||
|
||||
FROM node:16-alpine
|
||||
FROM node:24-alpine
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=builder /usr/src/app .
|
||||
COPY docker/package.json docker/yarn.lock .
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY --from=builder --exclude=package.json --exclude=yarn.lock --exclude=.yarn* --exclude=.git --exclude=external --exclude=node_modules /usr/src/app .
|
||||
|
||||
EXPOSE 5000
|
||||
CMD [ "yarn", "start:inject" ]
|
||||
|
|
|
|||
60
README.md
60
README.md
|
|
@ -1,9 +1,44 @@
|
|||
# Deprecation Notice
|
||||
|
||||
This project is deprecated, however it still may receive maintenance updates.
|
||||
|
||||
PRs for small fixes are more than welcome.
|
||||
|
||||
## Deploying a new release
|
||||
|
||||
Ensure `.env.local` points to `https://app.revolt.chat/api`.
|
||||
|
||||
```bash
|
||||
cd ~/deployments/revite
|
||||
git pull
|
||||
git submodule update
|
||||
|
||||
# check:
|
||||
git status
|
||||
|
||||
export REVOLT_SAAS_BRANCH=revite/main
|
||||
export REMOTE=root@production
|
||||
scripts/publish.sh
|
||||
|
||||
# SSH in and restart revite:
|
||||
ssh $REMOTE
|
||||
tmux a -t 4
|
||||
```
|
||||
|
||||
# Revite
|
||||
|
||||
## Description
|
||||
|
||||
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
|
||||
|
||||
## Pending Rewrite
|
||||
|
||||
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/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||
- `src/lib`: this needs to be organised
|
||||
|
||||
## Stack
|
||||
|
||||
- [Preact](https://preactjs.com/)
|
||||
|
|
@ -35,6 +70,7 @@ Get revite up and running locally.
|
|||
git clone --recursive https://github.com/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
yarn dev
|
||||
```
|
||||
|
||||
|
|
@ -42,17 +78,19 @@ You can now access the client at http://local.revolt.chat:3000.
|
|||
|
||||
## CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------- | -------------------------------------------- |
|
||||
| `yarn pull` | Setup assets required for Revite. |
|
||||
| `yarn dev` | Start the Revolt client in development mode. |
|
||||
| `yarn build` | Build the Revolt client. |
|
||||
| `yarn preview` | Start a local server with the built client. |
|
||||
| `yarn lint` | Run ESLint on the client. |
|
||||
| `yarn fmt` | Run Prettier on the client. |
|
||||
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
||||
| `yarn start` | Start a local sirv server with built client. |
|
||||
| `yarn start:inject` | Inject a given API URL and start server. |
|
||||
| Command | Description |
|
||||
| --------------------------------------- | -------------------------------------------- |
|
||||
| `yarn pull` | Setup assets required for Revite. |
|
||||
| `yarn dev` | Start the Revolt client in development mode. |
|
||||
| `yarn build` | Build the Revolt client. |
|
||||
| `yarn build:deps` | Build external dependencies. |
|
||||
| `yarn preview` | Start a local server with the built client. |
|
||||
| `yarn lint` | Run ESLint on the client. |
|
||||
| `yarn fmt` | Run Prettier on the client. |
|
||||
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
||||
| `yarn start` | Start a local sirv server with built client. |
|
||||
| `yarn start:inject` | Inject a given API URL and start server. |
|
||||
| `yarn lint \| egrep "no-literals" -B 1` | Scan for untranslated strings. |
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell rec {
|
||||
buildInputs = [
|
||||
pkgs.nodejs
|
||||
pkgs.nodejs.pkgs.yarn
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "inject-and-sirv",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"start:inject": "node scripts/inject.js && sirv dist_injected --port 5000 --cors --single --host"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"fs-extra": "^11.3.0",
|
||||
"klaw": "^4.1.0",
|
||||
"sirv-cli": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@polka/url@^1.0.0-next.24":
|
||||
version "1.0.0-next.29"
|
||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
|
||||
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
|
||||
|
||||
console-clear@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/console-clear/-/console-clear-1.1.1.tgz#995e20cbfbf14dd792b672cde387bd128d674bf7"
|
||||
integrity sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==
|
||||
|
||||
fs-extra@^11.3.0:
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
|
||||
integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
get-port@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
|
||||
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
klaw@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/klaw/-/klaw-4.1.0.tgz#5df608067d8cb62bbfb24374f8e5d956323338f3"
|
||||
integrity sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==
|
||||
|
||||
kleur@^4.1.4:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
|
||||
|
||||
local-access@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798"
|
||||
integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
|
||||
|
||||
mrmime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
|
||||
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
||||
|
||||
sade@^1.6.0:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
|
||||
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
|
||||
dependencies:
|
||||
mri "^1.1.0"
|
||||
|
||||
semiver@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/semiver/-/semiver-1.1.0.tgz#9c97fb02c21c7ce4fcf1b73e2c7a24324bdddd5f"
|
||||
integrity sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==
|
||||
|
||||
sirv-cli@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sirv-cli/-/sirv-cli-3.0.1.tgz#9a53e4fa85fdc08d54a76fd76a7c866cd4c3988b"
|
||||
integrity sha512-ICXaF2u6IQhLZ0EXF6nqUF4YODfSQSt+mGykt4qqO5rY+oIiwdg7B8w2PVDBJlQulaS2a3J8666CUoDoAuCGvg==
|
||||
dependencies:
|
||||
console-clear "^1.1.0"
|
||||
get-port "^5.1.1"
|
||||
kleur "^4.1.4"
|
||||
local-access "^1.0.1"
|
||||
sade "^1.6.0"
|
||||
semiver "^1.0.0"
|
||||
sirv "^3.0.0"
|
||||
tinydate "^1.0.0"
|
||||
|
||||
sirv@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.1.tgz#32a844794655b727f9e2867b777e0060fbe07bf3"
|
||||
integrity sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==
|
||||
dependencies:
|
||||
"@polka/url" "^1.0.0-next.24"
|
||||
mrmime "^2.0.0"
|
||||
totalist "^3.0.0"
|
||||
|
||||
tinydate@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tinydate/-/tinydate-1.3.0.tgz#e6ca8e5a22b51bb4ea1c3a2a4fd1352dbd4c57fb"
|
||||
integrity sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==
|
||||
|
||||
totalist@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
|
||||
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 4be02430c73bb4c69013a3b20b811c1391e1666d
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit e010e46ee9f226373a253c351b50ccdeea1c8b50
|
||||
Subproject commit 788261ef41e083ec917ee9bee37bc5b0e4f8d153
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit a45710f80cd7b4424a2114d2a32cbb83a4d95761
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": [
|
||||
"systems"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770169770,
|
||||
"narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "aa290c9891fa4ebe88f8889e59633d20cc06a5f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
description = "Node+yarn dev shell flake";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
systems.url = "github:nix-systems/default";
|
||||
flake-utils = {
|
||||
url = "github:numtide/flake-utils";
|
||||
inputs.systems.follows = "systems";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
nodejs_24
|
||||
yarn
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
78
package.json
78
package.json
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"dev": "node scripts/setup_assets.js --check && vite",
|
||||
"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",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
|
||||
|
|
@ -39,34 +40,21 @@
|
|||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"require-jsdoc": [
|
||||
"error",
|
||||
{
|
||||
"require": {
|
||||
"FunctionDeclaration": true,
|
||||
"MethodDefinition": true,
|
||||
"ClassDeclaration": true,
|
||||
"ArrowFunctionExpression": false,
|
||||
"FunctionExpression": false
|
||||
},
|
||||
"ignore": {
|
||||
"MethodDefinition": [
|
||||
"toJSON",
|
||||
"hydrate"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
"react/jsx-no-literals": "warn"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@revoltchat/rehype-katex": "6.0.3-patch.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"klaw": "^3.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"sirv-cli": "^1.0.14",
|
||||
"vite": "^2.6.14"
|
||||
"vite": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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/bitter": "^4.5.7",
|
||||
"@fontsource/comic-neue": "^4.4.5",
|
||||
|
|
@ -87,29 +75,30 @@
|
|||
"@fontsource/space-mono": "^4.4.5",
|
||||
"@fontsource/ubuntu": "^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",
|
||||
"@preact/preset-vite": "^2.0.0",
|
||||
"@revoltchat/ui": "1.0.33",
|
||||
"@revoltchat/ui": "^1.0.77",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@styled-icons/boxicons-logos": "^10.38.0",
|
||||
"@styled-icons/boxicons-regular": "^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",
|
||||
"@traptitech/markdown-it-katex": "^3.4.3",
|
||||
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
||||
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
||||
"@types/lodash": "^4",
|
||||
"@types/lodash.defaultsdeep": "^4.6.6",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/markdown-it": "^12.0.2",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/node": "^15.14.9",
|
||||
"@types/preact-i18n": "^2.3.0",
|
||||
"@types/prismjs": "^1.16.5",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react-beautiful-dnd": "^13",
|
||||
"@types/react-helmet": "^6.1.1",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-scroll": "^1.8.2",
|
||||
"@types/semver": "^7",
|
||||
"@types/styled-components": "^5.1.10",
|
||||
"@types/twemoji": "^12.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||
|
|
@ -121,22 +110,26 @@
|
|||
"detect-browser": "^5.2.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-preact": "^1.1.4",
|
||||
"eslint-plugin-jsdoc": "^39.3.2",
|
||||
"eslint-plugin-mobx": "^0.0.8",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"history": "4",
|
||||
"json-stringify-deterministic": "^1.0.2",
|
||||
"localforage": "^1.9.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.defaultsdeep": "^4.6.1",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"long": "^5.2.0",
|
||||
"markdown-it": "^12.0.6",
|
||||
"markdown-it-emoji": "^2.0.0",
|
||||
"mdast-util-to-hast": "^12.1.2",
|
||||
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
||||
"mobx": "^6.6.0",
|
||||
"mobx-react-lite": "3.4.0",
|
||||
"preact": "^10.5.14",
|
||||
"preact-context-menu": "0.4.0-patch.0",
|
||||
"preact-context-menu": "0.4.1",
|
||||
"preact-i18n": "^2.4.0-preactx",
|
||||
"prettier": "^2.3.1",
|
||||
"prismjs": "^1.23.0",
|
||||
"prismjs": "^1.28.0",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-device-detect": "2.2.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
|
|
@ -145,16 +138,29 @@
|
|||
"react-router-dom": "^5.2.0",
|
||||
"react-scroll": "^1.8.2",
|
||||
"react-virtuoso": "^2.12.0",
|
||||
"revolt.js": "6.0.1",
|
||||
"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",
|
||||
"sass": "^1.35.1",
|
||||
"semver": "^7.3.7",
|
||||
"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",
|
||||
"styled-components": "^5.3.0",
|
||||
"typescript": "^4.4.2",
|
||||
"ulid": "^2.3.0",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^4.1.0",
|
||||
"use-resize-observer": "^7.0.0",
|
||||
"vite-plugin-pwa": "^0.11.13",
|
||||
"vite-plugin-pwa": "^0.12.3",
|
||||
"workbox-precaching": "^6.1.5"
|
||||
},
|
||||
"name": "client",
|
||||
|
|
@ -162,5 +168,9 @@
|
|||
"repository": "https://github.com/revoltchat/revite.git",
|
||||
"author": "Paul <paulmakles@gmail.com>",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.2.0"
|
||||
"packageManager": "yarn@3.2.0",
|
||||
"resolutions": {
|
||||
"@revoltchat/ui": "portal:external/components",
|
||||
"revolt.js": "portal:external/revolt.js"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 828 B |
|
|
@ -2,21 +2,26 @@
|
|||
# Build and publish release to production server
|
||||
|
||||
# Remote Server
|
||||
REMOTE=revolt-de-nrb-1
|
||||
if [ -z "$REMOTE" ]; then
|
||||
echo "Please set REMOTE!"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Remote Directory
|
||||
REMOTE_DIR=/root/revite
|
||||
REMOTE_DIR=/root/deployments/revite
|
||||
|
||||
# Post-install script
|
||||
POST_INSTALL="pm2 restart revite"
|
||||
POST_INSTALL=""
|
||||
|
||||
# Assets
|
||||
export REVOLT_SAAS=https://github.com/revoltchat/assets
|
||||
|
||||
|
||||
# Exit when any command fails
|
||||
set -e
|
||||
|
||||
# 1. Build Revite
|
||||
yarn
|
||||
yarn build
|
||||
yarn build:highmem
|
||||
|
||||
# 2. Archive built files
|
||||
tar -czvf build.tar.gz dist
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import Lottie, { LottieRefCurrentProps } from "lottie-react";
|
||||
|
||||
import { JSX } from "preact";
|
||||
|
||||
import usernameAnim from "../controllers/modals/components/legacy/usernameUpdateLottie.json";
|
||||
|
||||
type Element =
|
||||
| string
|
||||
| {
|
||||
type: "image";
|
||||
src: string;
|
||||
shadow?: boolean;
|
||||
}
|
||||
| { type: "element"; element: JSX.Element };
|
||||
|
||||
export interface ChangelogPost {
|
||||
date: Date;
|
||||
title: string;
|
||||
content: Element[];
|
||||
}
|
||||
|
||||
export const changelogEntries: Record<number, ChangelogPost> = {
|
||||
1: {
|
||||
date: new Date("2022-06-12T20:39:16.674Z"),
|
||||
title: "Secure your account with 2FA",
|
||||
content: [
|
||||
"Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.",
|
||||
{
|
||||
type: "image",
|
||||
src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png",
|
||||
},
|
||||
"Once enabled, you will be prompted on login.",
|
||||
{
|
||||
type: "image",
|
||||
src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png",
|
||||
},
|
||||
"Other authentication methods coming later, stay tuned!",
|
||||
],
|
||||
},
|
||||
2: {
|
||||
date: new Date("2023-02-23T20:00:00.000Z"),
|
||||
title: "In-App Reporting Is Here",
|
||||
content: [
|
||||
"You can now report any user, server, or message directly from the app.",
|
||||
{
|
||||
type: "image",
|
||||
src: "https://autumn.revolt.chat/attachments/ZuDVIjGiCl61Pk9XGk5qfc8-idN9EnFAk55DUQp713/the.png",
|
||||
shadow: true,
|
||||
},
|
||||
"If you want to learn more about how we're making Revolt safer for you, check out our new blog post :point_right: [https://revolt.chat/posts/improving-user-safety](https://revolt.chat/posts/improving-user-safety)",
|
||||
],
|
||||
},
|
||||
3: {
|
||||
date: new Date("2023-06-11T15:00:00.000Z"),
|
||||
title: "Usernames are Changing",
|
||||
content: [
|
||||
{
|
||||
type: "element",
|
||||
element: (
|
||||
<Lottie
|
||||
animationData={usernameAnim}
|
||||
style={{
|
||||
background: "var(--secondary-background)",
|
||||
borderRadius: "6px",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
"Revolt has undergone a significant change to its username system, transitioning from unique username handles to a new system of display names and usernames with four-digit number tags called discriminators. The four-digit number tags serve as identifiers to differentiate users with the same username, allowing individuals to select desired usernames that reflect their identity.",
|
||||
{
|
||||
type: "element",
|
||||
element: (
|
||||
<a href="https://revolt.chat/posts/evolving-usernames">
|
||||
Read more on our blog!
|
||||
</a>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const changelogEntryArray = Object.keys(changelogEntries).map(
|
||||
(index) => changelogEntries[index as unknown as number],
|
||||
);
|
||||
|
||||
export const latestChangelog = changelogEntryArray.length;
|
||||
|
|
@ -151,7 +151,6 @@ export const emojiDictionary = {
|
|||
hole: "🕳️",
|
||||
bomb: "💣",
|
||||
speech_balloon: "💬",
|
||||
eye_speech_bubble: "👁️🗨️",
|
||||
left_speech_bubble: "🗨️",
|
||||
right_anger_bubble: "🗯️",
|
||||
thought_balloon: "💭",
|
||||
|
|
@ -1849,108 +1848,109 @@ export const emojiDictionary = {
|
|||
england: "🏴",
|
||||
scotland: "🏴",
|
||||
wales: "🏴",
|
||||
...{
|
||||
"1984": "custom:1984.gif",
|
||||
KekW: "custom:KekW.png",
|
||||
amogus: "custom:amogus.gif",
|
||||
awaa: "custom:awaa.png",
|
||||
boohoo: "custom:boohoo.png",
|
||||
boohoo_goes_hard: "custom:boohoo_goes_hard.png",
|
||||
boohoo_shaken: "custom:boohoo_shaken.png",
|
||||
cat_arrival: "custom:cat_arrival.gif",
|
||||
cat_awson: "custom:cat_awson.png",
|
||||
cat_blob: "custom:cat_blob.png",
|
||||
cat_bonk: "custom:cat_bonk.png",
|
||||
cat_concern: "custom:cat_concern.png",
|
||||
cat_fast: "custom:cat_fast.gif",
|
||||
cat_kitty: "custom:cat_kitty.png",
|
||||
cat_lick: "custom:cat_lick.gif",
|
||||
cat_not_like: "custom:cat_not_like.png",
|
||||
cat_put: "custom:cat_put.gif",
|
||||
cat_pwease: "custom:cat_pwease.png",
|
||||
cat_rage: "custom:cat_rage.png",
|
||||
cat_sad: "custom:cat_sad.png",
|
||||
cat_snuff: "custom:cat_snuff.gif",
|
||||
cat_spin: "custom:cat_spin.gif",
|
||||
cat_squish: "custom:cat_squish.gif",
|
||||
cat_stare: "custom:cat_stare.gif",
|
||||
cat_steal: "custom:cat_steal.gif",
|
||||
cat_sussy: "custom:cat_sussy.gif",
|
||||
clueless: "custom:clueless.png",
|
||||
death: "custom:death.gif",
|
||||
developers: "custom:developers.gif",
|
||||
fastwawa: "custom:fastwawa.gif",
|
||||
ferris: "custom:ferris.png",
|
||||
ferris_bongo: "custom:ferris_bongo.gif",
|
||||
ferris_nom: "custom:ferris_nom.png",
|
||||
ferris_pensive: "custom:ferris_pensive.png",
|
||||
ferris_unsafe: "custom:ferris_unsafe.png",
|
||||
flesh: "custom:flesh.png",
|
||||
flooshed: "custom:flooshed.png",
|
||||
flosh: "custom:flosh.png",
|
||||
flushee: "custom:flushee.png",
|
||||
forgor: "custom:forgor.png",
|
||||
hollow: "custom:hollow.png",
|
||||
john: "custom:john.png",
|
||||
lightspeed: "custom:lightspeed.png",
|
||||
little_guy: "custom:little_guy.png",
|
||||
lmaoooo: "custom:lmaoooo.gif",
|
||||
lol: "custom:lol.png",
|
||||
looking: "custom:looking.gif",
|
||||
marie: "custom:marie.png",
|
||||
marie_furret: "custom:marie_furret.gif",
|
||||
marie_smug: "custom:marie_smug.png",
|
||||
megumin: "custom:megumin.png",
|
||||
michi_above: "custom:michi_above.png",
|
||||
michi_awww: "custom:michi_awww.gif",
|
||||
michi_drag: "custom:michi_drag.gif",
|
||||
michi_flustered: "custom:michi_flustered.png",
|
||||
michi_glare: "custom:michi_glare.png",
|
||||
michi_sus: "custom:michi_sus.png",
|
||||
monkaS: "custom:monkaS.png",
|
||||
monkaStare: "custom:monkaStare.png",
|
||||
monkey_grr: "custom:monkey_grr.png",
|
||||
monkey_pensive: "custom:monkey_pensive.png",
|
||||
monkey_zany: "custom:monkey_zany.png",
|
||||
nazu_sit: "custom:nazu_sit.png",
|
||||
nazu_sus: "custom:nazu_sus.png",
|
||||
ok_and: "custom:ok_and.gif",
|
||||
owo: "custom:owo.png",
|
||||
pat: "custom:pat.png",
|
||||
pointThink: "custom:pointThink.png",
|
||||
rainbowHype: "custom:rainbowHype.gif",
|
||||
rawr: "custom:rawr.png",
|
||||
rember: "custom:rember.png",
|
||||
revolt: "custom:revolt.png",
|
||||
sickly: "custom:sickly.png",
|
||||
stare: "custom:stare.png",
|
||||
tfyoulookingat: "custom:tfyoulookingat.png",
|
||||
thanks: "custom:thanks.png",
|
||||
thonk: "custom:thonk.png",
|
||||
trol: "custom:trol.png",
|
||||
troll_smile: "custom:troll_smile.gif",
|
||||
uber: "custom:uber.png",
|
||||
ubertroll: "custom:ubertroll.png",
|
||||
verycool: "custom:verycool.png",
|
||||
verygood: "custom:verygood.png",
|
||||
wawafast: "custom:wawafast.gif",
|
||||
wawastance: "custom:wawastance.png",
|
||||
yeahokayyy: "custom:yeahokayyy.png",
|
||||
yed: "custom:yed.png",
|
||||
yems: "custom:yems.png",
|
||||
michael: "custom:michael.gif",
|
||||
charle: "custom:charle.gif",
|
||||
sadge: "custom:sadge.webp",
|
||||
sus: "custom:sus.webp",
|
||||
chade: "custom:chade.gif",
|
||||
gigachad: "custom:gigachad.webp",
|
||||
sippy: "custom:sippy.webp",
|
||||
ayame_heart: "custom:ayame_heart.png",
|
||||
catgirl_peek: "custom:catgirl_peek.png",
|
||||
girl_happy: "custom:girl_happy.png",
|
||||
hug_plushie: "custom:hug_plushie.png",
|
||||
huggies: "custom:huggies.png",
|
||||
noted: "custom:noted.gif",
|
||||
waving: "custom:waving.png",
|
||||
},
|
||||
// ...{
|
||||
// 1984: "custom:1984.gif",
|
||||
// KekW: "custom:KekW.png",
|
||||
// amogus: "custom:amogus.gif",
|
||||
// awaa: "custom:awaa.png",
|
||||
// boohoo: "custom:boohoo.png",
|
||||
// boohoo_goes_hard: "custom:boohoo_goes_hard.png",
|
||||
// boohoo_shaken: "custom:boohoo_shaken.png",
|
||||
// cat_arrival: "custom:cat_arrival.gif",
|
||||
// cat_awson: "custom:cat_awson.png",
|
||||
// cat_blob: "custom:cat_blob.png",
|
||||
// cat_bonk: "custom:cat_bonk.png",
|
||||
// cat_concern: "custom:cat_concern.png",
|
||||
// cat_fast: "custom:cat_fast.gif",
|
||||
// cat_kitty: "custom:cat_kitty.png",
|
||||
// cat_lick: "custom:cat_lick.gif",
|
||||
// cat_not_like: "custom:cat_not_like.png",
|
||||
// cat_put: "custom:cat_put.gif",
|
||||
// cat_pwease: "custom:cat_pwease.png",
|
||||
// cat_rage: "custom:cat_rage.png",
|
||||
// cat_sad: "custom:cat_sad.png",
|
||||
// cat_snuff: "custom:cat_snuff.gif",
|
||||
// cat_spin: "custom:cat_spin.gif",
|
||||
// cat_squish: "custom:cat_squish.gif",
|
||||
// cat_stare: "custom:cat_stare.gif",
|
||||
// cat_steal: "custom:cat_steal.gif",
|
||||
// cat_sussy: "custom:cat_sussy.gif",
|
||||
// clueless: "custom:clueless.png",
|
||||
// death: "custom:death.gif",
|
||||
// developers: "custom:developers.gif",
|
||||
// fastwawa: "custom:fastwawa.gif",
|
||||
// ferris: "custom:ferris.png",
|
||||
// ferris_bongo: "custom:ferris_bongo.gif",
|
||||
// ferris_nom: "custom:ferris_nom.png",
|
||||
// ferris_pensive: "custom:ferris_pensive.png",
|
||||
// ferris_unsafe: "custom:ferris_unsafe.png",
|
||||
// flesh: "custom:flesh.png",
|
||||
// flooshed: "custom:flooshed.png",
|
||||
// flosh: "custom:flosh.png",
|
||||
// flushee: "custom:flushee.png",
|
||||
// forgor: "custom:forgor.png",
|
||||
// hollow: "custom:hollow.png",
|
||||
// john: "custom:john.png",
|
||||
// lightspeed: "custom:lightspeed.png",
|
||||
// little_guy: "custom:little_guy.png",
|
||||
// lmaoooo: "custom:lmaoooo.gif",
|
||||
// lol: "custom:lol.png",
|
||||
// looking: "custom:looking.gif",
|
||||
// marie: "custom:marie.png",
|
||||
// marie_furret: "custom:marie_furret.gif",
|
||||
// marie_smug: "custom:marie_smug.png",
|
||||
// megumin: "custom:megumin.png",
|
||||
// michi_above: "custom:michi_above.png",
|
||||
// michi_awww: "custom:michi_awww.gif",
|
||||
// michi_drag: "custom:michi_drag.gif",
|
||||
// michi_flustered: "custom:michi_flustered.png",
|
||||
// michi_glare: "custom:michi_glare.png",
|
||||
// michi_sus: "custom:michi_sus.png",
|
||||
// monkaS: "custom:monkaS.png",
|
||||
// monkaStare: "custom:monkaStare.png",
|
||||
// monkey_grr: "custom:monkey_grr.png",
|
||||
// monkey_pensive: "custom:monkey_pensive.png",
|
||||
// monkey_zany: "custom:monkey_zany.png",
|
||||
// nazu_sit: "custom:nazu_sit.png",
|
||||
// nazu_sus: "custom:nazu_sus.png",
|
||||
// ok_and: "custom:ok_and.gif",
|
||||
// owo: "custom:owo.png",
|
||||
// pat: "custom:pat.png",
|
||||
// pointThink: "custom:pointThink.png",
|
||||
// rainbowHype: "custom:rainbowHype.gif",
|
||||
// rawr: "custom:rawr.png",
|
||||
// rember: "custom:rember.png",
|
||||
// revolt: "custom:revolt.png",
|
||||
// sickly: "custom:sickly.png",
|
||||
// stare: "custom:stare.png",
|
||||
// tfyoulookingat: "custom:tfyoulookingat.png",
|
||||
// thanks: "custom:thanks.png",
|
||||
// thonk: "custom:thonk.png",
|
||||
// trol: "custom:trol.png",
|
||||
// troll_smile: "custom:troll_smile.gif",
|
||||
// uber: "custom:uber.png",
|
||||
// ubertroll: "custom:ubertroll.png",
|
||||
// verycool: "custom:verycool.png",
|
||||
// verygood: "custom:verygood.png",
|
||||
// wawafast: "custom:wawafast.gif",
|
||||
// wawastance: "custom:wawastance.png",
|
||||
// yeahokayyy: "custom:yeahokayyy.png",
|
||||
// yed: "custom:yed.png",
|
||||
// yems: "custom:yems.png",
|
||||
// michael: "custom:michael.gif",
|
||||
// charle: "custom:charle.gif",
|
||||
// sadge: "custom:sadge.webp",
|
||||
// sus: "custom:sus.webp",
|
||||
// chade: "custom:chade.gif",
|
||||
// gigachad: "custom:gigachad.webp",
|
||||
// sippy: "custom:sippy.webp",
|
||||
// ayame_heart: "custom:ayame_heart.png",
|
||||
// catgirl_peek: "custom:catgirl_peek.png",
|
||||
// girl_happy: "custom:girl_happy.png",
|
||||
// hug_plushie: "custom:hug_plushie.png",
|
||||
// huggies: "custom:huggies.png",
|
||||
// noted: "custom:noted.gif",
|
||||
// waving: "custom:waving.png",
|
||||
// mogusvented: "custom:mogusvented.png",
|
||||
// },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
The following folders should not be added to or modified:
|
||||
|
||||
- `common`
|
||||
- `markdown`
|
||||
- `native`
|
||||
- `ui`
|
||||
|
||||
The following are part-legacy, will remain in place and will be rewritten to some degree still:
|
||||
|
||||
- `navigation`
|
||||
|
||||
The following are mostly good to go:
|
||||
|
||||
- `settings`
|
||||
|
|
@ -4,17 +4,13 @@ import { Channel } from "revolt.js";
|
|||
import styled from "styled-components/macro";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button } from "@revoltchat/ui";
|
||||
import { Button, Checkbox, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SECTION_NSFW } from "../../mobx/stores/Layout";
|
||||
|
||||
import Checkbox from "../ui/Checkbox";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
const Base = styled.div`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
|
@ -49,14 +45,36 @@ type Props = {
|
|||
channel: Channel;
|
||||
};
|
||||
|
||||
let geoBlock:
|
||||
| undefined
|
||||
| {
|
||||
countryCode: string;
|
||||
isAgeRestrictedGeo: true;
|
||||
};
|
||||
|
||||
export default observer((props: Props) => {
|
||||
const history = useHistory();
|
||||
const layout = useApplicationState().layout;
|
||||
const [geoLoaded, setGeoLoaded] = useState(typeof geoBlock !== "undefined");
|
||||
const [ageGate, setAgeGate] = useState(false);
|
||||
|
||||
if (ageGate || !props.gated) {
|
||||
useEffect(() => {
|
||||
if (!geoLoaded) {
|
||||
fetch("https://geo.revolt.chat")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
geoBlock = data;
|
||||
setGeoLoaded(true);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!geoBlock) return <Preloader type="spinner" />;
|
||||
|
||||
if ((ageGate && !geoBlock.isAgeRestrictedGeo) || !props.gated) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
props.channel.channel_type === "Group" ||
|
||||
|
|
@ -80,23 +98,40 @@ export default observer((props: Props) => {
|
|||
</a>
|
||||
</span>
|
||||
|
||||
<Checkbox
|
||||
checked={layout.getSectionState(SECTION_NSFW, false)}
|
||||
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}>
|
||||
<Text id="app.main.channel.nsfw.confirm" />
|
||||
</Checkbox>
|
||||
<div className="actions">
|
||||
<Button palette="secondary" onClick={() => history.goBack()}>
|
||||
<Text id="app.special.modals.actions.back" />
|
||||
</Button>
|
||||
<Button
|
||||
palette="secondary"
|
||||
onClick={() =>
|
||||
layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
|
||||
}>
|
||||
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
|
||||
</Button>
|
||||
</div>
|
||||
{geoBlock.isAgeRestrictedGeo ? (
|
||||
<div style={{ maxWidth: "420px", textAlign: "center" }}>
|
||||
{geoBlock.countryCode === "GB"
|
||||
? "This channel is not available in your region while we review options on legal compliance."
|
||||
: "This content is not available in your region."}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Checkbox
|
||||
title={<Text id="app.main.channel.nsfw.confirm" />}
|
||||
value={layout.getSectionState(SECTION_NSFW, false)}
|
||||
onChange={() =>
|
||||
layout.toggleSectionState(SECTION_NSFW, false)
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<Button
|
||||
palette="secondary"
|
||||
onClick={() => history.goBack()}>
|
||||
<Text id="app.special.modals.actions.back" />
|
||||
</Button>
|
||||
<Button
|
||||
palette="secondary"
|
||||
onClick={() =>
|
||||
layout.getSectionState(SECTION_NSFW) &&
|
||||
setAgeGate(true)
|
||||
}>
|
||||
<Text
|
||||
id={`app.main.channel.nsfw.${props.type}.confirm`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Base>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Link } from "react-router-dom";
|
||||
import { Channel, User } from "revolt.js";
|
||||
import { Emoji as CustomEmoji } from "revolt.js/esm/maps/Emojis";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { StateUpdater, useState } from "preact/hooks";
|
||||
|
||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { emojiDictionary } from "../../assets/emojis";
|
||||
import { useClient } from "../../controllers/client/ClientController";
|
||||
import ChannelIcon from "./ChannelIcon";
|
||||
import Emoji from "./Emoji";
|
||||
import ServerIcon from "./ServerIcon";
|
||||
import Tooltip from "./Tooltip";
|
||||
import UserIcon from "./user/UserIcon";
|
||||
|
||||
export type AutoCompleteState =
|
||||
|
|
@ -15,7 +19,7 @@ export type AutoCompleteState =
|
|||
| ({ selected: number; within: boolean } & (
|
||||
| {
|
||||
type: "emoji";
|
||||
matches: string[];
|
||||
matches: (string | CustomEmoji)[];
|
||||
}
|
||||
| {
|
||||
type: "user";
|
||||
|
|
@ -60,7 +64,7 @@ export function useAutoComplete(
|
|||
const cursor = el.selectionStart;
|
||||
const content = el.value.slice(0, cursor);
|
||||
|
||||
const valid = /\w/;
|
||||
const valid = /[\w\-]/;
|
||||
|
||||
let j = content.length - 1;
|
||||
if (content[j] === "@") {
|
||||
|
|
@ -88,7 +92,7 @@ export function useAutoComplete(
|
|||
? "emoji"
|
||||
: "user",
|
||||
search.toLowerCase(),
|
||||
j + 1,
|
||||
current === ":" ? j + 1 : j,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -105,16 +109,23 @@ export function useAutoComplete(
|
|||
|
||||
if (type === "emoji") {
|
||||
// ! TODO: we should convert it to a Binary Search Tree and use that
|
||||
const matches = Object.keys(emojiDictionary)
|
||||
.filter((emoji: string) => emoji.match(regex))
|
||||
.splice(0, 5);
|
||||
const matches = [
|
||||
...Object.keys(emojiDictionary).filter((emoji: string) =>
|
||||
emoji.match(regex),
|
||||
),
|
||||
...Array.from(client.emojis.values()).filter((emoji) =>
|
||||
emoji.name.match(regex),
|
||||
),
|
||||
].splice(0, 5);
|
||||
|
||||
if (matches.length > 0) {
|
||||
const currentPosition =
|
||||
state.type !== "none" ? state.selected : 0;
|
||||
|
||||
setState({
|
||||
// @ts-ignore-next-line are you high
|
||||
type: "emoji",
|
||||
// @ts-ignore-next-line
|
||||
matches,
|
||||
selected: Math.min(currentPosition, matches.length - 1),
|
||||
within: false,
|
||||
|
|
@ -234,15 +245,18 @@ export function useAutoComplete(
|
|||
|
||||
const content = el.value.split("");
|
||||
if (state.type === "emoji") {
|
||||
const selected = state.matches[state.selected];
|
||||
content.splice(
|
||||
index,
|
||||
search.length,
|
||||
state.matches[state.selected],
|
||||
selected instanceof CustomEmoji
|
||||
? selected._id
|
||||
: selected,
|
||||
": ",
|
||||
);
|
||||
} else if (state.type === "user") {
|
||||
content.splice(
|
||||
index - 1,
|
||||
index,
|
||||
search.length + 1,
|
||||
"<@",
|
||||
state.matches[state.selected]._id,
|
||||
|
|
@ -250,7 +264,7 @@ export function useAutoComplete(
|
|||
);
|
||||
} else {
|
||||
content.splice(
|
||||
index - 1,
|
||||
index,
|
||||
search.length + 1,
|
||||
"<#",
|
||||
state.matches[state.selected]._id,
|
||||
|
|
@ -389,12 +403,17 @@ export default function AutoComplete({
|
|||
setState,
|
||||
onClick,
|
||||
}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
|
||||
const client = useClient();
|
||||
return (
|
||||
<Base detached={detached}>
|
||||
<div>
|
||||
{state.type === "emoji" &&
|
||||
state.matches.map((match, i) => (
|
||||
<button
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
key={match}
|
||||
className={i === state.selected ? "active" : ""}
|
||||
onMouseEnter={() =>
|
||||
|
|
@ -413,15 +432,61 @@ export default function AutoComplete({
|
|||
})
|
||||
}
|
||||
onClick={onClick}>
|
||||
<Emoji
|
||||
emoji={
|
||||
(emojiDictionary as Record<string, string>)[
|
||||
match
|
||||
]
|
||||
}
|
||||
size={20}
|
||||
/>
|
||||
:{match}:
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
{match instanceof CustomEmoji ? (
|
||||
<img
|
||||
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>
|
||||
))}
|
||||
{state.type === "user" &&
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { Channel } from "revolt.js";
|
||||
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import fallback from "./assets/group.png";
|
||||
|
||||
import { useClient } from "../../controllers/client/ClientController";
|
||||
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
||||
|
||||
interface Props extends IconBaseProps<Channel> {
|
||||
|
|
@ -22,7 +19,7 @@ export default observer(
|
|||
keyof Props | "children" | "as"
|
||||
>,
|
||||
) => {
|
||||
const client = useContext(AppContext);
|
||||
const client = useClient();
|
||||
|
||||
const {
|
||||
size,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||
|
||||
import { Details } from "@revoltchat/ui";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import Details from "../ui/Details";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
defaultValue: boolean;
|
||||
|
|
@ -34,7 +32,7 @@ export default function CollapsibleSection({
|
|||
}
|
||||
{...detailsProps}>
|
||||
<summary>
|
||||
<div class="padding">
|
||||
<div className="padding">
|
||||
<ChevronDown size={20} />
|
||||
{summary}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { emojiDictionary } from "../../assets/emojis";
|
||||
|
||||
export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji";
|
||||
|
||||
let EMOJI_PACK: EmojiPack = "mutant";
|
||||
|
|
@ -40,12 +42,12 @@ function toCodePoint(rune: string) {
|
|||
.join("-");
|
||||
}
|
||||
|
||||
function parseEmoji(emoji: string) {
|
||||
if (emoji.startsWith("custom:")) {
|
||||
return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
|
||||
7,
|
||||
)}`;
|
||||
}
|
||||
export function parseEmoji(emoji: string) {
|
||||
// if (emoji.startsWith("custom:")) {
|
||||
// return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
|
||||
// 7,
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
const codepoint = toCodePoint(emoji);
|
||||
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useApplicationState } from "../../mobx/State";
|
||||
import { ComboBox } from "@revoltchat/ui";
|
||||
|
||||
import ComboBox from "../ui/ComboBox";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { Language, Languages } from "../../../external/lang/Languages";
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import styled, { css } from "styled-components/macro";
|
|||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import IconButton from "../ui/IconButton";
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { modalController } from "../../controllers/modals/ModalController";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -60,6 +61,9 @@ const ServerBanner = styled.div<Omit<Props, "server">>`
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
|
||||
cursor: pointer;
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -121,7 +125,13 @@ export default observer(({ server }: Props) => {
|
|||
</svg>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
<div className="title">{server.name}</div>
|
||||
<a
|
||||
className="title"
|
||||
onClick={() =>
|
||||
modalController.push({ type: "server_info", server })
|
||||
}>
|
||||
{server.name}
|
||||
</a>
|
||||
{server.havePermission("ManageServer") && (
|
||||
<Link to={`/server/${server._id}/settings`}>
|
||||
<IconButton>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import styled from "styled-components/macro";
|
|||
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { useClient } from "../../controllers/client/ClientController";
|
||||
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
||||
|
||||
interface Props extends IconBaseProps<Server> {
|
||||
|
|
@ -34,7 +33,7 @@ export default observer(
|
|||
keyof Props | "children" | "as"
|
||||
>,
|
||||
) => {
|
||||
const client = useContext(AppContext);
|
||||
const client = useClient();
|
||||
|
||||
const { target, attachment, size, animate, server_name, ...imgProps } =
|
||||
props;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import styled from "styled-components/macro";
|
|||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
type Props = Omit<TippyProps, "children"> & {
|
||||
children: Children;
|
||||
content: Children;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
|
|||
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import IconButton from "../ui/IconButton";
|
||||
|
||||
import { updateSW } from "../../main";
|
||||
import { updateSW } from "../../updateWorker";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
let pendingUpdate = false;
|
||||
|
|
@ -31,7 +31,7 @@ export default function UpdateIndicator({ style }: Props) {
|
|||
|
||||
if (style === "titlebar") {
|
||||
return (
|
||||
<div class="actions">
|
||||
<div className="actions">
|
||||
<Tooltip
|
||||
content="A new update is available!"
|
||||
placement="bottom">
|
||||
|
|
@ -46,7 +46,7 @@ export default function UpdateIndicator({ style }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (window.isNative) return null;
|
||||
if (window.isNative && window.native.getConfig().frame) return null;
|
||||
|
||||
return (
|
||||
<IconButton onClick={() => updateSW(true)}>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,16 @@ import { useTriggerEvents } from "preact-context-menu";
|
|||
import { memo } from "preact/compat";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Category } from "@revoltchat/ui";
|
||||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
import { I18nError } from "../../../context/Locale";
|
||||
|
||||
import Overline from "../../ui/Overline";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import Markdown from "../../markdown/Markdown";
|
||||
import UserIcon from "../user/UserIcon";
|
||||
import { Username } from "../user/UserShort";
|
||||
|
|
@ -26,6 +25,7 @@ import MessageBase, {
|
|||
} from "./MessageBase";
|
||||
import Attachment from "./attachments/Attachment";
|
||||
import { MessageReply } from "./attachments/MessageReply";
|
||||
import { Reactions } from "./attachments/Reactions";
|
||||
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
|
||||
import Embed from "./embed/Embed";
|
||||
import InviteList from "./embed/EmbedInvite";
|
||||
|
|
@ -33,7 +33,7 @@ import InviteList from "./embed/EmbedInvite";
|
|||
interface Props {
|
||||
attachContext?: boolean;
|
||||
queued?: QueuedMessage;
|
||||
message: MessageObject;
|
||||
message: MessageObject & { webhook: { name: string; avatar?: string } };
|
||||
highlight?: boolean;
|
||||
contrast?: boolean;
|
||||
content?: Children;
|
||||
|
|
@ -52,11 +52,9 @@ const Message = observer(
|
|||
queued,
|
||||
hideReply,
|
||||
}: Props) => {
|
||||
const client = useClient();
|
||||
const client = message.client;
|
||||
const user = message.author;
|
||||
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const content = message.content;
|
||||
const head =
|
||||
preferHead || (message.reply_ids && message.reply_ids.length > 0);
|
||||
|
|
@ -65,12 +63,16 @@ const Message = observer(
|
|||
? useTriggerEvents("Menu", {
|
||||
user: message.author_id,
|
||||
contextualChannel: message.channel_id,
|
||||
contextualMessage: message._id,
|
||||
// eslint-disable-next-line
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const openProfile = () =>
|
||||
openScreen({ id: "profile", user_id: message.author_id });
|
||||
modalController.push({
|
||||
type: "user_profile",
|
||||
user_id: message.author_id,
|
||||
});
|
||||
|
||||
const handleUserClick = (e: MouseEvent) => {
|
||||
if (e.shiftKey && user?._id) {
|
||||
|
|
@ -87,6 +89,7 @@ const Message = observer(
|
|||
|
||||
// ! FIXME(?): animate on hover
|
||||
const [mouseHovering, setAnimate] = useState(false);
|
||||
const [reactionsOpen, setReactionsOpen] = useState(false);
|
||||
useEffect(() => setAnimate(false), [replacement]);
|
||||
|
||||
return (
|
||||
|
|
@ -115,7 +118,11 @@ const Message = observer(
|
|||
}
|
||||
contrast={contrast}
|
||||
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"}
|
||||
{...(attachContext
|
||||
? useTriggerEvents("Menu", {
|
||||
|
|
@ -131,6 +138,11 @@ const Message = observer(
|
|||
<UserIcon
|
||||
className="avatar"
|
||||
url={message.generateMasqAvatarURL()}
|
||||
override={
|
||||
message.webhook?.avatar
|
||||
? `https://autumn.revolt.chat/avatars/${message.webhook.avatar}`
|
||||
: undefined
|
||||
}
|
||||
target={user}
|
||||
size={36}
|
||||
onClick={handleUserClick}
|
||||
|
|
@ -151,6 +163,7 @@ const Message = observer(
|
|||
showServerIdentity
|
||||
onClick={handleUserClick}
|
||||
masquerade={message.masquerade!}
|
||||
override={message.webhook?.name}
|
||||
{...userContext}
|
||||
/>
|
||||
<MessageDetail
|
||||
|
|
@ -159,10 +172,13 @@ const Message = observer(
|
|||
/>
|
||||
</span>
|
||||
)}
|
||||
{replacement ?? <Markdown content={content} />}
|
||||
{replacement ??
|
||||
(content && <Markdown content={content} />)}
|
||||
{!queued && <InviteList message={message} />}
|
||||
{queued?.error && (
|
||||
<Overline type="error" error={queued.error} />
|
||||
<Category>
|
||||
<I18nError error={queued.error} />
|
||||
</Category>
|
||||
)}
|
||||
{message.attachments?.map((attachment, index) => (
|
||||
<Attachment
|
||||
|
|
@ -177,10 +193,13 @@ const Message = observer(
|
|||
{message.embeds?.map((embed, index) => (
|
||||
<Embed key={index} embed={embed} />
|
||||
))}
|
||||
{mouseHovering &&
|
||||
<Reactions message={message} />
|
||||
{(mouseHovering || reactionsOpen) &&
|
||||
!replacement &&
|
||||
!isTouchscreenDevice && (
|
||||
<MessageOverlayBar
|
||||
reactionsOpen={reactionsOpen}
|
||||
setReactionsOpen={setReactionsOpen}
|
||||
message={message}
|
||||
queued={queued}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||
import { HappyBeaming, Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||
import Axios, { CancelTokenSource } from "axios";
|
||||
import Long from "long";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import {
|
||||
Channel,
|
||||
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
Permission,
|
||||
Server,
|
||||
U32_MAX,
|
||||
UserPermission,
|
||||
} from "revolt.js";
|
||||
import { Channel } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
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, Picker } from "@revoltchat/ui";
|
||||
|
||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { debounce } from "../../../lib/debounce";
|
||||
|
|
@ -28,20 +22,25 @@ import {
|
|||
SMOOTH_SCROLL_ON_RECEIVE,
|
||||
} from "../../../lib/renderer/Singleton";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { state, useApplicationState } from "../../../mobx/State";
|
||||
import { DraftObject } from "../../../mobx/stores/Draft";
|
||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
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 {
|
||||
FileUploader,
|
||||
grabFiles,
|
||||
uploadFile,
|
||||
} from "../../../context/revoltjs/FileUploads";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../context/revoltjs/util";
|
||||
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
} from "../../../controllers/client/jsx/legacy/FileUploads";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import { RenderEmoji } from "../../markdown/plugins/emoji";
|
||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||
import { PermissionTooltip } from "../Tooltip";
|
||||
import FilePreview from "./bars/FilePreview";
|
||||
|
|
@ -104,7 +103,7 @@ const Blocked = styled.div`
|
|||
`;
|
||||
|
||||
const Action = styled.div`
|
||||
> div {
|
||||
> a {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
|
|
@ -127,7 +126,7 @@ const Action = styled.div`
|
|||
`;
|
||||
|
||||
const FileAction = styled.div`
|
||||
> div {
|
||||
> a {
|
||||
height: 48px;
|
||||
width: 62px;
|
||||
display: flex;
|
||||
|
|
@ -136,6 +135,10 @@ const FileAction = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const FloatingLayer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
|
||||
width: 16px;
|
||||
`;
|
||||
|
|
@ -146,6 +149,67 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
|
|||
// Tests for code block delimiters (``` at start of line)
|
||||
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
|
||||
export const CAN_UPLOAD_AT_ONCE = 5;
|
||||
|
||||
|
|
@ -157,12 +221,42 @@ export default observer(({ channel }: Props) => {
|
|||
});
|
||||
const [typing, setTyping] = useState<boolean | number>(false);
|
||||
const [replies, setReplies] = useState<Reply[]>([]);
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const [picker, setPicker] = useState(false);
|
||||
const client = useClient();
|
||||
const translate = useTranslation();
|
||||
|
||||
const closePicker = useCallback(() => setPicker(false), []);
|
||||
|
||||
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")) {
|
||||
return (
|
||||
<Base>
|
||||
|
|
@ -184,7 +278,12 @@ export default observer(({ channel }: Props) => {
|
|||
|
||||
// Push message content to draft.
|
||||
const setMessage = useCallback(
|
||||
(content?: string) => state.draft.set(channel._id, content),
|
||||
(content?: string) => {
|
||||
const dobj: DraftObject = {
|
||||
content,
|
||||
};
|
||||
state.draft.set(channel._id, dobj);
|
||||
},
|
||||
[state.draft, channel._id],
|
||||
);
|
||||
|
||||
|
|
@ -206,7 +305,7 @@ export default observer(({ channel }: Props) => {
|
|||
if (!state.draft.has(channel._id)) {
|
||||
setMessage(text);
|
||||
} else {
|
||||
setMessage(`${state.draft.get(channel._id)}\n${text}`);
|
||||
setMessage(`${state.draft.get(channel._id)?.content}\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,8 +323,8 @@ export default observer(({ channel }: Props) => {
|
|||
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
||||
return;
|
||||
|
||||
const content = state.draft.get(channel._id)?.trim() ?? "";
|
||||
if (uploadState.type === "attached") return sendFile(content);
|
||||
const content = state.draft.get(channel._id)?.content?.trim() ?? "";
|
||||
if (uploadState.type !== "none") return sendFile(content);
|
||||
if (content.length === 0) return;
|
||||
|
||||
internalEmit("NewMessages", "hide");
|
||||
|
|
@ -307,8 +406,11 @@ export default observer(({ channel }: Props) => {
|
|||
* @returns
|
||||
*/
|
||||
async function sendFile(content: string) {
|
||||
if (uploadState.type !== "attached") return;
|
||||
if (uploadState.type !== "attached" && uploadState.type !== "failed")
|
||||
return;
|
||||
|
||||
const attachments: string[] = [];
|
||||
setMessage;
|
||||
|
||||
const cancel = Axios.CancelToken.source();
|
||||
const files = uploadState.files;
|
||||
|
|
@ -432,7 +534,7 @@ export default observer(({ channel }: Props) => {
|
|||
}
|
||||
|
||||
function isInCodeBlock(cursor: number): boolean {
|
||||
const content = state.draft.get(channel._id) || "";
|
||||
const content = state.draft.get(channel._id)?.content || "";
|
||||
const contentBeforeCursor = content.substring(0, cursor);
|
||||
|
||||
let delimiterCount = 0;
|
||||
|
|
@ -482,7 +584,10 @@ export default observer(({ channel }: Props) => {
|
|||
files: [...uploadState.files, ...files],
|
||||
}),
|
||||
() =>
|
||||
openScreen({ id: "error", error: "FileTooLarge" }),
|
||||
modalController.push({
|
||||
type: "error",
|
||||
error: "FileTooLarge",
|
||||
}),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
|
@ -505,6 +610,22 @@ export default observer(({ channel }: Props) => {
|
|||
replies={replies}
|
||||
setReplies={setReplies}
|
||||
/>
|
||||
<FloatingLayer>
|
||||
{picker && (
|
||||
<HackAlertThisFileWillBeReplaced
|
||||
onSelect={(emoji) => {
|
||||
const v = state.draft.get(channel._id);
|
||||
const cnt: DraftObject = {
|
||||
content:
|
||||
(v?.content ? `${v.content} ` : "") +
|
||||
`:${emoji}:`,
|
||||
};
|
||||
state.draft.set(channel._id, cnt);
|
||||
}}
|
||||
onClose={closePicker}
|
||||
/>
|
||||
)}
|
||||
</FloatingLayer>
|
||||
<Base>
|
||||
{channel.havePermission("UploadFiles") ? (
|
||||
<FileAction>
|
||||
|
|
@ -553,7 +674,7 @@ export default observer(({ channel }: Props) => {
|
|||
id="message"
|
||||
maxLength={2000}
|
||||
onKeyUp={onKeyUp}
|
||||
value={state.draft.get(channel._id) ?? ""}
|
||||
value={state.draft.get(channel._id)?.content ?? ""}
|
||||
padding="var(--message-box-padding)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.ctrlKey && e.key === "Enter") {
|
||||
|
|
@ -625,16 +746,11 @@ export default observer(({ channel }: Props) => {
|
|||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{/*<Action>
|
||||
<IconButton>
|
||||
<Box size={24} />
|
||||
</IconButton>
|
||||
</Action>
|
||||
<Action>
|
||||
<IconButton>
|
||||
<IconButton onClick={() => setPicker(!picker)}>
|
||||
<HappyBeaming size={24} />
|
||||
</IconButton>
|
||||
</Action>*/}
|
||||
</Action>
|
||||
<Action>
|
||||
<IconButton
|
||||
className={
|
||||
|
|
|
|||
|
|
@ -9,15 +9,26 @@ import {
|
|||
EditAlt,
|
||||
Edit,
|
||||
MessageSquareEdit,
|
||||
Key,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Message, API } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Row } from "@revoltchat/ui";
|
||||
|
||||
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 MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
|
||||
|
||||
|
|
@ -67,12 +78,17 @@ const iconDictionary = {
|
|||
channel_renamed: EditAlt,
|
||||
channel_description_changed: Edit,
|
||||
channel_icon_changed: MessageSquareEdit,
|
||||
channel_ownership_changed: Key,
|
||||
text: InfoCircle,
|
||||
};
|
||||
|
||||
export const SystemMessage = observer(
|
||||
({ attachContext, message, highlight, hideInfo }: Props) => {
|
||||
const data = message.asSystemMessage;
|
||||
if (!data) return null;
|
||||
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
const SystemMessageIcon =
|
||||
iconDictionary[data.type as API.SystemMessage["type"]] ??
|
||||
InfoCircle;
|
||||
|
|
@ -98,16 +114,39 @@ export const SystemMessage = observer(
|
|||
case "user_joined":
|
||||
case "user_left":
|
||||
case "user_kicked":
|
||||
case "user_banned":
|
||||
case "user_banned": {
|
||||
const createdAt = data.user ? decodeTime(data.user._id) : null;
|
||||
children = (
|
||||
<TextReact
|
||||
id={`app.main.channel.system.${data.type}`}
|
||||
fields={{
|
||||
user: <UserShort user={data.user} />,
|
||||
}}
|
||||
/>
|
||||
<Row centred>
|
||||
<TextReact
|
||||
id={`app.main.channel.system.${data.type}`}
|
||||
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;
|
||||
}
|
||||
case "channel_renamed":
|
||||
children = (
|
||||
<TextReact
|
||||
|
|
@ -130,6 +169,22 @@ export const SystemMessage = observer(
|
|||
/>
|
||||
);
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import { API } from "revolt.js";
|
|||
import styles from "./Attachment.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
import AttachmentActions from "./AttachmentActions";
|
||||
import { SizedGrid } from "./Grid";
|
||||
import ImageFile from "./ImageFile";
|
||||
|
|
@ -21,7 +20,7 @@ interface Props {
|
|||
const MAX_ATTACHMENT_WIDTH = 480;
|
||||
|
||||
export default function Attachment({ attachment, hasContent }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useClient();
|
||||
const { filename, metadata } = attachment;
|
||||
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
||||
|
||||
|
|
|
|||
|
|
@ -11,23 +11,23 @@ import styles from "./AttachmentActions.module.scss";
|
|||
import classNames from "classnames";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { determineFileSize } from "../../../../lib/fileSize";
|
||||
|
||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
|
||||
interface Props {
|
||||
attachment: API.File;
|
||||
}
|
||||
|
||||
export default function AttachmentActions({ attachment }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useClient();
|
||||
const { filename, metadata, size } = attachment;
|
||||
|
||||
const url = client.generateFileURL(attachment);
|
||||
const open_url = `${url}/${filename}`;
|
||||
const download_url = url?.replace("attachments", "attachments/download");
|
||||
const open_url = url;
|
||||
const download_url = `${url}/${filename}`;
|
||||
|
||||
const filesize = determineFileSize(size);
|
||||
|
||||
|
|
@ -49,10 +49,11 @@ export default function AttachmentActions({ attachment }: Props) {
|
|||
</IconButton>
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||
// target={isFirefox || window.native ? "_blank" : "_self"}
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import styled from "styled-components/macro";
|
|||
|
||||
import { Ref } from "preact";
|
||||
|
||||
import { Children } from "../../../../types/Preact";
|
||||
|
||||
const Grid = styled.div<{ width: number; height: number }>`
|
||||
--width: ${(props) => props.width}px;
|
||||
--height: ${(props) => props.height}px;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { API } from "revolt.js";
|
|||
|
||||
import styles from "./Attachment.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||
|
||||
enum ImageLoadingState {
|
||||
Loading,
|
||||
|
|
@ -19,8 +19,7 @@ type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
|||
|
||||
export default function ImageFile({ attachment, ...props }: Props) {
|
||||
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useClient();
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
|
||||
return (
|
||||
|
|
@ -32,7 +31,9 @@ export default function ImageFile({ attachment, ...props }: Props) {
|
|||
className={classNames(styles.image, {
|
||||
[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")}
|
||||
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
||||
onError={() => setLoading(ImageLoadingState.Error)}
|
||||
|
|
|
|||
|
|
@ -221,13 +221,15 @@ export const MessageReply = observer(
|
|||
</em>
|
||||
</>
|
||||
)}
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={message.content?.replace(
|
||||
/\n/g,
|
||||
" ",
|
||||
)}
|
||||
/>
|
||||
{message.content && (
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={message.content.replace(
|
||||
/\n/g,
|
||||
" ",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,16 +3,12 @@ import { API } from "revolt.js";
|
|||
|
||||
import styles from "./Attachment.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
StatusContext,
|
||||
} from "../../../../context/revoltjs/RevoltClient";
|
||||
import { Button, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import Preloader from "../../../ui/Preloader";
|
||||
import { Button } from "@revoltchat/ui";
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline";
|
||||
|
||||
interface Props {
|
||||
attachment: API.File;
|
||||
|
|
@ -24,9 +20,8 @@ export default function TextFile({ attachment }: Props) {
|
|||
const [gated, setGated] = useState(attachment.size > 100_000);
|
||||
const [content, setContent] = useState<undefined | string>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const status = useContext(StatusContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const client = useClient();
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -57,7 +52,7 @@ export default function TextFile({ attachment }: Props) {
|
|||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [content, loading, gated, status, attachment._id, attachment.size, url]);
|
||||
}, [content, loading, gated, attachment._id, attachment.size, url]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -149,12 +149,12 @@ function FileEntry({
|
|||
<EmptyEntry className="icon">
|
||||
<File size={36} />
|
||||
</EmptyEntry>
|
||||
<div class="overlay">
|
||||
<div className="overlay">
|
||||
<XCircle size={36} />
|
||||
</div>
|
||||
</PreviewBox>
|
||||
<span class="fn">{file.name}</span>
|
||||
<span class="size">{determineFileSize(file.size)}</span>
|
||||
<span className="fn">{file.name}</span>
|
||||
<span className="size">{determineFileSize(file.size)}</span>
|
||||
</Entry>
|
||||
);
|
||||
|
||||
|
|
@ -169,13 +169,18 @@ function FileEntry({
|
|||
return (
|
||||
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
|
||||
<PreviewBox onClick={remove}>
|
||||
<img class="icon" src={url} alt={file.name} loading="eager" />
|
||||
<div class="overlay">
|
||||
<img
|
||||
className="icon"
|
||||
src={url}
|
||||
alt={file.name}
|
||||
loading="eager"
|
||||
/>
|
||||
<div className="overlay">
|
||||
<XCircle size={36} />
|
||||
</div>
|
||||
</PreviewBox>
|
||||
<span class="fn">{file.name}</span>
|
||||
<span class="size">{determineFileSize(file.size)}</span>
|
||||
<span className="fn">{file.name}</span>
|
||||
<span className="size">{determineFileSize(file.size)}</span>
|
||||
</Entry>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import {
|
|||
Share,
|
||||
InfoSquare,
|
||||
Notification,
|
||||
HappyBeaming,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Permission } from "revolt.js";
|
||||
import { Message as MessageObject } from "revolt.js";
|
||||
import styled from "styled-components";
|
||||
|
||||
|
|
@ -18,17 +18,16 @@ import { internalEmit } from "../../../../lib/eventEmitter";
|
|||
import { shiftKeyPressed } from "../../../../lib/modifiers";
|
||||
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||
|
||||
import { state } from "../../../../mobx/State";
|
||||
import { QueuedMessage } from "../../../../mobx/stores/MessageQueue";
|
||||
|
||||
import {
|
||||
Screen,
|
||||
useIntermediate,
|
||||
} from "../../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||
import Tooltip from "../../../common/Tooltip";
|
||||
import { ReactionWrapper } from "../attachments/Reactions";
|
||||
|
||||
interface Props {
|
||||
reactionsOpen: boolean;
|
||||
setReactionsOpen: (v: boolean) => void;
|
||||
message: MessageObject;
|
||||
queued?: QueuedMessage;
|
||||
}
|
||||
|
|
@ -87,127 +86,151 @@ const Divider = styled.div`
|
|||
background: var(--tertiary-background);
|
||||
`;
|
||||
|
||||
export const MessageOverlayBar = observer(({ message, queued }: Props) => {
|
||||
const client = useClient();
|
||||
const { openScreen, writeClipboard } = useIntermediate();
|
||||
const isAuthor = message.author_id === client.user!._id;
|
||||
export const MessageOverlayBar = observer(
|
||||
({ reactionsOpen, setReactionsOpen, message, queued }: Props) => {
|
||||
const client = message.client;
|
||||
const isAuthor = message.author_id === client.user!._id;
|
||||
|
||||
const [copied, setCopied] = useState<"link" | "id">(null!);
|
||||
const [extraActions, setExtra] = useState(shiftKeyPressed);
|
||||
const [copied, setCopied] = useState<"link" | "id">(null!);
|
||||
const [extraActions, setExtra] = useState(shiftKeyPressed);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey);
|
||||
useEffect(() => {
|
||||
const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey);
|
||||
|
||||
document.addEventListener("keyup", handler);
|
||||
document.addEventListener("keydown", handler);
|
||||
document.addEventListener("keyup", handler);
|
||||
document.addEventListener("keydown", handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keyup", handler);
|
||||
document.removeEventListener("keydown", handler);
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
document.removeEventListener("keyup", handler);
|
||||
document.removeEventListener("keydown", handler);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<OverlayBar>
|
||||
<Tooltip content="Reply">
|
||||
<Entry onClick={() => internalEmit("ReplyBar", "add", message)}>
|
||||
<Share size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
return (
|
||||
<OverlayBar>
|
||||
{message.channel?.havePermission("SendMessage") && (
|
||||
<Tooltip content="Reply">
|
||||
<Entry
|
||||
onClick={() =>
|
||||
internalEmit("ReplyBar", "add", message)
|
||||
}>
|
||||
<Share size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<Tooltip content="Edit">
|
||||
{message.channel?.havePermission("React") && (
|
||||
<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
|
||||
onClick={() =>
|
||||
internalEmit(
|
||||
"MessageRenderer",
|
||||
"edit_message",
|
||||
message._id,
|
||||
)
|
||||
openContextMenu("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel_id,
|
||||
queued,
|
||||
})
|
||||
}>
|
||||
<Pencil size={18} />
|
||||
<DotsVerticalRounded size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAuthor ||
|
||||
(message.channel &&
|
||||
message.channel.havePermission("ManageMessages")) ? (
|
||||
<Tooltip content="Delete">
|
||||
<Entry
|
||||
onClick={(e) =>
|
||||
e.shiftKey
|
||||
? message.delete()
|
||||
: openScreen({
|
||||
id: "special_prompt",
|
||||
type: "delete_message",
|
||||
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,
|
||||
);
|
||||
{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;
|
||||
if (index > 0) {
|
||||
unread_id = messages[index - 1]._id;
|
||||
}
|
||||
let unread_id = message._id;
|
||||
if (index > 0) {
|
||||
unread_id = messages[index - 1]._id;
|
||||
}
|
||||
|
||||
internalEmit("NewMessages", "mark", unread_id);
|
||||
message.channel?.ack(unread_id, true);
|
||||
}}>
|
||||
<Notification size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={copied === "link" ? "Copied!" : "Copy Link"}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("link");
|
||||
writeClipboard(message.url);
|
||||
}}>
|
||||
<LinkAlt size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={copied === "id" ? "Copied!" : "Copy ID"}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("id");
|
||||
writeClipboard(message._id);
|
||||
}}>
|
||||
<InfoSquare size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</OverlayBar>
|
||||
);
|
||||
});
|
||||
internalEmit(
|
||||
"NewMessages",
|
||||
"mark",
|
||||
unread_id,
|
||||
);
|
||||
message.channel?.ack(unread_id, true);
|
||||
}}>
|
||||
<Notification size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
copied === "link" ? "Copied!" : "Copy Link"
|
||||
}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("link");
|
||||
modalController.writeText(message.url);
|
||||
}}>
|
||||
<LinkAlt size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={copied === "id" ? "Copied!" : "Copy ID"}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("id");
|
||||
modalController.writeText(message._id);
|
||||
}}>
|
||||
<InfoSquare size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</OverlayBar>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Bar } from "./JumpToBottom";
|
|||
export default observer(
|
||||
({ channel, last_id }: { channel: Channel; last_id?: string }) => {
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const [timeAgo, setTimeAgo] = useState("");
|
||||
const hide = () => setHidden(true);
|
||||
|
||||
useEffect(() => setHidden(false), [last_id]);
|
||||
|
|
@ -29,6 +30,14 @@ export default observer(
|
|||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (last_id) {
|
||||
try {
|
||||
setTimeAgo(dayjs(decodeTime(last_id)).fromNow());
|
||||
} catch (err) {}
|
||||
}
|
||||
}, [last_id]);
|
||||
|
||||
const renderer = getRenderer(channel);
|
||||
const history = useHistory();
|
||||
if (renderer.state !== "RENDER") return null;
|
||||
|
|
@ -52,7 +61,7 @@ export default observer(
|
|||
<Text
|
||||
id="app.main.channel.misc.new_messages"
|
||||
fields={{
|
||||
time_ago: dayjs(decodeTime(last_id)).fromNow(),
|
||||
time_ago: timeAgo,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import styled from "styled-components/macro";
|
|||
import { Text } from "preact-i18n";
|
||||
import { StateUpdater, useEffect } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||
|
||||
import { useApplicationState } from "../../../../mobx/State";
|
||||
|
|
@ -14,8 +16,6 @@ import { SECTION_MENTION } from "../../../../mobx/stores/Layout";
|
|||
import { Reply } from "../../../../mobx/stores/MessageQueue";
|
||||
|
||||
import Tooltip from "../../../common/Tooltip";
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
|
||||
import Markdown from "../../../markdown/Markdown";
|
||||
import UserShort from "../../user/UserShort";
|
||||
import { SystemMessage } from "../SystemMessage";
|
||||
|
|
@ -152,11 +152,11 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||
return (
|
||||
<Base key={reply.id}>
|
||||
<ReplyBase preview>
|
||||
<div class="replyto">
|
||||
<div className="replyto">
|
||||
<Text id="app.main.channel.reply.replying" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="username">
|
||||
<div className="content">
|
||||
<div className="username">
|
||||
<UserShort
|
||||
size={16}
|
||||
showServerIdentity
|
||||
|
|
@ -164,7 +164,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||
masquerade={message.masquerade!}
|
||||
/>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div className="message">
|
||||
{message.attachments && (
|
||||
<>
|
||||
<File size={16} />
|
||||
|
|
@ -185,18 +185,20 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||
hideInfo
|
||||
/>
|
||||
) : (
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={message.content?.replace(
|
||||
/\n/g,
|
||||
" ",
|
||||
)}
|
||||
/>
|
||||
message.content && (
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={message.content.replace(
|
||||
/\n/g,
|
||||
" ",
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ReplyBase>
|
||||
<span class="actions">
|
||||
<span className="actions">
|
||||
{message.author_id !== client.user!._id && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
|
|
@ -225,7 +227,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||
content={
|
||||
<Text id="app.main.channel.reply.toggle" />
|
||||
}>
|
||||
<span class="toggle">
|
||||
<span className="toggle">
|
||||
<At size={15} />
|
||||
<Text
|
||||
id={
|
||||
|
|
|
|||
|
|
@ -76,7 +76,9 @@ export default observer(({ channel }: Props) => {
|
|||
if (users.length >= 5) {
|
||||
text = <Text id="app.main.channel.typing.several" />;
|
||||
} else if (users.length > 1) {
|
||||
const userlist = [...users].map((x) => x!.username);
|
||||
const userlist = [...users].map(
|
||||
(x) => x!.display_name ?? x!.username,
|
||||
);
|
||||
const user = userlist.pop();
|
||||
|
||||
text = (
|
||||
|
|
@ -92,7 +94,9 @@ export default observer(({ channel }: Props) => {
|
|||
text = (
|
||||
<Text
|
||||
id="app.main.channel.typing.single"
|
||||
fields={{ user: users[0]!.username }}
|
||||
fields={{
|
||||
user: users[0]!.display_name ?? users[0]!.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
&.website {
|
||||
gap: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> div:nth-child(1) {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import styles from "./Embed.module.scss";
|
|||
import classNames from "classnames";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
||||
import Markdown from "../../../markdown/Markdown";
|
||||
import Attachment from "../attachments/Attachment";
|
||||
|
|
@ -24,7 +23,6 @@ const MAX_PREVIEW_SIZE = 150;
|
|||
export default function Embed({ embed }: Props) {
|
||||
const client = useClient();
|
||||
|
||||
const { openScreen, openLink } = useIntermediate();
|
||||
const maxWidth = Math.min(
|
||||
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
||||
MAX_EMBED_WIDTH,
|
||||
|
|
@ -69,7 +67,8 @@ export default function Embed({ embed }: Props) {
|
|||
break;
|
||||
}
|
||||
case "Twitch":
|
||||
case "Lightspeed": {
|
||||
case "Lightspeed":
|
||||
case "Streamable": {
|
||||
mw = 1280;
|
||||
mh = 720;
|
||||
break;
|
||||
|
|
@ -143,7 +142,11 @@ export default function Embed({ embed }: Props) {
|
|||
<a
|
||||
onMouseDown={(ev) =>
|
||||
(ev.button === 0 || ev.button === 1) &&
|
||||
openLink(embed.url!)
|
||||
modalController.openLink(
|
||||
embed.url!,
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
}
|
||||
className={styles.title}>
|
||||
{embed.title}
|
||||
|
|
@ -191,8 +194,13 @@ export default function Embed({ embed }: Props) {
|
|||
type="text/html"
|
||||
frameBorder="0"
|
||||
loading="lazy"
|
||||
onClick={() => openScreen({ id: "image_viewer", embed })}
|
||||
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
||||
onClick={() =>
|
||||
modalController.push({ type: "image_viewer", embed })
|
||||
}
|
||||
onMouseDown={(ev) =>
|
||||
ev.button === 1 &&
|
||||
modalController.openLink(embed.url, undefined, true)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Group } from "@styled-icons/boxicons-solid";
|
||||
import { reaction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Message, API } from "revolt.js";
|
||||
|
|
@ -7,20 +6,18 @@ import styled, { css } from "styled-components/macro";
|
|||
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button } from "@revoltchat/ui";
|
||||
import { Button, Category, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../../context/revoltjs/util";
|
||||
import { I18nError } from "../../../../context/Locale";
|
||||
|
||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||
import Overline from "../../../ui/Overline";
|
||||
import Preloader from "../../../ui/Preloader";
|
||||
import {
|
||||
useClient,
|
||||
useSession,
|
||||
} from "../../../../controllers/client/ClientController";
|
||||
import { takeError } from "../../../../controllers/client/jsx/error";
|
||||
|
||||
const EmbedInviteBase = styled.div`
|
||||
width: 400px;
|
||||
|
|
@ -79,8 +76,8 @@ type Props = {
|
|||
|
||||
export function EmbedInvite({ code }: Props) {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
||||
|
|
@ -91,7 +88,7 @@ export function EmbedInvite({ code }: Props) {
|
|||
useEffect(() => {
|
||||
if (
|
||||
typeof invite === "undefined" &&
|
||||
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
|
||||
(session.state === "Online" || session.state === "Ready")
|
||||
) {
|
||||
client
|
||||
.fetchInvite(code)
|
||||
|
|
@ -100,7 +97,7 @@ export function EmbedInvite({ code }: Props) {
|
|||
)
|
||||
.catch((err) => setError(takeError(err)));
|
||||
}
|
||||
}, [client, code, invite, status]);
|
||||
}, [client, code, invite, session.state]);
|
||||
|
||||
if (typeof invite === "undefined") {
|
||||
return error ? (
|
||||
|
|
@ -129,8 +126,16 @@ export function EmbedInvite({ code }: Props) {
|
|||
<EmbedInviteName>{invite.server_name}</EmbedInviteName>
|
||||
<EmbedInviteMemberCount>
|
||||
<Group size={12} />
|
||||
{invite.member_count.toLocaleString()}{" "}
|
||||
{invite.member_count === 1 ? "member" : "members"}
|
||||
{invite.member_count != null ? (
|
||||
<>
|
||||
{invite.member_count.toLocaleString()}{" "}
|
||||
{invite.member_count === 1
|
||||
? "member"
|
||||
: "members"}
|
||||
</>
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</EmbedInviteMemberCount>
|
||||
</EmbedInviteDetails>
|
||||
{processing ? (
|
||||
|
|
@ -160,7 +165,11 @@ export function EmbedInvite({ code }: Props) {
|
|||
</Button>
|
||||
)}
|
||||
</EmbedInviteBase>
|
||||
{joinError && <Overline type="error" error={joinError} />}
|
||||
{joinError && (
|
||||
<Category>
|
||||
<I18nError error={joinError} />
|
||||
</Category>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { API } from "revolt.js";
|
|||
|
||||
import styles from "./Embed.module.scss";
|
||||
|
||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||
|
||||
interface Props {
|
||||
embed: API.Embed;
|
||||
|
|
@ -14,7 +14,6 @@ interface Props {
|
|||
|
||||
export default function EmbedMedia({ embed, width, height }: Props) {
|
||||
if (embed.type !== "Website") return null;
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useClient();
|
||||
|
||||
switch (embed.special?.type) {
|
||||
|
|
@ -50,7 +49,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
|||
case "Lightspeed":
|
||||
return (
|
||||
<iframe
|
||||
src={`https://next.lightspeed.tv/embed/${embed.special.id}`}
|
||||
src={`https://new.lightspeed.tv/embed/${embed.special.id}/stream`}
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
scrolling="no"
|
||||
|
|
@ -93,6 +92,16 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
case "Streamable": {
|
||||
return (
|
||||
<iframe
|
||||
src={`https://streamable.com/e/${embed.special.id}?loop=0`}
|
||||
seamless
|
||||
loading="lazy"
|
||||
style={{ height }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
if (embed.video) {
|
||||
const url = embed.video.url;
|
||||
|
|
@ -115,10 +124,10 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
|||
className={styles.image}
|
||||
src={client.proxyFile(url)}
|
||||
loading="lazy"
|
||||
style={{ width, height }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "image_viewer",
|
||||
modalController.push({
|
||||
type: "image_viewer",
|
||||
embed: embed.image!,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { API } from "revolt.js";
|
|||
|
||||
import styles from "./Embed.module.scss";
|
||||
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
interface Props {
|
||||
embed: API.Image;
|
||||
|
|
@ -20,7 +20,7 @@ export default function EmbedMediaActions({ embed }: Props) {
|
|||
</span>
|
||||
<a
|
||||
href={embed.url}
|
||||
class={styles.openIcon}
|
||||
className={styles.openIcon}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@ enum Badges {
|
|||
Paw = 128,
|
||||
EarlyAdopter = 256,
|
||||
ReservedRelevantJokeBadge1 = 512,
|
||||
ReservedRelevantJokeBadge2 = 1024,
|
||||
}
|
||||
|
||||
const BadgesBase = styled.div`
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
margin-top: 4px;
|
||||
flex-direction: row;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ export default function UserBadges({ badges, uid }: Props) {
|
|||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
|
||||
}>
|
||||
<Shield size={32} color="gray" />
|
||||
<Shield size={24} color="gray" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
|
|
@ -119,7 +119,7 @@ export default function UserBadges({ badges, uid }: Props) {
|
|||
}}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://insrt.uk/donate",
|
||||
"https://wiki.revolt.chat/notes/project/financial-support/",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
|
|
@ -135,6 +135,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 ? (
|
||||
<Tooltip content="🦊">
|
||||
<img src="/assets/badges/paw.svg" />
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
import { User } from "revolt.js";
|
||||
|
||||
import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
|
||||
import { Checkbox, Row, Column } from "@revoltchat/ui";
|
||||
|
||||
import UserIcon from "./UserIcon";
|
||||
import { Username } from "./UserShort";
|
||||
|
||||
type UserProps = Omit<CheckboxProps, "children"> & { user: User };
|
||||
type UserProps = { value: boolean; onChange: (v: boolean) => void; user: User };
|
||||
|
||||
export default function UserCheckbox({ user, ...props }: UserProps) {
|
||||
return (
|
||||
<Checkbox {...props}>
|
||||
<UserIcon target={user} size={32} />
|
||||
<Username user={user} />
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
{...props}
|
||||
title={
|
||||
<Row centred>
|
||||
<UserIcon target={user} size={32} />
|
||||
<Column centred>
|
||||
<Username user={user} />
|
||||
</Column>
|
||||
</Row>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@ import styled from "styled-components/macro";
|
|||
import { openContextMenu } from "preact-context-menu";
|
||||
import { Text, Localizer } from "preact-i18n";
|
||||
|
||||
import { Header, IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import Header from "../../ui/Header";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import Tooltip from "../Tooltip";
|
||||
import UserStatus from "./UserStatus";
|
||||
|
||||
|
|
@ -31,9 +29,14 @@ const HeaderBase = styled.div`
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.new-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.username {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
|
@ -49,17 +52,22 @@ interface Props {
|
|||
}
|
||||
|
||||
export default observer(({ user }: Props) => {
|
||||
const { writeClipboard } = useIntermediate();
|
||||
|
||||
return (
|
||||
<Header topBorder placement="secondary">
|
||||
<Header topBorder palette="secondary">
|
||||
<HeaderBase>
|
||||
<div className="new-name">
|
||||
{user.display_name ?? user.username}
|
||||
</div>
|
||||
<Localizer>
|
||||
<Tooltip content={<Text id="app.special.copy_username" />}>
|
||||
<span
|
||||
className="username"
|
||||
onClick={() => writeClipboard(user.username)}>
|
||||
@{user.username}
|
||||
onClick={() =>
|
||||
modalController.writeText(user.username)
|
||||
}>
|
||||
{user.username}
|
||||
{"#"}
|
||||
{user.discriminator}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Localizer>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { User } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
import Tooltip from "../Tooltip";
|
||||
import { Username } from "./UserShort";
|
||||
import UserStatus from "./UserStatus";
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ import styled, { css } from "styled-components/macro";
|
|||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import fallback from "../assets/user.png";
|
||||
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import IconBase, { IconBaseProps } from "../IconBase";
|
||||
|
||||
type VoiceStatus = "muted" | "deaf";
|
||||
interface Props extends IconBaseProps<User> {
|
||||
status?: boolean;
|
||||
override?: string;
|
||||
voice?: VoiceStatus;
|
||||
masquerade?: API.Masquerade;
|
||||
showServerIdentity?: boolean;
|
||||
|
|
@ -26,6 +26,8 @@ export function useStatusColour(user?: User) {
|
|||
return user?.online && user?.status?.presence !== "Invisible"
|
||||
? user?.status?.presence === "Idle"
|
||||
? theme.getVariable("status-away")
|
||||
: user?.status?.presence === "Focus"
|
||||
? theme.getVariable("status-focus")
|
||||
: user?.status?.presence === "Busy"
|
||||
? theme.getVariable("status-busy")
|
||||
: theme.getVariable("status-online")
|
||||
|
|
@ -69,12 +71,15 @@ export default observer(
|
|||
showServerIdentity,
|
||||
masquerade,
|
||||
innerRef,
|
||||
override,
|
||||
...svgProps
|
||||
} = props;
|
||||
|
||||
let { url } = props;
|
||||
if (masquerade?.avatar) {
|
||||
url = client.proxyFile(masquerade.avatar);
|
||||
} else if (override) {
|
||||
url = override;
|
||||
} else if (!url) {
|
||||
let override;
|
||||
if (target && showServerIdentity) {
|
||||
|
|
@ -114,7 +119,7 @@ export default observer(
|
|||
y="0"
|
||||
width="32"
|
||||
height="32"
|
||||
class="icon"
|
||||
className="icon"
|
||||
mask={mask ?? (status ? "url(#user)" : undefined)}>
|
||||
{<img src={url} draggable={false} loading="lazy" />}
|
||||
</foreignObject>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { TimeFive } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { User, API } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { Ref } from "preact";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
import { dayjs } from "../../../context/Locale";
|
||||
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import Tooltip from "../Tooltip";
|
||||
import UserIcon from "./UserIcon";
|
||||
|
||||
const BotBadge = styled.div`
|
||||
|
|
@ -27,15 +30,34 @@ const BotBadge = styled.div`
|
|||
border-radius: calc(var(--border-radius) / 2);
|
||||
`;
|
||||
|
||||
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
|
||||
type UsernameProps = Omit<
|
||||
JSX.HTMLAttributes<HTMLElement>,
|
||||
"children" | "as"
|
||||
> & {
|
||||
user?: User;
|
||||
prefixAt?: boolean;
|
||||
masquerade?: API.Masquerade;
|
||||
showServerIdentity?: boolean | "both";
|
||||
|
||||
override?: string;
|
||||
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(
|
||||
({
|
||||
user,
|
||||
|
|
@ -43,12 +65,18 @@ export const Username = observer(
|
|||
masquerade,
|
||||
showServerIdentity,
|
||||
innerRef,
|
||||
override,
|
||||
...otherProps
|
||||
}: UsernameProps) => {
|
||||
let username = user?.username;
|
||||
let color;
|
||||
let username =
|
||||
(user as unknown as { display_name: string })?.display_name ??
|
||||
user?.username;
|
||||
let color = masquerade?.colour;
|
||||
let timed_out: Date | undefined;
|
||||
|
||||
if (user && showServerIdentity) {
|
||||
if (override) {
|
||||
username = override;
|
||||
} else if (user && showServerIdentity) {
|
||||
const { server } = useParams<{ server?: string }>();
|
||||
if (server) {
|
||||
const client = useClient();
|
||||
|
|
@ -66,15 +94,14 @@ export const Username = observer(
|
|||
}
|
||||
}
|
||||
|
||||
if (member.roles && member.roles.length > 0) {
|
||||
const srv = client.servers.get(member._id.server);
|
||||
if (srv?.roles) {
|
||||
for (const role of member.roles) {
|
||||
const c = srv.roles[role]?.colour;
|
||||
if (c) {
|
||||
color = c;
|
||||
continue;
|
||||
}
|
||||
if (member.timeout) {
|
||||
timed_out = member.timeout;
|
||||
}
|
||||
|
||||
if (!color) {
|
||||
for (const [_, { colour }] of member.orderedRoles) {
|
||||
if (colour) {
|
||||
color = colour;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,14 +109,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) {
|
||||
return (
|
||||
<>
|
||||
<span {...otherProps} ref={innerRef} style={{ color }}>
|
||||
{masquerade?.name ?? username ?? (
|
||||
<Text id="app.main.channel.unknown_user" />
|
||||
)}
|
||||
</span>
|
||||
{el}
|
||||
<BotBadge>
|
||||
{masquerade ? (
|
||||
<Text id="app.main.channel.bridge" />
|
||||
|
|
@ -101,14 +152,18 @@ export const Username = observer(
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...otherProps} ref={innerRef} style={{ color }}>
|
||||
{prefixAt ? "@" : undefined}
|
||||
{masquerade?.name ?? username ?? (
|
||||
<Text id="app.main.channel.unknown_user" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
if (override) {
|
||||
return (
|
||||
<>
|
||||
{el}
|
||||
<BotBadge>
|
||||
<Text id="app.main.channel.bot" />
|
||||
</BotBadge>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -125,9 +180,9 @@ export default function UserShort({
|
|||
masquerade?: API.Masquerade;
|
||||
showServerIdentity?: boolean;
|
||||
}) {
|
||||
const { openScreen } = useIntermediate();
|
||||
const openProfile = () =>
|
||||
user && openScreen({ id: "profile", user_id: user._id });
|
||||
user &&
|
||||
modalController.push({ type: "user_profile", user_id: user._id });
|
||||
|
||||
const handleUserClick = (e: MouseEvent) => {
|
||||
if (e.shiftKey && user?._id) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ export default observer(({ user, tooltip }: Props) => {
|
|||
return <Text id="app.status.idle" />;
|
||||
}
|
||||
|
||||
if (user.status?.presence === "Focus") {
|
||||
return <Text id="app.status.focus" />;
|
||||
}
|
||||
|
||||
if (user.status?.presence === "Invisible") {
|
||||
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";
|
||||
|
||||
const Renderer = lazy(() => import("./Renderer"));
|
||||
const Renderer = lazy(() => import("./RemarkRenderer"));
|
||||
|
||||
export interface MarkdownProps {
|
||||
content?: string | null;
|
||||
content: string;
|
||||
disallowBigEmoji?: boolean;
|
||||
}
|
||||
|
||||
export default function Markdown(props: MarkdownProps) {
|
||||
if (!props.content) return null;
|
||||
|
||||
return (
|
||||
// @ts-expect-error Typings mis-match.
|
||||
<Suspense fallback={props.content}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,266 @@
|
|||
import "katex/dist/katex.min.css";
|
||||
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";
|
||||
|
||||
// @ts-expect-error no typings
|
||||
import rehypeKatex from "@revoltchat/rehype-katex";
|
||||
|
||||
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,
|
||||
maxLength: 512,
|
||||
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 =
|
||||
/(^(?:(?:[>*+-]|\d+\.)[^\S\r\n]*){5})(?:(?:[>*+-]|\d+\.)[^\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(() => {
|
||||
try {
|
||||
render
|
||||
.process(sanitisedContent)
|
||||
.then((file) => setContent(file.result));
|
||||
} catch (err) {
|
||||
setContent("Message failed to render." as never);
|
||||
}
|
||||
}, [sanitisedContent]);
|
||||
|
||||
const largeEmoji = useMemo(
|
||||
() => !disallowBigEmoji && isOnlyEmoji(content!),
|
||||
[content, disallowBigEmoji],
|
||||
);
|
||||
|
||||
return <Container largeEmoji={largeEmoji}>{Content}</Container>;
|
||||
});
|
||||
|
|
@ -1,292 +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 { generateEmoji } from "../common/Emoji";
|
||||
|
||||
import { emojiDictionary } from "../../assets/emojis";
|
||||
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") {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { passThroughComponents } from "./plugins/remarkRegexComponent";
|
||||
import { timestampHandler } from "./plugins/timestamps";
|
||||
|
||||
export const handlers = {
|
||||
...passThroughComponents("emoji", "spoiler", "mention", "channel"),
|
||||
timestamp: timestampHandler,
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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,
|
||||
undefined,
|
||||
ev.currentTarget.innerText !== href,
|
||||
) && ev.preventDefault()
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
|
|
@ -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}/original`
|
||||
: 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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
@ -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),
|
||||
);
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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-graphql";
|
||||
import "prismjs/components/prism-shell-session";
|
||||
import "prismjs/components/prism-java";
|
||||
import "prismjs/components/prism-powershell";
|
||||
import "prismjs/components/prism-swift";
|
||||
import "prismjs/components/prism-yaml";
|
||||
|
|
@ -87,7 +86,6 @@ import "prismjs/components/prism-moonscript";
|
|||
import "prismjs/components/prism-qml";
|
||||
import "prismjs/components/prism-vim";
|
||||
import "prismjs/components/prism-nim";
|
||||
import "prismjs/components/prism-swift";
|
||||
import "prismjs/components/prism-haml";
|
||||
import "prismjs/components/prism-ada";
|
||||
import "prismjs/components/prism-arduino";
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const TitlebarBase = styled.div<Props>`
|
|||
export function Titlebar(props: Props) {
|
||||
return (
|
||||
<TitlebarBase {...props}>
|
||||
<div class="title">
|
||||
<div className="title">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 193.733 37.438">
|
||||
|
|
@ -114,7 +114,7 @@ export function Titlebar(props: Props) {
|
|||
<Wrench size="12.5" />
|
||||
)}
|
||||
</div>
|
||||
{/*<div class="actions quick">
|
||||
{/*<div className="actions quick">
|
||||
<Tooltip
|
||||
content="Mute"
|
||||
placement="bottom">
|
||||
|
|
@ -130,9 +130,9 @@ export function Titlebar(props: Props) {
|
|||
</div>
|
||||
</Tooltip>
|
||||
</div>*/}
|
||||
<div class="drag" />
|
||||
<div className="drag" />
|
||||
<UpdateIndicator style="titlebar" />
|
||||
<div class="actions">
|
||||
<div className="actions">
|
||||
<div onClick={window.native.min}>
|
||||
<svg
|
||||
aria-hidden="false"
|
||||
|
|
@ -164,7 +164,7 @@ export function Titlebar(props: Props) {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div onClick={window.native.close} class="error">
|
||||
<div onClick={window.native.close} className="error">
|
||||
<svg
|
||||
aria-hidden="false"
|
||||
width="12"
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import { observer } from "mobx-react-lite";
|
|||
import { useHistory, useLocation } from "react-router";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { Centred, IconButton } from "@revoltchat/ui";
|
||||
|
||||
import ConditionalLink from "../../lib/ConditionalLink";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { useClient } from "../../controllers/client/ClientController";
|
||||
import UserIcon from "../common/user/UserIcon";
|
||||
import IconButton from "../ui/IconButton";
|
||||
|
||||
const Base = styled.div`
|
||||
background: var(--secondary-background);
|
||||
|
|
@ -24,8 +24,19 @@ const Navbar = styled.div`
|
|||
height: var(--bottom-navigation-height);
|
||||
`;
|
||||
|
||||
/**
|
||||
* I've decided that this whole component
|
||||
* needs to be re-written 👍👍👍👍👍👍
|
||||
*/
|
||||
|
||||
const Button = styled.a<{ active: boolean }>`
|
||||
flex: 1;
|
||||
color: var(--foreground);
|
||||
|
||||
// ok
|
||||
* {
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
> a,
|
||||
> div,
|
||||
|
|
@ -63,7 +74,7 @@ export default observer(() => {
|
|||
<Base>
|
||||
<Navbar>
|
||||
<Button active={homeActive}>
|
||||
<IconButton
|
||||
<Centred
|
||||
onClick={() => {
|
||||
if (settingsActive) {
|
||||
if (history.length > 0) {
|
||||
|
|
@ -80,14 +91,14 @@ export default observer(() => {
|
|||
}
|
||||
}}>
|
||||
<Message size={24} />
|
||||
</IconButton>
|
||||
</Centred>
|
||||
</Button>
|
||||
<Button active={friendsActive}>
|
||||
<ConditionalLink active={friendsActive} to="/friends">
|
||||
<IconButton>
|
||||
<IconButton>
|
||||
<ConditionalLink active={friendsActive} to="/friends">
|
||||
<Group size={25} />
|
||||
</IconButton>
|
||||
</ConditionalLink>
|
||||
</ConditionalLink>
|
||||
</IconButton>
|
||||
</Button>
|
||||
{/*<Button active={searchActive}>
|
||||
<ConditionalLink active={searchActive} to="/search">
|
||||
|
|
@ -104,20 +115,20 @@ export default observer(() => {
|
|||
</ConditionalLink>
|
||||
</Button>*/}
|
||||
<Button active={discoverActive}>
|
||||
<ConditionalLink
|
||||
active={discoverActive}
|
||||
to="/discover/servers">
|
||||
<IconButton>
|
||||
<IconButton>
|
||||
<ConditionalLink
|
||||
active={discoverActive}
|
||||
to="/discover/servers">
|
||||
<Compass size={24} />
|
||||
</IconButton>
|
||||
</ConditionalLink>
|
||||
</ConditionalLink>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Button active={settingsActive}>
|
||||
<ConditionalLink active={settingsActive} to="/settings">
|
||||
<IconButton>
|
||||
<IconButton>
|
||||
<ConditionalLink active={settingsActive} to="/settings">
|
||||
<UserIcon target={user} size={26} status={true} />
|
||||
</IconButton>
|
||||
</ConditionalLink>
|
||||
</ConditionalLink>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</Navbar>
|
||||
</Base>
|
||||
|
|
|
|||
|
|
@ -5,23 +5,20 @@ import { User, Channel } from "revolt.js";
|
|||
|
||||
import styles from "./Item.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { Ref } from "preact";
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import ChannelIcon from "../../common/ChannelIcon";
|
||||
import Tooltip from "../../common/Tooltip";
|
||||
import UserIcon from "../../common/user/UserIcon";
|
||||
import { Username } from "../../common/user/UserShort";
|
||||
import UserStatus from "../../common/user/UserStatus";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
|
||||
type CommonProps = Omit<
|
||||
JSX.HTMLAttributes<HTMLDivElement>,
|
||||
|
|
@ -52,7 +49,6 @@ export const UserButton = observer((props: UserProps) => {
|
|||
channel,
|
||||
...divProps
|
||||
} = props;
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -113,8 +109,7 @@ export const UserButton = observer((props: UserProps) => {
|
|||
className={styles.icon}
|
||||
onClick={(e) =>
|
||||
stopPropagation(e) &&
|
||||
openScreen({
|
||||
id: "special_prompt",
|
||||
modalController.push({
|
||||
type: "close_dm",
|
||||
target: channel,
|
||||
})
|
||||
|
|
@ -151,7 +146,6 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||
return <UserButton {...{ active, alert, channel, user }} />;
|
||||
}
|
||||
|
||||
const { openScreen } = useIntermediate();
|
||||
const alerting = alert && !muted && !active;
|
||||
|
||||
return (
|
||||
|
|
@ -161,16 +155,16 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||
data-alert={alerting}
|
||||
data-muted={muted}
|
||||
aria-label={channel.name}
|
||||
className={classNames(styles.item, { [styles.compact]: compact })}
|
||||
className={classNames(styles.item, {
|
||||
[styles.compact]: compact,
|
||||
})}
|
||||
{...useTriggerEvents("Menu", {
|
||||
channel: channel._id,
|
||||
unread: !!alert,
|
||||
})}>
|
||||
<ChannelIcon
|
||||
className={styles.avatar}
|
||||
target={channel}
|
||||
size={compact ? 24 : 32}
|
||||
/>
|
||||
<div className={styles.avatar}>
|
||||
<ChannelIcon target={channel} size={compact ? 24 : 32} />
|
||||
</div>
|
||||
<div className={styles.name}>
|
||||
<div>{channel.name}</div>
|
||||
{channel.channel_type === "Group" && (
|
||||
|
|
@ -183,7 +177,9 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||
<Text
|
||||
id="quantities.members"
|
||||
plural={channel.recipients!.length}
|
||||
fields={{ count: channel.recipients!.length }}
|
||||
fields={{
|
||||
count: channel.recipients!.length,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -199,8 +195,7 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
|||
<IconButton
|
||||
className={styles.icon}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_prompt",
|
||||
modalController.push({
|
||||
type: "leave_group",
|
||||
target: channel,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,45 +1,47 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { Banner, Button, Column } from "@revoltchat/ui";
|
||||
|
||||
import Banner from "../../ui/Banner";
|
||||
import { useSession } from "../../../controllers/client/ClientController";
|
||||
|
||||
export default function ConnectionStatus() {
|
||||
const status = useContext(StatusContext);
|
||||
const client = useClient();
|
||||
function ConnectionStatus() {
|
||||
const session = useSession()!;
|
||||
|
||||
if (status === ClientStatus.OFFLINE) {
|
||||
if (session.state === "Offline") {
|
||||
return (
|
||||
<Banner>
|
||||
<Text id="app.special.status.offline" />
|
||||
</Banner>
|
||||
);
|
||||
} else if (status === ClientStatus.DISCONNECTED) {
|
||||
} else if (session.state === "Disconnected") {
|
||||
return (
|
||||
<Banner>
|
||||
<Text id="app.special.status.disconnected" /> <br />
|
||||
<a onClick={() => client.websocket.connect()}>
|
||||
<Text id="app.special.status.reconnect" />
|
||||
</a>
|
||||
<Column centred>
|
||||
<Text id="app.special.status.disconnected" />
|
||||
<Button
|
||||
compact
|
||||
palette="secondary"
|
||||
onClick={() =>
|
||||
session.emit({
|
||||
action: "RETRY",
|
||||
})
|
||||
}>
|
||||
<Text id="app.status.reconnect" />
|
||||
</Button>
|
||||
</Column>
|
||||
</Banner>
|
||||
);
|
||||
} else if (status === ClientStatus.CONNECTING) {
|
||||
return (
|
||||
<Banner>
|
||||
<Text id="app.special.status.connecting" />
|
||||
</Banner>
|
||||
);
|
||||
} else if (status === ClientStatus.RECONNECTING) {
|
||||
} else if (session.state === "Connecting") {
|
||||
return (
|
||||
<Banner>
|
||||
<Text id="app.special.status.reconnecting" />
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default observer(ConnectionStatus);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Plus } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Home,
|
||||
UserDetail,
|
||||
|
|
@ -11,18 +12,18 @@ import styled, { css } from "styled-components/macro";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { Category, IconButton } from "@revoltchat/ui";
|
||||
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Category from "../../ui/Category";
|
||||
import placeholderSVG from "../items/placeholder.svg";
|
||||
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
||||
import ConnectionStatus from "../items/ConnectionStatus";
|
||||
|
|
@ -44,10 +45,9 @@ const Navbar = styled.div`
|
|||
|
||||
export default observer(() => {
|
||||
const { pathname } = useLocation();
|
||||
const client = useContext(AppContext);
|
||||
const client = useClient();
|
||||
const state = useApplicationState();
|
||||
const { channel: channel_id } = useParams<{ channel: string }>();
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const channels = [...client.channels.values()].filter(
|
||||
(x) =>
|
||||
|
|
@ -125,15 +125,17 @@ export default observer(() => {
|
|||
</ButtonItem>
|
||||
</Link>
|
||||
)}
|
||||
<Category
|
||||
text={<Text id="app.main.categories.conversations" />}
|
||||
action={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_group",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Category>
|
||||
<Text id="app.main.categories.conversations" />
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
modalController.push({
|
||||
type: "create_group",
|
||||
})
|
||||
}>
|
||||
<Plus size={16} />
|
||||
</IconButton>
|
||||
</Category>
|
||||
{channels.length === 0 && (
|
||||
<img src={placeholderSVG} loading="eager" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import { ServerList } from "@revoltchat/ui";
|
|||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import { IS_REVOLT } from "../../../version";
|
||||
|
||||
/**
|
||||
* Server list sidebar shim component
|
||||
|
|
@ -16,13 +17,11 @@ import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|||
export default observer(() => {
|
||||
const client = useClient();
|
||||
const state = useApplicationState();
|
||||
const { openScreen } = useIntermediate();
|
||||
const { server: server_id } = useParams<{ server?: string }>();
|
||||
|
||||
const createServer = useCallback(
|
||||
() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
modalController.push({
|
||||
type: "create_server",
|
||||
}),
|
||||
[],
|
||||
|
|
@ -37,6 +36,7 @@ export default observer(() => {
|
|||
home={state.layout.getLastHomePath}
|
||||
servers={state.ordering.orderedServers}
|
||||
reorder={state.ordering.reorderServer}
|
||||
showDiscovery={IS_REVOLT}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { Redirect, useParams } from "react-router";
|
||||
import { Server } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { Ref } from "preact";
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { Category } from "@revoltchat/ui";
|
||||
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
|
@ -14,12 +14,9 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import CollapsibleSection from "../../common/CollapsibleSection";
|
||||
import ServerHeader from "../../common/ServerHeader";
|
||||
import Category from "../../ui/Category";
|
||||
|
||||
import { ChannelButton } from "../items/ButtonItem";
|
||||
import ConnectionStatus from "../items/ConnectionStatus";
|
||||
|
||||
|
|
@ -53,8 +50,10 @@ const ServerList = styled.div`
|
|||
export default observer(() => {
|
||||
const client = useClient();
|
||||
const state = useApplicationState();
|
||||
const { server: server_id, channel: channel_id } =
|
||||
useParams<{ server: string; channel?: string }>();
|
||||
const { server: server_id, channel: channel_id } = useParams<{
|
||||
server: string;
|
||||
channel?: string;
|
||||
}>();
|
||||
|
||||
const server = client.servers.get(server_id);
|
||||
if (!server) return <Redirect to="/" />;
|
||||
|
|
@ -122,14 +121,18 @@ export default observer(() => {
|
|||
channels.push(addChannel(id));
|
||||
}
|
||||
|
||||
elements.push(
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category text={category.title} />}>
|
||||
{channels}
|
||||
</CollapsibleSection>,
|
||||
);
|
||||
if (category.title === "Default") {
|
||||
elements.push(...channels);
|
||||
} else {
|
||||
elements.push(
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category>{category.title}</Category>}>
|
||||
{channels}
|
||||
</CollapsibleSection>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,7 @@ import { memo } from "preact/compat";
|
|||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
import {
|
||||
Screen,
|
||||
useIntermediate,
|
||||
} from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import { UserButton } from "../items/ButtonItem";
|
||||
|
||||
export type MemberListGroup = {
|
||||
|
|
@ -55,15 +51,7 @@ const NoOomfie = styled.div`
|
|||
`;
|
||||
|
||||
const ItemContent = memo(
|
||||
({
|
||||
item,
|
||||
context,
|
||||
openScreen,
|
||||
}: {
|
||||
item: User;
|
||||
context: Channel;
|
||||
openScreen: (screen: Screen) => void;
|
||||
}) => (
|
||||
({ item, context }: { item: User; context: Channel }) => (
|
||||
<UserButton
|
||||
key={item._id}
|
||||
user={item}
|
||||
|
|
@ -77,13 +65,12 @@ const ItemContent = memo(
|
|||
`<@${item._id}>`,
|
||||
"mention",
|
||||
);
|
||||
} else
|
||||
[
|
||||
openScreen({
|
||||
id: "profile",
|
||||
user_id: item._id,
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
modalController.push({
|
||||
type: "user_profile",
|
||||
user_id: item._id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
|
@ -96,8 +83,6 @@ export default function MemberList({
|
|||
entries: MemberListGroup[];
|
||||
context: Channel;
|
||||
}) {
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
return (
|
||||
<GroupedVirtuoso
|
||||
groupCounts={entries.map((x) => x.users.length)}
|
||||
|
|
@ -114,7 +99,7 @@ export default function MemberList({
|
|||
)}
|
||||
{entry.type !== "no_offline" && (
|
||||
<>
|
||||
{" - "}
|
||||
{" – "}
|
||||
{entry.users.length}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -133,19 +118,20 @@ export default function MemberList({
|
|||
return (
|
||||
<NoOomfie>
|
||||
<div>
|
||||
Offline users temporarily disabled for this
|
||||
server, see issue{" "}
|
||||
Offline users have temporarily been disabled for
|
||||
larger servers - see{" "}
|
||||
<a
|
||||
href="https://github.com/revoltchat/delta/issues/128"
|
||||
target="_blank">
|
||||
#128
|
||||
href="https://github.com/revoltchat/backend/issues/178"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
issue #178
|
||||
</a>{" "}
|
||||
for when this will be resolved.
|
||||
</div>
|
||||
<div>
|
||||
You may re-enable them in{" "}
|
||||
You may re-enable them{" "}
|
||||
<Link to="/settings/experiments">
|
||||
<a>experiments</a>
|
||||
<a>here</a>
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
|
|
@ -158,11 +144,7 @@ export default function MemberList({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<ItemContent
|
||||
item={item}
|
||||
context={context}
|
||||
openScreen={openScreen}
|
||||
/>
|
||||
<ItemContent item={item} context={context} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import { observer } from "mobx-react-lite";
|
|||
import { useParams } from "react-router-dom";
|
||||
import { Channel, Server, User, API } from "revolt.js";
|
||||
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
useSession,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
} from "../../../controllers/client/ClientController";
|
||||
import { GenericSidebarBase } from "../SidebarBase";
|
||||
import MemberList, { MemberListGroup } from "./MemberList";
|
||||
|
||||
|
|
@ -78,17 +76,25 @@ function useEntries(
|
|||
|
||||
keys.forEach((key) => {
|
||||
let u;
|
||||
let member;
|
||||
|
||||
if (isServer) {
|
||||
const { server, user } = JSON.parse(key);
|
||||
if (server !== channel.server_id) return;
|
||||
|
||||
u = client.users.get(user);
|
||||
member = client.members.get(key);
|
||||
|
||||
if (!member?.hasPermission(channel, "ViewChannel")) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
u = client.users.get(key);
|
||||
member = client.members.get(key);
|
||||
}
|
||||
|
||||
if (!u) return;
|
||||
|
||||
const member = client.members.get(key);
|
||||
const sort = member?.nickname ?? u.username;
|
||||
const entry = [u, sort] as [User, string];
|
||||
|
||||
|
|
@ -182,7 +188,7 @@ export const GroupMemberSidebar = observer(
|
|||
);
|
||||
|
||||
// ! FIXME: this is temporary code until we get lazy guilds like subscriptions
|
||||
const FETCHED: Set<String> = new Set();
|
||||
const FETCHED: Set<string> = new Set();
|
||||
|
||||
export function resetMemberSidebarFetched() {
|
||||
FETCHED.clear();
|
||||
|
|
@ -205,18 +211,18 @@ function shouldSkipOffline(id: string) {
|
|||
|
||||
export const ServerMemberSidebar = observer(
|
||||
({ channel }: { channel: Channel }) => {
|
||||
const client = useClient();
|
||||
const status = useContext(StatusContext);
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
channel
|
||||
.server!.syncMembers(shouldSkipOffline(server_id))
|
||||
.catch(() => FETCHED.delete(server_id));
|
||||
}
|
||||
}, [status, channel]);
|
||||
}, [session.state, channel]);
|
||||
|
||||
const entries = useEntries(
|
||||
channel,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,10 @@ import styled from "styled-components/macro";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button } from "@revoltchat/ui";
|
||||
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import Message from "../../common/messaging/Message";
|
||||
import InputBox from "../../ui/InputBox";
|
||||
import Overline from "../../ui/Overline";
|
||||
import Preloader from "../../ui/Preloader";
|
||||
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
|
||||
type SearchState =
|
||||
|
|
@ -103,18 +98,20 @@ export function SearchSidebar({ close }: Props) {
|
|||
<GenericSidebarBase data-scroll-offset="with-padding">
|
||||
<GenericSidebarList>
|
||||
<SearchBase>
|
||||
<Overline type="accent" block hover>
|
||||
<a onClick={close}>« back to members</a>
|
||||
</Overline>
|
||||
<Overline type="subtle" block>
|
||||
<Category>
|
||||
<Error
|
||||
error={<a onClick={close}>« back to members</a>}
|
||||
/>
|
||||
</Category>
|
||||
<Category>
|
||||
<Text id="app.main.channel.search.title" />
|
||||
</Overline>
|
||||
</Category>
|
||||
<InputBox
|
||||
value={query}
|
||||
onKeyDown={(e) => e.key === "Enter" && search()}
|
||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
||||
/>
|
||||
<div class="sort">
|
||||
<div className="sort">
|
||||
{["Latest", "Oldest", "Relevance"].map((key) => (
|
||||
<Button
|
||||
key={key}
|
||||
|
|
@ -129,7 +126,7 @@ export function SearchSidebar({ close }: Props) {
|
|||
</div>
|
||||
{state.type === "loading" && <Preloader type="ring" />}
|
||||
{state.type === "results" && (
|
||||
<div class="list">
|
||||
<div className="list">
|
||||
{state.results.map((message) => {
|
||||
let href = "";
|
||||
if (channel?.channel_type === "TextChannel") {
|
||||
|
|
@ -140,7 +137,7 @@ export function SearchSidebar({ close }: Props) {
|
|||
|
||||
return (
|
||||
<Link to={href} key={message._id}>
|
||||
<div class="message">
|
||||
<div className="message">
|
||||
<Message
|
||||
message={message}
|
||||
head
|
||||
|
|
|
|||
|
|
@ -1,279 +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 TextAreaAutoSize from "../../lib/TextAreaAutoSize";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import {
|
||||
Fonts,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MonospaceFonts,
|
||||
MONOSPACE_FONTS,
|
||||
MONOSPACE_FONT_KEYS,
|
||||
} from "../../context/Theme";
|
||||
|
||||
import Checkbox from "../ui/Checkbox";
|
||||
import ColourSwatches from "../ui/ColourSwatches";
|
||||
import ComboBox from "../ui/ComboBox";
|
||||
import Radio from "../ui/Radio";
|
||||
import CategoryButton from "../ui/fluent/CategoryButton";
|
||||
|
||||
import { EmojiSelector } from "./appearance/EmojiSelector";
|
||||
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
|
||||
|
||||
/**
|
||||
* Component providing a way to switch the base theme being used.
|
||||
*/
|
||||
export const ThemeBaseSelectorShim = 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.
|
||||
* TODO: stabilise
|
||||
*/
|
||||
export const ThemeShopShim = () => {
|
||||
return (
|
||||
<Link to="/discover/themes" replace>
|
||||
<CategoryButton
|
||||
icon={<Brush size={24} />}
|
||||
action="chevron"
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.discover.description" />
|
||||
}
|
||||
hover>
|
||||
<Text id="app.settings.pages.appearance.discover.title" />
|
||||
</CategoryButton>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component providing a way to change current accent colour.
|
||||
*/
|
||||
export const ThemeAccentShim = 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 ThemeCustomCSSShim = 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 DisplayCompactShim = () => {
|
||||
// TODO: WIP feature
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.message_display" />
|
||||
</h3>
|
||||
<div /* className={styles.display} */>
|
||||
<Radio
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.display.default_description" />
|
||||
}
|
||||
checked>
|
||||
<Text id="app.settings.pages.appearance.display.default" />
|
||||
</Radio>
|
||||
<Radio
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.display.compact_description" />
|
||||
}
|
||||
disabled>
|
||||
<Text id="app.settings.pages.appearance.display.compact" />
|
||||
</Radio>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component providing a way to change primary text font.
|
||||
*/
|
||||
export const DisplayFontShim = 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 DisplayMonospaceFontShim = 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 DisplayLigaturesShim = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
if (settings.theme.getFont() !== "Inter") return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={settings.get("appearance:ligatures") ?? false}
|
||||
onChange={(v) => settings.set("appearance:ligatures", v)}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.ligatures" />
|
||||
</Checkbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle showing the send button on desktop.
|
||||
*/
|
||||
export const ShowSendButtonShim = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={settings.get("appearance:show_send_button") ?? false}
|
||||
onChange={(v) => settings.set("appearance:show_send_button", v)}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
|
||||
</Checkbox>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle seasonal themes.
|
||||
*/
|
||||
export const DisplaySeasonalShim = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={settings.get("appearance:seasonal") ?? true}
|
||||
onChange={(v) => settings.set("appearance:seasonal", v)}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
|
||||
</Checkbox>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle transparency effects.
|
||||
*/
|
||||
export const DisplayTransparencyShim = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={settings.get("appearance:transparency") ?? true}
|
||||
onChange={(v) => settings.set("appearance:transparency", v)}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.theme_options.transparency" />
|
||||
</Checkbox>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to change emoji pack.
|
||||
*/
|
||||
export const DisplayEmojiShim = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
return (
|
||||
<EmojiSelector
|
||||
value={settings.get("appearance:emoji")}
|
||||
setValue={(v) => settings.set("appearance:emoji", v)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Block } from "@styled-icons/boxicons-regular";
|
||||
import { Trash } from "@styled-icons/boxicons-solid";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { CategoryButton } from "@revoltchat/ui";
|
||||
|
||||
import {
|
||||
clientController,
|
||||
useClient,
|
||||
} from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
export default function AccountManagement() {
|
||||
const client = useClient();
|
||||
|
||||
const callback = (route: "disable" | "delete") => () =>
|
||||
modalController.mfaFlow(client).then(
|
||||
(ticket) =>
|
||||
ticket &&
|
||||
client.api
|
||||
.post(`/auth/account/${route}`, undefined, {
|
||||
headers: {
|
||||
"X-MFA-Ticket": ticket.token,
|
||||
},
|
||||
})
|
||||
.then(clientController.logoutCurrent),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.account.manage.title" />
|
||||
</h3>
|
||||
|
||||
<h5>
|
||||
<Text id="app.settings.pages.account.manage.description" />
|
||||
</h5>
|
||||
<CategoryButton
|
||||
icon={<Block size={24} color="var(--error)" />}
|
||||
description={
|
||||
<Text id="app.settings.pages.account.manage.disable_description" />
|
||||
}
|
||||
action="chevron"
|
||||
onClick={callback("disable")}>
|
||||
<Text id="app.settings.pages.account.manage.disable" />
|
||||
</CategoryButton>
|
||||
|
||||
<CategoryButton
|
||||
icon={<Trash size={24} color="var(--error)" />}
|
||||
description={
|
||||
<Text id="app.settings.pages.account.manage.delete_description" />
|
||||
}
|
||||
action="chevron"
|
||||
onClick={callback("delete")}>
|
||||
<Text id="app.settings.pages.account.manage.delete" />
|
||||
</CategoryButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { At } from "@styled-icons/boxicons-regular";
|
||||
import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
AccountDetail,
|
||||
CategoryButton,
|
||||
Column,
|
||||
HiddenValue,
|
||||
} from "@revoltchat/ui";
|
||||
|
||||
import { useSession } from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
export default observer(() => {
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
|
||||
const [email, setEmail] = useState("...");
|
||||
|
||||
useEffect(() => {
|
||||
if (email === "..." && session.state === "Online") {
|
||||
client.api
|
||||
.get("/auth/account/")
|
||||
.then((account) => setEmail(account.email));
|
||||
}
|
||||
}, [client, email, session.state]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column group>
|
||||
<AccountDetail user={client.user!} />
|
||||
</Column>
|
||||
|
||||
{(
|
||||
[
|
||||
[
|
||||
"username",
|
||||
client.user!.username +
|
||||
"#" +
|
||||
client.user!.discriminator,
|
||||
At,
|
||||
],
|
||||
["email", email, Envelope],
|
||||
["password", "•••••••••", Key],
|
||||
] as const
|
||||
).map(([field, value, Icon]) => (
|
||||
<CategoryButton
|
||||
key={field}
|
||||
icon={<Icon size={24} />}
|
||||
description={
|
||||
field === "email" ? (
|
||||
<HiddenValue
|
||||
value={value}
|
||||
placeholder={"•••••••••••@••••••.•••"}
|
||||
/>
|
||||
) : (
|
||||
value
|
||||
)
|
||||
}
|
||||
account
|
||||
action={<Pencil size={20} />}
|
||||
onClick={() =>
|
||||
modalController.push({
|
||||
type: "modify_account",
|
||||
client,
|
||||
field,
|
||||
})
|
||||
}>
|
||||
<Text id={`login.${field}`} />
|
||||
</CategoryButton>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import { ListOl } from "@styled-icons/boxicons-regular";
|
||||
import { Lock } from "@styled-icons/boxicons-solid";
|
||||
import { API } from "revolt.js";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
||||
|
||||
import { useSession } from "../../../controllers/client/ClientController";
|
||||
import { takeError } from "../../../controllers/client/jsx/error";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
/**
|
||||
* Temporary helper function for Axios config
|
||||
* @param token Token
|
||||
* @returns Headers
|
||||
*/
|
||||
export function toConfig(token: string) {
|
||||
return {
|
||||
headers: {
|
||||
"X-MFA-Ticket": token,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for configuring MFA on an account.
|
||||
*/
|
||||
export default function MultiFactorAuthentication() {
|
||||
// Pull in prerequisites
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
|
||||
// Keep track of MFA state
|
||||
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
// Fetch the current MFA status on account
|
||||
useEffect(() => {
|
||||
if (!mfa && session.state === "Online") {
|
||||
client!.api
|
||||
.get("/auth/mfa/")
|
||||
.then(setMFA)
|
||||
.catch((err) => setError(takeError(err)));
|
||||
}
|
||||
}, [mfa, client, session.state]);
|
||||
|
||||
// Action called when recovery code button is pressed
|
||||
const recoveryAction = useCallback(async () => {
|
||||
// Perform MFA flow first
|
||||
const ticket = await modalController.mfaFlow(client);
|
||||
|
||||
// Check whether action was cancelled
|
||||
if (typeof ticket === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decide whether to generate or fetch.
|
||||
let codes;
|
||||
if (mfa!.recovery_active) {
|
||||
// Fetch existing recovery codes
|
||||
codes = await client.api.post(
|
||||
"/auth/mfa/recovery",
|
||||
undefined,
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
} else {
|
||||
// Generate new recovery codes
|
||||
codes = await client.api.patch(
|
||||
"/auth/mfa/recovery",
|
||||
undefined,
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
|
||||
setMFA({
|
||||
...mfa!,
|
||||
recovery_active: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Display the codes to the user
|
||||
modalController.push({
|
||||
type: "mfa_recovery",
|
||||
client,
|
||||
codes,
|
||||
});
|
||||
}, [mfa]);
|
||||
|
||||
// Action called when TOTP button is pressed
|
||||
const totpAction = useCallback(async () => {
|
||||
// Perform MFA flow first
|
||||
const ticket = await modalController.mfaFlow(client);
|
||||
|
||||
// Check whether action was cancelled
|
||||
if (typeof ticket === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decide whether to disable or enable.
|
||||
if (mfa!.totp_mfa) {
|
||||
// Disable TOTP authentication
|
||||
await client.api.delete(
|
||||
"/auth/mfa/totp",
|
||||
{},
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
|
||||
setMFA({
|
||||
...mfa!,
|
||||
totp_mfa: false,
|
||||
});
|
||||
} else {
|
||||
// Generate a TOTP secret
|
||||
const { secret } = await client.api.post(
|
||||
"/auth/mfa/totp",
|
||||
undefined,
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
|
||||
// Open secret modal
|
||||
let success;
|
||||
while (!success) {
|
||||
try {
|
||||
// Make the user generator a token
|
||||
const totp_code = await modalController.mfaEnableTOTP(
|
||||
secret,
|
||||
client.user!.username,
|
||||
);
|
||||
|
||||
if (totp_code) {
|
||||
// Check whether it is valid
|
||||
await client.api.put(
|
||||
"/auth/mfa/totp",
|
||||
{
|
||||
totp_code,
|
||||
},
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
|
||||
// Mark as successful and activated
|
||||
success = true;
|
||||
|
||||
setMFA({
|
||||
...mfa!,
|
||||
totp_mfa: true,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}, [mfa]);
|
||||
|
||||
const mfaActive = !!mfa?.totp_mfa;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.account.2fa.title" />
|
||||
</h3>
|
||||
<h5>
|
||||
<Text id="app.settings.pages.account.2fa.description" />
|
||||
</h5>
|
||||
|
||||
{error && (
|
||||
<Category compact>
|
||||
<Error error={error} />
|
||||
</Category>
|
||||
)}
|
||||
|
||||
<CategoryButton
|
||||
icon={<ListOl size={24} />}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.account.2fa.${
|
||||
mfa?.recovery_active
|
||||
? "view_recovery"
|
||||
: "generate_recovery"
|
||||
}_long`}
|
||||
/>
|
||||
}
|
||||
disabled={!mfa}
|
||||
onClick={recoveryAction}>
|
||||
<Text
|
||||
id={`app.settings.pages.account.2fa.${
|
||||
mfa?.recovery_active
|
||||
? "view_recovery"
|
||||
: "generate_recovery"
|
||||
}`}
|
||||
/>
|
||||
</CategoryButton>
|
||||
<CategoryButton
|
||||
icon={
|
||||
<Lock
|
||||
size={24}
|
||||
color={!mfa?.totp_mfa ? "var(--error)" : undefined}
|
||||
/>
|
||||
}
|
||||
description={"Set up time-based one-time password."}
|
||||
disabled={!mfa || (!mfa.recovery_active && !mfa.totp_mfa)}
|
||||
onClick={totpAction}>
|
||||
<Text
|
||||
id={`app.settings.pages.account.2fa.${
|
||||
mfa?.totp_mfa ? "remove" : "add"
|
||||
}_auth`}
|
||||
/>
|
||||
</CategoryButton>
|
||||
|
||||
{mfa && (
|
||||
<Tip palette={mfaActive ? "primary" : "error"}>
|
||||
<Text
|
||||
id={`app.settings.pages.account.2fa.two_factor_${
|
||||
mfaActive ? "on" : "off"
|
||||
}`}
|
||||
/>
|
||||
</Tip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,170 +1,11 @@
|
|||
import { Pencil } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { useDebounceCallback } from "../../../lib/debounce";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { Variables } from "../../../context/Theme";
|
||||
|
||||
import InputBox from "../../ui/InputBox";
|
||||
|
||||
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,
|
||||
);
|
||||
import Overrides from "./legacy/ThemeOverrides";
|
||||
import ThemeTools from "./legacy/ThemeTools";
|
||||
|
||||
export default function ThemeOverrides() {
|
||||
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
|
||||
class="entry"
|
||||
key={key}
|
||||
style={{ backgroundColor: theme.getVariable(key) }}>
|
||||
<div class="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 class="override">
|
||||
<div
|
||||
class="picker"
|
||||
onClick={(e) =>
|
||||
e.currentTarget.parentElement?.parentElement
|
||||
?.querySelector("input")
|
||||
?.click()
|
||||
}>
|
||||
<Pencil size={24} />
|
||||
</div>
|
||||
<InputBox
|
||||
type="text"
|
||||
class="text"
|
||||
value={theme.getVariable(key)}
|
||||
onChange={(el) =>
|
||||
setVariable({
|
||||
key,
|
||||
value: el.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
<>
|
||||
<ThemeTools />
|
||||
<Overrides />
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,11 +2,12 @@ import styled from "styled-components/macro";
|
|||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { EmojiPack } from "../../common/Emoji";
|
||||
import mutantSVG from "./mutant_emoji.svg";
|
||||
import notoSVG from "./noto_emoji.svg";
|
||||
import openmojiSVG from "./openmoji_emoji.svg";
|
||||
import twemojiSVG from "./twemoji_emoji.svg";
|
||||
import mutantSVG from "./assets/mutant_emoji.svg";
|
||||
import notoSVG from "./assets/noto_emoji.svg";
|
||||
import openmojiSVG from "./assets/openmoji_emoji.svg";
|
||||
import twemojiSVG from "./assets/twemoji_emoji.svg";
|
||||
|
||||
import { EmojiPack } from "../../../common/Emoji";
|
||||
|
||||
const Container = styled.div`
|
||||
gap: 12px;
|
||||
|
|
@ -87,10 +88,10 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||
</h3>
|
||||
<Container>
|
||||
<div class="row">
|
||||
<div className="row">
|
||||
<div>
|
||||
<div
|
||||
class="button"
|
||||
className="button"
|
||||
onClick={() => setValue("mutant")}
|
||||
data-active={!value || value === "mutant"}>
|
||||
<img
|
||||
|
|
@ -112,7 +113,7 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="button"
|
||||
className="button"
|
||||
onClick={() => setValue("twemoji")}
|
||||
data-active={value === "twemoji"}>
|
||||
<img
|
||||
|
|
@ -125,10 +126,10 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||
<h4>Twemoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div className="row">
|
||||
<div>
|
||||
<div
|
||||
class="button"
|
||||
className="button"
|
||||
onClick={() => setValue("openmoji")}
|
||||
data-active={value === "openmoji"}>
|
||||
<img
|
||||
|
|
@ -142,7 +143,7 @@ export function EmojiSelector({ value, setValue }: Props) {
|
|||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="button"
|
||||
className="button"
|
||||
onClick={() => setValue("noto")}
|
||||
data-active={value === "noto"}>
|
||||
<img
|
||||
|
|
@ -0,0 +1 @@
|
|||
These components need to be ported to @revoltchat/ui.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue