mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-10-31 04:21:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6a3e775e | ||
|
|
ea7f7f3b52 | ||
|
|
75372ddf7e | ||
|
|
bed249f4d3 | ||
|
|
84b84a31fa | ||
|
|
add955d46f | ||
|
|
952a92ba1b | ||
|
|
b6d763df35 | ||
|
|
bb3f114e01 | ||
|
|
39284db8d1 | ||
|
|
52980560dd | ||
|
|
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 | ||
|
|
4541a34cef | ||
|
|
83a3585940 |
No files matched your search
@@ -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,66 +0,0 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
title: "bug: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: branch
|
||||
attributes:
|
||||
label: Branch
|
||||
description: What branch of Revolt are you using?
|
||||
options:
|
||||
- Production (app.revolt.chat)
|
||||
- Nightly (nightly.revolt.chat)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: commit-hash
|
||||
attributes:
|
||||
label: Commit hash
|
||||
description: What is your commit hash? You can find this at the bottom of Settings, next to the branch name.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other (please specify in the "What happened" form)
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. (To get this, press `CTRL`- or `CMD`-`SHIFT`-`I` and navigate to the "Console" tab.)
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
id: desktop
|
||||
attributes:
|
||||
label: Desktop
|
||||
description: Is this bug specific to [the desktop client](https://github.com/revoltchat/desktop)? (If not, leave this unchecked.)
|
||||
options:
|
||||
- label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself.
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: pwa
|
||||
attributes:
|
||||
label: PWA
|
||||
description: Is this bug specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
|
||||
options:
|
||||
- label: Yes, this bug is specific to the PWA.
|
||||
required: false
|
||||
@@ -1,7 +0,0 @@
|
||||
contact_links:
|
||||
- name: Lounge Chat
|
||||
url: https://rvlt.gg/Testers
|
||||
about: Ask questions and discuss with others.
|
||||
- name: Discussions
|
||||
url: https://github.com/orgs/revoltchat/discussions
|
||||
about: For larger feature requests and general question & answer.
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Feature request
|
||||
description: Make a feature request
|
||||
title: "feature request: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before you start, a lot of bigger features may be better suited as [API issues](https://github.com/revoltchat/delta/issues/new) or [centralised discussions](https://github.com/revoltchat/revolt/discussions/new).
|
||||
- type: textarea
|
||||
id: your-idea
|
||||
attributes:
|
||||
label: What do you want to see?
|
||||
description: Describe your idea in as much detail as possible - if applicable, screenshots/mockups are really useful.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: pwa
|
||||
attributes:
|
||||
label: PWA
|
||||
description: Is this feature request specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
|
||||
options:
|
||||
- label: Yes, this feature request is specific to the PWA.
|
||||
required: false
|
||||
@@ -1,7 +0,0 @@
|
||||
## 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 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)
|
||||
* [ ] I have included screenshots to demonstrate my changes
|
||||
@@ -1,111 +1,55 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- "!.github/workflows/preview_*.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"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "handmade"
|
||||
pull_request:
|
||||
branches:
|
||||
- "handmade"
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
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'
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
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 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: Resolve registry host
|
||||
id: registry
|
||||
run: echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: revoltchat/client, ghcr.io/revoltchat/client
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
images: ${{ steps.registry.outputs.host }}/${{ github.repository_owner }}/handmade-revolt-web-client
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name == 'release'
|
||||
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 }}
|
||||
registry: ${{ steps.registry.outputs.host }}
|
||||
username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
|
||||
password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
platforms: linux/amd64
|
||||
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,16 +0,0 @@
|
||||
name: Mirroring
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
|
||||
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,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
|
||||
+8
-4
@@ -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" ]
|
||||
@@ -1,3 +1,50 @@
|
||||
# Handmade Revolt
|
||||
Fork of Revolt (now Stoat chat) maintained by the handmade cities community.
|
||||
|
||||
## Deploying a new release (TODO: Adjust this for handmadecities)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Container image
|
||||
|
||||
Releases publish a linux/amd64 image to the Gitea package registry at
|
||||
`git.handmadecities.com/HMC/handmade-revolt-web-client` (tags: `<version>`,
|
||||
`<major>.<minor>`, and `latest`).
|
||||
|
||||
The registry requires authentication even for pulls (anonymous pull is not
|
||||
supported by Gitea). On a host that needs to pull the image, log in once with a
|
||||
**read-only** token (a token scoped to `read:package`); the credentials are
|
||||
saved to `~/.docker/config.json` (or `/root/.docker/config.json`) and persist across reboots:
|
||||
|
||||
```bash
|
||||
echo '<PACKAGE_PULL_TOKEN>' | docker login git.handmadecities.com -u <pull-username> --password-stdin
|
||||
|
||||
docker pull git.handmadecities.com/HMC/handmade-revolt-web-client:latest
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `~/.docker/config.json` is per-user so you'll need to log in separately from all users that will need to pull images.
|
||||
- Credentials are stored base64-encoded, not encrypted; keep the file at `chmod 600` and use a `read:package`-only token so a leak can't push.
|
||||
- Rotate by running `docker login` again, or `docker logout git.handmadecities.com` to remove the stored credential.
|
||||
|
||||
# Revite
|
||||
|
||||
## Description
|
||||
@@ -10,8 +57,6 @@ The following code is pending a partial or full rewrite:
|
||||
|
||||
- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||
- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||
- `src/context/intermediate`: modal system is being rewritten from scratch
|
||||
- `src/context/revoltjs`: client state management needs to be rewritten and include support for concurrent clients
|
||||
- `src/lib`: this needs to be organised
|
||||
|
||||
## Stack
|
||||
@@ -45,6 +90,7 @@ Get revite up and running locally.
|
||||
git clone --recursive https://github.com/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
yarn dev
|
||||
```
|
||||
|
||||
@@ -57,6 +103,7 @@ You can now access the client at http://local.revolt.chat:3000.
|
||||
| `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. |
|
||||
|
||||
@@ -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==
|
||||
+1
Submodule external/components added at 4be02430c7.
Vendored
+1
-1
Submodule external/lang updated: 50838167d7...788261ef41.
+1
Submodule external/revolt.js added at a45710f80c.
Generated
+64
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
+36
-14
@@ -3,7 +3,8 @@
|
||||
"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}",
|
||||
@@ -43,13 +44,17 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -70,25 +75,25 @@
|
||||
"@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.46",
|
||||
"@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",
|
||||
@@ -106,15 +111,16 @@
|
||||
"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",
|
||||
@@ -122,7 +128,7 @@
|
||||
"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",
|
||||
@@ -132,17 +138,29 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scroll": "^1.8.2",
|
||||
"react-virtuoso": "^2.12.0",
|
||||
"revolt.js": "6.0.3",
|
||||
"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",
|
||||
@@ -150,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"
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+10
-5
@@ -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
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
type Element =
|
||||
| string
|
||||
| {
|
||||
type: "image";
|
||||
src: string;
|
||||
};
|
||||
|
||||
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!",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const changelogEntryArray = Object.keys(changelogEntries).map(
|
||||
(index) => changelogEntries[index as unknown as number],
|
||||
);
|
||||
|
||||
export const latestChangelog = changelogEntryArray.length;
|
||||
@@ -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;
|
||||
+105
-105
@@ -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",
|
||||
// },
|
||||
};
|
||||
@@ -4,9 +4,9 @@ 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, Checkbox } from "@revoltchat/ui";
|
||||
import { Button, Checkbox, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SECTION_NSFW } from "../../mobx/stores/Layout";
|
||||
@@ -45,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" ||
|
||||
@@ -76,23 +98,40 @@ export default observer((props: Props) => {
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<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>
|
||||
{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] === "@") {
|
||||
@@ -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,10 +245,13 @@ 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") {
|
||||
@@ -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,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}`;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Text } from "preact-i18n";
|
||||
|
||||
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;
|
||||
|
||||
@@ -13,9 +13,8 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||
|
||||
import { I18nError } from "../../../context/Locale";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import Markdown from "../../markdown/Markdown";
|
||||
import 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,7 +172,8 @@ const Message = observer(
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{replacement ?? <Markdown content={content} />}
|
||||
{replacement ??
|
||||
(content && <Markdown content={content} />)}
|
||||
{!queued && <InviteList message={message} />}
|
||||
{queued?.error && (
|
||||
<Category>
|
||||
@@ -179,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,4 +1,4 @@
|
||||
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||
import { HappyBeaming, Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||
import Axios, { CancelTokenSource } from "axios";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Channel } from "revolt.js";
|
||||
@@ -6,9 +6,10 @@ 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 } from "@revoltchat/ui";
|
||||
import { IconButton, Picker } from "@revoltchat/ui";
|
||||
|
||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { debounce } from "../../../lib/debounce";
|
||||
@@ -21,18 +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 { modalController } from "../../../context/modals";
|
||||
import { dayjs } from "../../../context/Locale";
|
||||
|
||||
import { emojiDictionary } from "../../../assets/emojis";
|
||||
import {
|
||||
clientController,
|
||||
useClient,
|
||||
} from "../../../controllers/client/ClientController";
|
||||
import { takeError } from "../../../controllers/client/jsx/error";
|
||||
import {
|
||||
FileUploader,
|
||||
grabFiles,
|
||||
uploadFile,
|
||||
} from "../../../context/revoltjs/FileUploads";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../context/revoltjs/util";
|
||||
|
||||
} 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";
|
||||
@@ -127,6 +135,10 @@ const FileAction = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const FloatingLayer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
|
||||
width: 16px;
|
||||
`;
|
||||
@@ -137,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;
|
||||
|
||||
@@ -148,11 +221,42 @@ export default observer(({ channel }: Props) => {
|
||||
});
|
||||
const [typing, setTyping] = useState<boolean | number>(false);
|
||||
const [replies, setReplies] = useState<Reply[]>([]);
|
||||
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>
|
||||
@@ -174,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],
|
||||
);
|
||||
|
||||
@@ -196,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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,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");
|
||||
@@ -297,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;
|
||||
@@ -422,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;
|
||||
@@ -498,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>
|
||||
@@ -546,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") {
|
||||
@@ -618,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_"));
|
||||
|
||||
|
||||
@@ -15,19 +15,19 @@ import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { determineFileSize } from "../../../../lib/fileSize";
|
||||
|
||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
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,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,357 @@
|
||||
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 { modalController } from "../../../../controllers/modals/ModalController";
|
||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||
import Tooltip from "../../Tooltip";
|
||||
import UserIcon from "../../user/UserIcon";
|
||||
import { Username } from "../../user/UserShort";
|
||||
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);
|
||||
`}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Tooltip content for reaction users
|
||||
*/
|
||||
const TooltipUserList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
max-width: 200px;
|
||||
`;
|
||||
|
||||
const TooltipUserRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9em;
|
||||
padding: 4px 6px;
|
||||
margin: -4px -6px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
`;
|
||||
|
||||
const TooltipMoreText = styled.div`
|
||||
font-size: 0.85em;
|
||||
color: var(--secondary-foreground);
|
||||
padding-top: 2px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const userIds = user_ids ? Array.from(user_ids) : [];
|
||||
const longPressTimer = useRef<number | null>(null);
|
||||
const didLongPress = useRef(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (didLongPress.current) {
|
||||
didLongPress.current = false;
|
||||
return;
|
||||
}
|
||||
active ? message.unreact(id) : message.react(id);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
modalController.push({
|
||||
type: "reaction_users",
|
||||
emoji: id,
|
||||
userIds,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
didLongPress.current = false;
|
||||
longPressTimer.current = window.setTimeout(() => {
|
||||
didLongPress.current = true;
|
||||
openModal();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e: TouchEvent) => {
|
||||
e.stopPropagation();
|
||||
if (longPressTimer.current) {
|
||||
clearTimeout(longPressTimer.current);
|
||||
longPressTimer.current = null;
|
||||
// Short tap - trigger manually since preventDefault blocks click
|
||||
if (!didLongPress.current) {
|
||||
active ? message.unreact(id) : message.react(id);
|
||||
}
|
||||
}
|
||||
didLongPress.current = false;
|
||||
};
|
||||
|
||||
const openUserProfile = (userId: string) => {
|
||||
modalController.push({ type: "user_profile", user_id: userId });
|
||||
};
|
||||
|
||||
// Build tooltip content showing up to 10 users
|
||||
const tooltipContent = (
|
||||
<TooltipUserList>
|
||||
{userIds.slice(0, 10).map((userId) => {
|
||||
const user = client.users.get(userId);
|
||||
return (
|
||||
<TooltipUserRow
|
||||
key={userId}
|
||||
onClick={() => openUserProfile(userId)}>
|
||||
<UserIcon target={user} size={18} />
|
||||
<Username user={user} />
|
||||
</TooltipUserRow>
|
||||
);
|
||||
})}
|
||||
{userIds.length > 10 && (
|
||||
<TooltipMoreText>
|
||||
and {userIds.length - 10} more...
|
||||
</TooltipMoreText>
|
||||
)}
|
||||
</TooltipUserList>
|
||||
);
|
||||
|
||||
const reactionElement = (
|
||||
<Reaction
|
||||
active={active}
|
||||
onClick={handleClick}
|
||||
onTouchStart={isTouchscreenDevice ? handleTouchStart : undefined}
|
||||
onTouchEnd={isTouchscreenDevice ? handleTouchEnd : undefined}
|
||||
onTouchCancel={isTouchscreenDevice ? handleTouchEnd : undefined}>
|
||||
<RenderEmoji match={id} /> {user_ids?.size || 0}
|
||||
</Reaction>
|
||||
);
|
||||
|
||||
// Desktop: show tooltip on hover
|
||||
// Mobile: long-press opens modal, handled by touch events
|
||||
if (!isTouchscreenDevice && userIds.length > 0) {
|
||||
return (
|
||||
<Tooltip content={tooltipContent} delay={300} interactive>
|
||||
{reactionElement}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return reactionElement;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Determine two lists of 'required' and 'optional' reactions
|
||||
*/
|
||||
const { required, optional } = (() => {
|
||||
const required = new Set<string>();
|
||||
const optional = new Set<string>();
|
||||
|
||||
if (message.interactions?.reactions) {
|
||||
for (const reaction of message.interactions.reactions) {
|
||||
required.add(reaction);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of message.reactions.keys()) {
|
||||
if (!required.has(key)) {
|
||||
optional.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
required,
|
||||
optional,
|
||||
};
|
||||
})();
|
||||
|
||||
// Don't render list if nothing is going to show anyways
|
||||
if (required.size === 0 && optional.size === 0) return null;
|
||||
|
||||
return (
|
||||
<List>
|
||||
{Array.from(required, (id) => (
|
||||
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
|
||||
))}
|
||||
{required.size !== 0 && optional.size !== 0 && <Divider />}
|
||||
{Array.from(optional, (id) => (
|
||||
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
|
||||
))}
|
||||
{message.channel?.havePermission("React") && (
|
||||
<ReactionWrapper
|
||||
message={message}
|
||||
open={showPicker}
|
||||
setOpen={setPicker}>
|
||||
<IconButton className={showPicker ? "" : "add"}>
|
||||
<Plus size={20} />
|
||||
</IconButton>
|
||||
</ReactionWrapper>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
});
|
||||
|
||||
const Base = styled.div`
|
||||
> div {
|
||||
position: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* ! FIXME: rewrite
|
||||
*/
|
||||
export const ReactionWrapper: React.FC<{
|
||||
message: Message;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
}> = ({ open, setOpen, message, children }) => {
|
||||
const { x, y, reference, floating, strategy } = useFloating({
|
||||
open,
|
||||
middleware: [
|
||||
offset(4),
|
||||
shift({ mainAxis: true, crossAxis: true, padding: 4 }),
|
||||
autoPlacement(),
|
||||
],
|
||||
});
|
||||
|
||||
const skip = useRef();
|
||||
const toggle = () => {
|
||||
if (skip.current) {
|
||||
skip.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(!open);
|
||||
|
||||
if (!open) {
|
||||
skip.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={reference}
|
||||
onClick={toggle}
|
||||
style={{ width: "fit-content" }}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<div id="reaction">
|
||||
{open && (
|
||||
<Base
|
||||
ref={floating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
}}>
|
||||
<HackAlertThisFileWillBeReplaced
|
||||
onSelect={(emoji) =>
|
||||
message.react(
|
||||
emojiDictionary[
|
||||
emoji as keyof typeof emojiDictionary
|
||||
] ?? emoji,
|
||||
)
|
||||
}
|
||||
onClose={toggle}
|
||||
/>
|
||||
</Base>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -3,15 +3,12 @@ import { API } from "revolt.js";
|
||||
|
||||
import styles from "./Attachment.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
StatusContext,
|
||||
} from "../../../../context/revoltjs/RevoltClient";
|
||||
import { useClient } from "../../../../controllers/client/ClientController";
|
||||
import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline";
|
||||
|
||||
interface Props {
|
||||
attachment: API.File;
|
||||
@@ -23,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(() => {
|
||||
@@ -56,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -185,13 +185,15 @@ 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>
|
||||
|
||||
@@ -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";
|
||||
@@ -12,14 +11,13 @@ import { Button, Category, Preloader } from "@revoltchat/ui";
|
||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { I18nError } from "../../../../context/Locale";
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../../context/revoltjs/util";
|
||||
|
||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||
import {
|
||||
useClient,
|
||||
useSession,
|
||||
} from "../../../../controllers/client/ClientController";
|
||||
import { takeError } from "../../../../controllers/client/jsx/error";
|
||||
|
||||
const EmbedInviteBase = styled.div`
|
||||
width: 400px;
|
||||
@@ -78,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);
|
||||
@@ -90,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)
|
||||
@@ -99,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 ? (
|
||||
@@ -128,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 ? (
|
||||
|
||||
@@ -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!,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -11,8 +11,7 @@ import { Header, IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import Tooltip from "../Tooltip";
|
||||
import UserStatus from "./UserStatus";
|
||||
|
||||
@@ -30,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;
|
||||
}
|
||||
|
||||
@@ -48,17 +52,22 @@ interface Props {
|
||||
}
|
||||
|
||||
export default observer(({ user }: Props) => {
|
||||
const { writeClipboard } = useIntermediate();
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
@@ -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")
|
||||
@@ -56,7 +58,7 @@ export default observer(
|
||||
keyof Props | "children" | "as"
|
||||
>,
|
||||
) => {
|
||||
const client = useApplicationState().client!;
|
||||
const client = useClient();
|
||||
|
||||
const {
|
||||
target,
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,294 +0,0 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import MarkdownKatex from "@traptitech/markdown-it-katex";
|
||||
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
|
||||
import "katex/dist/katex.min.css";
|
||||
import MarkdownIt from "markdown-it";
|
||||
// @ts-expect-error No typings.
|
||||
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
|
||||
import { RE_MENTIONS } from "revolt.js";
|
||||
|
||||
import styles from "./Markdown.module.scss";
|
||||
import { useCallback, useContext } from "preact/hooks";
|
||||
|
||||
import { internalEmit } from "../../lib/eventEmitter";
|
||||
import { determineLink } from "../../lib/links";
|
||||
|
||||
import { dayjs } from "../../context/Locale";
|
||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { emojiDictionary } from "../../assets/emojis";
|
||||
import { generateEmoji } from "../common/Emoji";
|
||||
import { MarkdownProps } from "./Markdown";
|
||||
import Prism from "./prism";
|
||||
|
||||
// TODO: global.d.ts file for defining globals
|
||||
declare global {
|
||||
interface Window {
|
||||
copycode: (element: HTMLDivElement) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for code block copy.
|
||||
if (typeof window !== "undefined") {
|
||||
window.copycode = function (element: HTMLDivElement) {
|
||||
try {
|
||||
const code = element.parentElement?.parentElement?.children[1];
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code.textContent?.trim() ?? "");
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
}
|
||||
|
||||
export const md: MarkdownIt = MarkdownIt({
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
highlight: (str, lang) => {
|
||||
const v = Prism.languages[lang];
|
||||
if (v) {
|
||||
const out = Prism.highlight(str, v, lang);
|
||||
return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
|
||||
}
|
||||
|
||||
return `<pre class="code"><code>${md.utils.escapeHtml(
|
||||
str,
|
||||
)}</code></pre>`;
|
||||
},
|
||||
})
|
||||
.disable("image")
|
||||
.use(MarkdownEmoji, { defs: emojiDictionary })
|
||||
.use(MarkdownSpoilers)
|
||||
.use(MarkdownKatex, {
|
||||
throwOnError: false,
|
||||
maxExpand: 0,
|
||||
maxSize: 10,
|
||||
strict: false,
|
||||
errorColor: "var(--error)",
|
||||
});
|
||||
|
||||
md.linkify.set({ fuzzyLink: false });
|
||||
|
||||
// TODO: global.d.ts file for defining globals
|
||||
declare global {
|
||||
interface Window {
|
||||
internalHandleURL: (element: HTMLAnchorElement) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Include emojis.
|
||||
md.renderer.rules.emoji = function (token, idx) {
|
||||
return generateEmoji(token[idx].content);
|
||||
};
|
||||
|
||||
// Force line breaks.
|
||||
// https://github.com/markdown-it/markdown-it/issues/211#issuecomment-508380611
|
||||
const defaultParagraphRenderer =
|
||||
md.renderer.rules.paragraph_open ||
|
||||
((tokens, idx, options, env, self) =>
|
||||
self.renderToken(tokens, idx, options));
|
||||
|
||||
md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) {
|
||||
let result = "";
|
||||
if (idx > 1) {
|
||||
const inline = tokens[idx - 2];
|
||||
const paragraph = tokens[idx];
|
||||
if (
|
||||
inline.type === "inline" &&
|
||||
inline.map &&
|
||||
inline.map[1] &&
|
||||
paragraph.map &&
|
||||
paragraph.map[0]
|
||||
) {
|
||||
const diff = paragraph.map[0] - inline.map[1];
|
||||
if (diff > 0) {
|
||||
result = "<br>".repeat(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result + defaultParagraphRenderer(tokens, idx, options, env, self);
|
||||
};
|
||||
|
||||
const RE_TWEMOJI = /:(\w+):/g;
|
||||
|
||||
// ! FIXME: Move to library
|
||||
const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
|
||||
|
||||
const RE_TIME = /<t:([0-9]+):(\w)>/g;
|
||||
|
||||
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||
const client = useContext(AppContext);
|
||||
const { openLink } = useIntermediate();
|
||||
|
||||
if (typeof content === "undefined") return null;
|
||||
if (!content || content.length === 0) return null;
|
||||
|
||||
// We replace the message with the mention at the time of render.
|
||||
// We don't care if the mention changes.
|
||||
const newContent = content
|
||||
.replace(RE_TIME, (sub: string, ...args: unknown[]) => {
|
||||
if (isNaN(args[0] as number)) return sub;
|
||||
const date = dayjs.unix(args[0] as number);
|
||||
const format = args[1] as string;
|
||||
let final = "";
|
||||
switch (format) {
|
||||
case "t":
|
||||
final = date.format("hh:mm");
|
||||
break;
|
||||
case "T":
|
||||
final = date.format("hh:mm:ss");
|
||||
break;
|
||||
case "R":
|
||||
final = date.fromNow();
|
||||
break;
|
||||
case "D":
|
||||
final = date.format("DD MMMM YYYY");
|
||||
break;
|
||||
case "F":
|
||||
final = date.format("dddd, DD MMMM YYYY hh:mm");
|
||||
break;
|
||||
default:
|
||||
final = date.format("DD MMMM YYYY hh:mm");
|
||||
break;
|
||||
}
|
||||
return `\`${final}\``;
|
||||
})
|
||||
.replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
|
||||
const id = args[0] as string,
|
||||
user = client.users.get(id);
|
||||
|
||||
if (user) {
|
||||
return `[@${user.username}](/@${id})`;
|
||||
}
|
||||
|
||||
return sub;
|
||||
})
|
||||
.replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
|
||||
const id = args[0] as string,
|
||||
channel = client.channels.get(id);
|
||||
|
||||
if (
|
||||
channel?.channel_type === "TextChannel" ||
|
||||
channel?.channel_type === "VoiceChannel"
|
||||
) {
|
||||
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
|
||||
}
|
||||
|
||||
return sub;
|
||||
});
|
||||
|
||||
const useLargeEmojis = disallowBigEmoji
|
||||
? false
|
||||
: content.replace(RE_TWEMOJI, "").trim().length === 0;
|
||||
|
||||
const toggle = useCallback((ev: MouseEvent) => {
|
||||
if (ev.currentTarget) {
|
||||
const element = ev.currentTarget as HTMLDivElement;
|
||||
if (element.classList.contains("spoiler")) {
|
||||
element.classList.add("shown");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLink = useCallback(
|
||||
(ev: MouseEvent) => {
|
||||
if (ev.currentTarget) {
|
||||
const element = ev.currentTarget as HTMLAnchorElement;
|
||||
|
||||
if (ev.shiftKey) {
|
||||
switch (element.dataset.type) {
|
||||
case "mention": {
|
||||
internalEmit(
|
||||
"MessageBox",
|
||||
"append",
|
||||
`<@${element.dataset.mentionId}>`,
|
||||
"mention",
|
||||
);
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
case "channel_mention": {
|
||||
internalEmit(
|
||||
"MessageBox",
|
||||
"append",
|
||||
`<#${element.dataset.mentionId}>`,
|
||||
"channel_mention",
|
||||
);
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (openLink(element.href)) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
[openLink],
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
el.querySelectorAll<HTMLDivElement>(".spoiler").forEach(
|
||||
(element) => {
|
||||
element.removeEventListener("click", toggle);
|
||||
element.addEventListener("click", toggle);
|
||||
},
|
||||
);
|
||||
|
||||
el.querySelectorAll<HTMLAnchorElement>("a").forEach(
|
||||
(element) => {
|
||||
element.removeEventListener("click", handleLink);
|
||||
element.addEventListener("click", handleLink);
|
||||
element.removeAttribute("data-type");
|
||||
element.removeAttribute("data-mention-id");
|
||||
element.removeAttribute("target");
|
||||
|
||||
const link = determineLink(element.href);
|
||||
switch (link.type) {
|
||||
case "profile": {
|
||||
element.setAttribute(
|
||||
"data-type",
|
||||
"mention",
|
||||
);
|
||||
element.setAttribute(
|
||||
"data-mention-id",
|
||||
link.id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "navigate": {
|
||||
if (link.navigation_type === "channel") {
|
||||
element.setAttribute(
|
||||
"data-type",
|
||||
"channel_mention",
|
||||
);
|
||||
element.setAttribute(
|
||||
"data-mention-id",
|
||||
link.channel_id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "external": {
|
||||
element.setAttribute("target", "_blank");
|
||||
element.setAttribute("rel", "noreferrer");
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
className={styles.markdown}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: md.render(newContent),
|
||||
}}
|
||||
data-large-emojis={useLargeEmojis}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -9,8 +9,7 @@ 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";
|
||||
|
||||
const Base = styled.div`
|
||||
|
||||
@@ -13,8 +13,7 @@ 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";
|
||||
@@ -50,7 +49,6 @@ export const UserButton = observer((props: UserProps) => {
|
||||
channel,
|
||||
...divProps
|
||||
} = props;
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -111,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,
|
||||
})
|
||||
@@ -149,7 +146,6 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
||||
return <UserButton {...{ active, alert, channel, user }} />;
|
||||
}
|
||||
|
||||
const { openScreen } = useIntermediate();
|
||||
const alerting = alert && !muted && !active;
|
||||
|
||||
return (
|
||||
@@ -159,7 +155,9 @@ 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,
|
||||
@@ -179,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>
|
||||
@@ -195,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 { Banner } from "@revoltchat/ui";
|
||||
import { Banner, Button, Column } from "@revoltchat/ui";
|
||||
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
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);
|
||||
@@ -20,11 +20,10 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
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";
|
||||
@@ -46,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) =>
|
||||
@@ -131,8 +129,7 @@ export default observer(() => {
|
||||
<Text id="app.main.categories.conversations" />
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
modalController.push({
|
||||
type: "create_group",
|
||||
})
|
||||
}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -14,8 +14,7 @@ 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 { ChannelButton } from "../items/ButtonItem";
|
||||
@@ -51,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="/" />;
|
||||
@@ -120,14 +121,18 @@ export default observer(() => {
|
||||
channels.push(addChannel(id));
|
||||
}
|
||||
|
||||
elements.push(
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category>{category.title}</Category>}>
|
||||
{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" rel="noreferrer">
|
||||
#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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,8 +7,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { useClient } from "../../../controllers/client/ClientController";
|
||||
import Message from "../../common/messaging/Message";
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@ import { Block } from "@styled-icons/boxicons-regular";
|
||||
import { Trash } from "@styled-icons/boxicons-solid";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { CategoryButton } from "@revoltchat/ui";
|
||||
|
||||
import { modalController } from "../../../context/modals";
|
||||
import {
|
||||
LogOutContext,
|
||||
clientController,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
} from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
export default function AccountManagement() {
|
||||
const logOut = useContext(LogOutContext);
|
||||
const client = useClient();
|
||||
|
||||
const callback = (route: "disable" | "delete") => () =>
|
||||
@@ -26,7 +24,7 @@ export default function AccountManagement() {
|
||||
"X-MFA-Ticket": ticket.token,
|
||||
},
|
||||
})
|
||||
.then(() => logOut(true)),
|
||||
.then(clientController.logoutCurrent),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -41,7 +39,7 @@ export default function AccountManagement() {
|
||||
<CategoryButton
|
||||
icon={<Block size={24} color="var(--error)" />}
|
||||
description={
|
||||
"Disable your account. You won't be able to access it unless you contact support."
|
||||
<Text id="app.settings.pages.account.manage.disable_description" />
|
||||
}
|
||||
action="chevron"
|
||||
onClick={callback("disable")}>
|
||||
@@ -51,7 +49,7 @@ export default function AccountManagement() {
|
||||
<CategoryButton
|
||||
icon={<Trash size={24} color="var(--error)" />}
|
||||
description={
|
||||
"Your account will be queued for deletion, a confirmation email will be sent."
|
||||
<Text id="app.settings.pages.account.manage.delete_description" />
|
||||
}
|
||||
action="chevron"
|
||||
onClick={callback("delete")}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
AccountDetail,
|
||||
@@ -12,26 +12,22 @@ import {
|
||||
HiddenValue,
|
||||
} from "@revoltchat/ui";
|
||||
|
||||
import { modalController } from "../../../context/modals";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { useSession } from "../../../controllers/client/ClientController";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
export default observer(() => {
|
||||
const client = useClient();
|
||||
const status = useContext(StatusContext);
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
|
||||
const [email, setEmail] = useState("...");
|
||||
|
||||
useEffect(() => {
|
||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
||||
if (email === "..." && session.state === "Online") {
|
||||
client.api
|
||||
.get("/auth/account/")
|
||||
.then((account) => setEmail(account.email));
|
||||
}
|
||||
}, [client, email, status]);
|
||||
}, [client, email, session.state]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -41,7 +37,13 @@ export default observer(() => {
|
||||
|
||||
{(
|
||||
[
|
||||
["username", client.user!.username, At],
|
||||
[
|
||||
"username",
|
||||
client.user!.username +
|
||||
"#" +
|
||||
client.user!.discriminator,
|
||||
At,
|
||||
],
|
||||
["email", email, Envelope],
|
||||
["password", "•••••••••", Key],
|
||||
] as const
|
||||
|
||||
@@ -3,17 +3,13 @@ import { Lock } from "@styled-icons/boxicons-solid";
|
||||
import { API } from "revolt.js";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
||||
|
||||
import { modalController } from "../../../context/modals";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../context/revoltjs/util";
|
||||
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
|
||||
@@ -33,8 +29,8 @@ export function toConfig(token: string) {
|
||||
*/
|
||||
export default function MultiFactorAuthentication() {
|
||||
// Pull in prerequisites
|
||||
const client = useClient();
|
||||
const status = useContext(StatusContext);
|
||||
const session = useSession()!;
|
||||
const client = session.client!;
|
||||
|
||||
// Keep track of MFA state
|
||||
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
||||
@@ -42,13 +38,13 @@ export default function MultiFactorAuthentication() {
|
||||
|
||||
// Fetch the current MFA status on account
|
||||
useEffect(() => {
|
||||
if (!mfa && status === ClientStatus.ONLINE) {
|
||||
client.api
|
||||
if (!mfa && session.state === "Online") {
|
||||
client!.api
|
||||
.get("/auth/mfa/")
|
||||
.then(setMFA)
|
||||
.catch((err) => setError(takeError(err)));
|
||||
}
|
||||
}, [client, mfa, status]);
|
||||
}, [mfa, client, session.state]);
|
||||
|
||||
// Action called when recovery code button is pressed
|
||||
const recoveryAction = useCallback(async () => {
|
||||
@@ -104,7 +100,11 @@ export default function MultiFactorAuthentication() {
|
||||
// Decide whether to disable or enable.
|
||||
if (mfa!.totp_mfa) {
|
||||
// Disable TOTP authentication
|
||||
await client.api.delete("/auth/mfa/totp", toConfig(ticket.token));
|
||||
await client.api.delete(
|
||||
"/auth/mfa/totp",
|
||||
{},
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
|
||||
setMFA({
|
||||
...mfa!,
|
||||
|
||||
@@ -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,290 +0,0 @@
|
||||
import { Brush } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Link } from "react-router-dom";
|
||||
// @ts-expect-error shade-blend-color does not have typings.
|
||||
import pSBC from "shade-blend-color";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import {
|
||||
CategoryButton,
|
||||
Checkbox,
|
||||
ColourSwatches,
|
||||
ComboBox,
|
||||
Radio,
|
||||
} from "@revoltchat/ui";
|
||||
|
||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import {
|
||||
Fonts,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MonospaceFonts,
|
||||
MONOSPACE_FONTS,
|
||||
MONOSPACE_FONT_KEYS,
|
||||
} from "../../../context/Theme";
|
||||
|
||||
import { EmojiSelector } from "./EmojiSelector";
|
||||
import { ThemeBaseSelector } from "./ThemeBaseSelector";
|
||||
|
||||
/**
|
||||
* Component providing a way to switch the base theme being used.
|
||||
*/
|
||||
export const ShimThemeBaseSelector = observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
return (
|
||||
<ThemeBaseSelector
|
||||
value={theme.isModified() ? undefined : theme.getBase()}
|
||||
setValue={(base) => {
|
||||
theme.setBase(base);
|
||||
theme.reset();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a link to the theme shop.
|
||||
* Only appears if experiment is enabled.
|
||||
*/
|
||||
export const ShimThemeShop = () => {
|
||||
return (
|
||||
<Link to="/discover/themes" replace>
|
||||
<CategoryButton
|
||||
icon={<Brush size={24} />}
|
||||
action="chevron"
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.discover.description" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.discover.title" />
|
||||
</CategoryButton>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component providing a way to change current accent colour.
|
||||
*/
|
||||
export const ShimThemeAccent = observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
</h3>
|
||||
<ColourSwatches
|
||||
value={theme.getVariable("accent")}
|
||||
onChange={(colour) => {
|
||||
theme.setVariable("accent", colour as string);
|
||||
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to edit custom CSS.
|
||||
*/
|
||||
export const ShimThemeCustomCSS = observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.custom_css" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={20}
|
||||
minHeight={480}
|
||||
code
|
||||
value={theme.getCSS() ?? ""}
|
||||
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to switch between compact and normal message view.
|
||||
*/
|
||||
export const ShimDisplayCompact = () => {
|
||||
// TODO: WIP feature
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.message_display" />
|
||||
</h3>
|
||||
<div /* className={styles.display} */>
|
||||
<Radio
|
||||
title={
|
||||
<Text id="app.settings.pages.appearance.display.default" />
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.display.default_description" />
|
||||
}
|
||||
value={true}
|
||||
/>
|
||||
<Radio
|
||||
title={
|
||||
<Text id="app.settings.pages.appearance.display.compact" />
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.display.compact_description" />
|
||||
}
|
||||
value={false}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component providing a way to change primary text font.
|
||||
*/
|
||||
export const ShimDisplayFont = observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.getFont()}
|
||||
onChange={(e) => theme.setFont(e.currentTarget.value as Fonts)}>
|
||||
{FONT_KEYS.map((key) => (
|
||||
<option value={key} key={key}>
|
||||
{FONTS[key as keyof typeof FONTS].name}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to change secondary, monospace text font.
|
||||
*/
|
||||
export const ShimDisplayMonospaceFont = observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.mono_font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.getMonospaceFont()}
|
||||
onChange={(e) =>
|
||||
theme.setMonospaceFont(
|
||||
e.currentTarget.value as MonospaceFonts,
|
||||
)
|
||||
}>
|
||||
{MONOSPACE_FONT_KEYS.map((key) => (
|
||||
<option value={key} key={key}>
|
||||
{
|
||||
MONOSPACE_FONTS[key as keyof typeof MONOSPACE_FONTS]
|
||||
.name
|
||||
}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle font ligatures.
|
||||
*/
|
||||
export const ShimDisplayLigatures = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
if (settings.theme.getFont() !== "Inter") return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
value={settings.get("appearance:ligatures") ?? false}
|
||||
onChange={(v) => settings.set("appearance:ligatures", v)}
|
||||
title={<Text id="app.settings.pages.appearance.ligatures" />}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle showing the send button on desktop.
|
||||
*/
|
||||
export const ShimShowSendButton = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
value={settings.get("appearance:show_send_button") ?? false}
|
||||
onChange={(v) => settings.set("appearance:show_send_button", v)}
|
||||
title={
|
||||
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle seasonal themes.
|
||||
*/
|
||||
export const ShimDisplaySeasonal = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
value={settings.get("appearance:seasonal") ?? true}
|
||||
onChange={(v) => settings.set("appearance:seasonal", v)}
|
||||
title={
|
||||
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to toggle transparency effects.
|
||||
*/
|
||||
export const ShimDisplayTransparency = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
value={settings.get("appearance:transparency") ?? true}
|
||||
onChange={(v) => settings.set("appearance:transparency", v)}
|
||||
title={
|
||||
<Text id="app.settings.pages.appearance.theme_options.transparency" />
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Component providing a way to change emoji pack.
|
||||
*/
|
||||
export const ShimDisplayEmoji = observer(() => {
|
||||
const settings = useApplicationState().settings;
|
||||
return (
|
||||
<EmojiSelector
|
||||
value={settings.get("appearance:emoji")}
|
||||
setValue={(v) => settings.set("appearance:emoji", v)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1,170 +1,11 @@
|
||||
import { Pencil } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { InputBox } from "@revoltchat/ui";
|
||||
|
||||
import { useDebounceCallback } from "../../../lib/debounce";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { Variables } from "../../../context/Theme";
|
||||
|
||||
const Container = styled.div`
|
||||
row-gap: 8px;
|
||||
display: grid;
|
||||
column-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
margin-bottom: 20px;
|
||||
|
||||
.entry {
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid black;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 8px;
|
||||
text-transform: capitalize;
|
||||
|
||||
background: inherit;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
.override {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
|
||||
.picker {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
const setVariable = useDebounceCallback(
|
||||
(data) => {
|
||||
const { key, value } = data as { key: Variables; value: string };
|
||||
theme.setVariable(key, value);
|
||||
},
|
||||
[theme],
|
||||
100,
|
||||
);
|
||||
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
|
||||
className="entry"
|
||||
key={key}
|
||||
style={{ backgroundColor: theme.getVariable(key) }}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="color"
|
||||
value={theme.getVariable(key)}
|
||||
onChange={(el) =>
|
||||
setVariable({
|
||||
key,
|
||||
value: el.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
color: theme.getContrastingVariable(
|
||||
key,
|
||||
theme.getVariable("primary-background"),
|
||||
),
|
||||
}}>
|
||||
{key}
|
||||
</span>
|
||||
<div className="override">
|
||||
<div
|
||||
className="picker"
|
||||
onClick={(e) =>
|
||||
e.currentTarget.parentElement?.parentElement
|
||||
?.querySelector("input")
|
||||
?.click()
|
||||
}>
|
||||
<Pencil size={24} />
|
||||
</div>
|
||||
<InputBox
|
||||
type="text"
|
||||
className="text"
|
||||
value={theme.getVariable(key)}
|
||||
onChange={(el) =>
|
||||
setVariable({
|
||||
key,
|
||||
value: el.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
<>
|
||||
<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));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ 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";
|
||||
import { EmojiPack } from "../../../common/Emoji";
|
||||
|
||||
const Container = styled.div`
|
||||
gap: 12px;
|
||||
@@ -0,0 +1 @@
|
||||
These components need to be ported to @revoltchat/ui.
|
||||
File renamed without changes.
@@ -0,0 +1,170 @@
|
||||
import { Pencil } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components/macro";
|
||||
|
||||
import { InputBox } from "@revoltchat/ui";
|
||||
|
||||
import { useDebounceCallback } from "../../../../lib/debounce";
|
||||
|
||||
import { useApplicationState } from "../../../../mobx/State";
|
||||
|
||||
import { Variables } from "../../../../context/Theme";
|
||||
|
||||
const Container = styled.div`
|
||||
row-gap: 8px;
|
||||
display: grid;
|
||||
column-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
margin-bottom: 20px;
|
||||
|
||||
.entry {
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid black;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 8px;
|
||||
text-transform: capitalize;
|
||||
|
||||
background: inherit;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
.override {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
|
||||
.picker {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(() => {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
const setVariable = useDebounceCallback(
|
||||
(data) => {
|
||||
const { key, value } = data as { key: Variables; value: string };
|
||||
theme.setVariable(key, value);
|
||||
},
|
||||
[theme],
|
||||
100,
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{(
|
||||
[
|
||||
"accent",
|
||||
"background",
|
||||
"foreground",
|
||||
"primary-background",
|
||||
"primary-header",
|
||||
"secondary-background",
|
||||
"secondary-foreground",
|
||||
"secondary-header",
|
||||
"tertiary-background",
|
||||
"tertiary-foreground",
|
||||
"block",
|
||||
"message-box",
|
||||
"mention",
|
||||
"scrollbar-thumb",
|
||||
"scrollbar-track",
|
||||
"status-online",
|
||||
"status-away",
|
||||
"status-busy",
|
||||
"status-streaming",
|
||||
"status-invisible",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
"hover",
|
||||
] as const
|
||||
).map((key) => (
|
||||
<div
|
||||
className="entry"
|
||||
key={key}
|
||||
style={{ backgroundColor: theme.getVariable(key) }}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="color"
|
||||
value={theme.getVariable(key)}
|
||||
onChange={(el) =>
|
||||
setVariable({
|
||||
key,
|
||||
value: el.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
color: theme.getContrastingVariable(
|
||||
key,
|
||||
theme.getVariable("primary-background"),
|
||||
),
|
||||
}}>
|
||||
{key}
|
||||
</span>
|
||||
<div className="override">
|
||||
<div
|
||||
className="picker"
|
||||
onClick={(e) =>
|
||||
e.currentTarget.parentElement?.parentElement
|
||||
?.querySelector("input")
|
||||
?.click()
|
||||
}>
|
||||
<Pencil size={24} />
|
||||
</div>
|
||||
<InputBox
|
||||
type="text"
|
||||
className="text"
|
||||
value={theme.getVariable(key)}
|
||||
onChange={(el) =>
|
||||
setVariable({
|
||||
key,
|
||||
value: el.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
+8
-16
@@ -5,11 +5,10 @@ import { Text } from "preact-i18n";
|
||||
|
||||
import { Button } from "@revoltchat/ui";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { useApplicationState } from "../../../../mobx/State";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import Tooltip from "../../common/Tooltip";
|
||||
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||
import Tooltip from "../../../common/Tooltip";
|
||||
|
||||
const Actions = styled.div`
|
||||
gap: 8px;
|
||||
@@ -38,7 +37,6 @@ const Actions = styled.div`
|
||||
`;
|
||||
|
||||
export default function ThemeTools() {
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
const theme = useApplicationState().settings.theme;
|
||||
|
||||
return (
|
||||
@@ -56,7 +54,9 @@ export default function ThemeTools() {
|
||||
</Tooltip>
|
||||
<div
|
||||
className="code"
|
||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||
onClick={() =>
|
||||
modalController.writeText(JSON.stringify(theme))
|
||||
}>
|
||||
<Tooltip content={<Text id="app.special.copy" />}>
|
||||
{" "}
|
||||
{JSON.stringify(theme)}
|
||||
@@ -72,16 +72,8 @@ export default function ThemeTools() {
|
||||
const text = await navigator.clipboard.readText();
|
||||
theme.hydrate(JSON.parse(text));
|
||||
} catch (err) {
|
||||
openScreen({
|
||||
id: "_input",
|
||||
question: (
|
||||
<Text id="app.settings.pages.appearance.import_theme" />
|
||||
),
|
||||
field: (
|
||||
<Text id="app.settings.pages.appearance.theme_data" />
|
||||
),
|
||||
callback: async (text) =>
|
||||
theme.hydrate(JSON.parse(text)),
|
||||
modalController.push({
|
||||
type: "import_theme",
|
||||
});
|
||||
}
|
||||
}}>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 264 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user