Compare commits

..

387 Commits

Author SHA1 Message Date
jmug 1b2a980a47 [chore] Workflow cleanup.
Docker / publish (push) Successful in 13m59s Details
2026-02-23 01:55:29 -08:00
Mariano Uvalle 1ce522579e Mobile reactions and WS reconnection
* [feat] Add reaction menu item for mobile clients.

* [fix] Reconnection to the websocket with exponential backoff.

* [chore] Dev flake.

* [hack] Push to my personal gcr.
2026-02-22 13:47:39 -08:00
Paul Makles 41f47a1a3f
merge: pull request #1143 from Asraye/fix/member-sidebar 2025-09-07 13:09:46 +01:00
Paul Makles c6130a19bf
merge: pull request #1109 from Docteh/smaller3 2025-09-07 13:06:39 +01:00
Paul Makles 3c7cb2a222
merge: pull request #1141 from Asraye/fixemoji-pack 2025-09-07 13:05:41 +01:00
Paul Makles 2625b37f70
merge: pull request #1142 from Asraye/ignore-default-category 2025-09-07 13:04:47 +01:00
Paul Makles ca3613725a
merge: pull request #1137 from Asraye/master 2025-09-07 13:04:05 +01:00
Paul Makles 707d0a2d7d
merge: pull request #1146 from Asraye/fix/attachment-actions 2025-09-07 13:01:20 +01:00
Asraye 33e686ab1b
fix: correct open/download link behaviour 2025-09-04 04:23:09 +10:00
Asraye f22e6fa9fe
fix: correct open/download link behaviour 2025-09-04 04:22:20 +10:00
Asraye 2e2886f01b
fix: filter out members without ViewChannel
Small fix to add a ViewChannel check when rendering members in the sidebar
2025-09-02 16:14:10 +10:00
Asraye cbdeb1ba6d FIX: Ignore Default category (parity with beta)
Just a little change so people still using Revite don't need to see an ugly "Default" category.
2025-08-31 03:47:43 +10:00
Asraye 608b0a3ef2
FIX: Emoji pack now persists on reload 2025-08-29 21:55:56 +10:00
Asraye 59226959ba
Add scroll on mobile 2025-08-21 16:40:51 +10:00
izzy 9d82873d2a chore: make sure role ordering updates are instantenous 2025-08-07 16:57:20 +02:00
izzy 56af4b9423 chore: add default.nix 2025-08-07 12:04:45 +02:00
izzy cce94d2e0e chore: bump lang submodule 2025-08-07 12:03:54 +02:00
izzy 8ba7a769b3 merge: remote-tracking branch 'origin/role-reordering' 2025-08-07 12:03:35 +02:00
Paul Makles 92e84309ec
merge: pull request #1123 from Asraye/patch 2025-08-04 18:03:53 +01:00
Paul Makles dd5548c5e6
merge: pull request #1126 from solunareclipse1/master 2025-08-04 17:44:49 +01:00
Declan Chidlow e7d7420d5b feat: Finish crappy role ranking implementation that works but is full of sin 2025-08-04 22:28:24 +08:00
izzy ee3b54b373
docs: shorten age gate message 2025-07-25 20:34:45 +01:00
izzy 3dc9f6b045
feat: geoblock age restricted content
https://www.ofcom.org.uk/online-safety/protecting-children/online-age-checks-must-be-in-force-from-tomorrow
https://petition.parliament.uk/petitions/722903
https://wikimediafoundation.org/news/2025/07/17/wikimedia-foundation-challenges-uk-online-safety-act-regulations/
2025-07-25 19:49:00 +01:00
izzy 2a1f7cb8bb
chore: bump lang submodule 2025-07-25 19:48:16 +01:00
solunareclipse1 1136e8e31b
fix: Make animated WebP emotes properly animate
Signed-off-by: solunareclipse1 <sol@solunareclipse1.net>
2025-07-10 23:41:57 -05:00
Asraye 5337031359
Update EmbedInvite.tsx 2025-06-12 16:26:09 +10:00
Asraye 670c4e5f6e
Update EmbedInvite.tsx 2025-06-12 15:46:07 +10:00
Declan Chidlow 20c8dde197 feat: Add role re-ordering
Doesn't currently update order in UI
2025-06-12 12:33:39 +08:00
Declan Chidlow ddaca7b0c4 feat: remove legacy role ranking 2025-06-12 09:59:57 +08:00
Asraye 94fab0852f
Ugly ahh patch 2025-06-11 18:08:33 +10:00
Kyle Kienapfel 2c41c5789c feat: drastically shrink the docker image
Right now the generated docker container contains a copy of the development environment used to compile the revite interface.
This PR loses about 440MB compressed, or around 800MB uncompressed from the final image size. node in the final image has been bumped from 16 to 24, sirv-cli, and the dependencies for inject.js have been updated
2025-05-18 01:59:51 -07:00
Kyle Kienapfel 184754ca0f chore: have GHA generate ARM64 image, add description to images
The box where githubs container website complains about a lack
a description is really big. Okay its not important.
2025-05-18 01:29:54 -07:00
Kyle Kienapfel 6bacbfbb56 fix: move disabled-js.svg to proper location 2025-05-18 01:29:17 -07:00
Paul Makles 9e293e0a30
merge: pull request #1108 from Zomatree/feat/reset-bot-password 2025-05-17 13:27:04 +01:00
Paul Makles d957fbd5dc
merge: pull request #1103 from Docteh/automation 2025-05-17 13:26:33 +01:00
Zomatree 0a8b70c8ed
chore: update submodule 2025-05-17 13:10:11 +01:00
Zomatree 07d74a4ef4
feat: reset bot token button 2025-05-17 12:59:39 +01:00
Kyle Kienapfel 38ab8a7801 chore: update link to contribution guide in PR template
[skip ci]
2025-05-12 16:14:26 -07:00
Paul Makles bccf1eebae
merge: pull request #1102 from Docteh/docky 2025-05-12 11:16:25 +01:00
Kyle Kienapfel 135dbe2da4 fix: Remove caching from GitHub actions to allow compilation of docker container
The GitHub action `actions/cache@v2` is no longer available. Last successful docker build was 2024-11-15

I've removed the caching, as merely upgrading the version of the cache script did not fix the problem. Since the revoltchat/revite is deprecated I believe that the lack of caching in builds shouldn't be an undue burden on GitHub. Only 9 commits have been made here since builds stopped working

The logic for publishing Docker containers has been shuffled so that the build is executed only once when publishing.

I have tested building to GitHub's container registry, but not to dockerhub, I do not forsee any issues there.

Main reason for doing this, is that a fix for attachment downloads (https://github.com/revoltchat/revite/pull/1067) is not easily available to anyone running self-hosted.
2025-05-11 13:21:08 -07:00
izzy f95d3d27d8
chore: remove the extra string 2025-05-09 08:48:41 +01:00
izzy 3d41a016cf
fix: login error messages not rendering properly 2025-05-09 08:42:58 +01:00
Paul Makles f9c2fcec56
chore: bump language submodule 2025-01-29 13:42:20 +00:00
Paul Makles 18243f0207
chore: always target blank for download button 2025-01-29 13:41:54 +00:00
Declan Chidlow 7750276554
fix: remove /download from attachments (#1067) 2025-01-29 13:39:17 +00:00
Declan Chidlow 97a6c7d399
fix: remove ability for owners to leave server from ServerInfo modal (#1070)
Co-authored-by: Declan Chidlow <git.rh92c@8shield.net>
2025-01-29 13:39:03 +00:00
Paul Makles ac2beaf549
chore: update URL to FAQ 2024-12-17 17:35:55 +00:00
Amy bf951e59ee
fix: appending mention to draft content (#1065) 2024-12-17 17:29:57 +00:00
Paul Makles 03f9f7673d
merge: pull request #1050 from JackDotJS/fix-blank-category-name 2024-12-17 17:29:16 +00:00
Paul Makles 478d375125 fix: add try-catch block around md renderer
fix: update recursive match to block numbered lists
2024-11-15 13:09:40 +00:00
Paul Makles cb6296a96e
chore: update languages submodule 2024-11-06 09:51:44 +00:00
Paul Makles 48a9d7d370
chore: update donation links 2024-11-06 09:51:35 +00:00
Paul Makles 5de18192b2
chore: delete .env.production 2024-10-27 22:31:42 +00:00
Paul Makles ef4218d12b
merge branch: 'insert/member-subscriptions' 2024-10-27 22:00:22 +00:00
Paul Makles df4f6578f7
feat: add session token to file uploads 2024-10-27 21:59:16 +00:00
JackDotJS 28c897ac3d fix(settings): fixed inability to rename categories when they have blank names
Signed-off-by: JackDotJS <jackdotjs@proton.me>
2024-10-27 11:14:04 -07:00
Paul Makles 86e8424e46
merge: pull request #1035 from DeclanChidlow/server-leave-mobile 2024-10-16 12:53:00 +01:00
Declan Chidlow 5e66bc8dc4 fix: Add ability to leave server through server info screen 2024-10-15 20:23:36 +08:00
Paul Makles 00e6ead9bd
chore: bump revolt.js submodule 2024-07-27 15:18:23 +02:00
Paul Makles 0f0808aa56
feat: add report button to user profiles 2024-06-23 12:23:42 +01:00
Paul Makles c972e6813f
fix: should always call subscribe 2024-06-19 18:16:30 +01:00
Paul Makles 61304f18c2
feat: implement support for Subscribe event 2024-06-19 17:40:59 +01:00
Paul Makles 2722d0a854
chore: remove this so people don't click it 2024-06-09 13:57:36 +01:00
Paul Makles dfd96c449a
chore: synchronise locales 2024-06-09 13:55:52 +01:00
Paul Makles 9eca58dda1
merge: pull request #967 from revoltchat/rexo/fix/remove-deprecated-emoji 2024-06-09 13:51:11 +01:00
Sophie L 6cf0ef95ad
fix: remove (broken/deprecated) built-in custom emoji
these have been broken for a while due to the data loss when insert's server failed, and were also deprecated upon the addition of custom server emoji.

they were meant to be temporary anyway, and other clients (including the new web app) don't implement them, so let's remove them to avoid confusion.
2024-05-16 15:29:53 +01:00
insertish d3661170a4 ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2024-03-28 13:37:37 +00:00
Paul Makles 5b6546b761
fix: allow retrying sending files 2024-03-15 19:50:17 +00:00
Sophie L c25ecc12b2 chore: rm mirroring script 2024-03-13 23:51:56 +00:00
Paul Makles 60fdb5c091
docs: legacy release procedure 2024-03-13 23:41:54 +00:00
Bob Bobs 472e6e07b5 fix: only delete session if error is Unauthorized 2024-03-13 22:55:12 +00:00
Paul Makles 15e8e10151
merge: pull request #864
fix(home): inconsistent discover button height
2023-11-11 19:16:59 +00:00
Paul Makles 782a9b54ba
chore: update dest directory 2023-10-04 19:22:54 +01:00
Paul Makles f52ede1c21
chore: bump languages submodule 2023-10-04 19:18:57 +01:00
Lea 7fef354fe6 fix: appeal to monkey brain 2023-10-04 19:13:07 +01:00
Lea d4e0f19f95 feat: admin panel link for system message user 2023-10-04 19:13:07 +01:00
wangdejiang fe63c6633f chore: resolve ts-to-JSX type hints 2023-08-25 18:44:57 +01:00
Sophie L be3565871c feat: translate account management options 2023-08-25 18:44:34 +01:00
Paul Makles bc604c0157
chore: bump language submodule 2023-08-01 23:10:06 +01:00
Paul Makles 8c183b530b
feat: add admin shortcuts for convenience 2023-08-01 23:09:50 +01:00
Paul Makles 9bbc22b091
chore: update lang submodule; disable typechecking for docker build 2023-07-06 19:01:29 +01:00
Paul Makles 0d5ffb4df8
chore: update alerts to use new health service 2023-07-06 19:00:30 +01:00
Paul Makles 9d83d7cc1c
chore: bump language submodule 2023-07-02 08:46:57 +01:00
FluffyCookie000 534b9227de
fix: usernames are evolving blog post link (#895) 2023-07-02 08:46:34 +01:00
Paul Makles 5363ff2572
feat: raise katex limit to 512, upgrade dependencies 2023-06-18 10:22:01 +02:00
Paul Makles 3f1c2be709
chore: update language submodule 2023-06-17 16:11:32 +01:00
Paul Makles 04027658e6
feat: add more validation to KaTeX in markdown 2023-06-17 16:11:22 +01:00
Paul Makles f1594a7b36
chore: show error on modify account 2023-06-15 18:43:36 +01:00
Paul Makles fa0fcedce5
chore: we cooking now frfr 2023-06-13 22:04:14 +01:00
Paul Makles 70f5e6fc7e
chore: monkey patch ulid parse error 2023-06-12 09:58:13 +01:00
Paul Makles 9b9ec867da
chore: update typing indicator to at least use display name 2023-06-12 09:52:00 +01:00
Paul Makles fead4ca879
feat: always show link warning for masked links 2023-06-12 09:51:17 +01:00
Paul Makles 449eee006d
fix: copy full username from user profile 2023-06-11 22:26:56 +01:00
Paul Makles cf691a8462
fix: use display name where available for friends 2023-06-11 22:22:52 +01:00
Paul Makles c5ca579c95
chore: add helpful hint for new username format 2023-06-11 19:00:34 +01:00
Paul Makles 636367faea
chore: strip emoji picker experiment
chore: use discrim
2023-06-11 18:36:07 +01:00
Paul Makles f20ada7c49
feat: add changelog entry in preparation for update 2023-06-11 17:31:22 +01:00
Paul Makles 75d07ffe0f
chore: move badges to top half of user profile 2023-06-11 13:02:38 +01:00
Paul Makles 5b600bec20
feat: back port discriminators and display names 2023-06-11 12:44:05 +01:00
temptrash 86218a7272 feat: swap pomelo usernames for discriminators 2023-06-10 15:01:12 +02:00
Paul Makles 9bc052ea87
chore: don't update the default .env file 2023-06-03 19:29:50 +01:00
Paul Makles 1862db89a3
feat: add support for webhooks
fix: removing reactions crashes client
2023-06-03 19:29:14 +01:00
Paul Makles 00708bb8f4
feat: include message context (if any) when reporting user 2023-05-31 12:06:31 +01:00
Paul Makles 7e87265ccb
chore: update lang submodule 2023-05-31 11:50:35 +01:00
kate d55d86aa3c
fix: comment out link for nightly desktop build (#882) 2023-05-06 19:19:45 +01:00
Paul Makles c426b6c184
chore: bump components and lang submodules 2023-04-21 21:12:20 +01:00
Paul Makles 4d58ff06f3
fix: weird typing errors that cropped up 2023-04-21 21:05:29 +01:00
Paul Makles 9f9902ffb1
fix: reset member sidebar fetch status when we become ready 2023-04-21 20:52:23 +01:00
Ashley e8989fcffa
add a blank space to re run CI 2023-03-22 18:40:24 +03:00
Ashley 06dea9300c
fix inconsonant height in buttons 2023-03-20 18:11:10 +03:00
Paul Makles c13896244c chore: update components submodule 2023-03-18 22:22:02 +00:00
Paul Makles e17e556ee6
chore: add notice to emoji menu 2023-03-14 21:57:47 +00:00
Paul Makles 7441569cfa
chore: enable emoji picker by default 2023-03-14 21:48:19 +00:00
Paul Makles eff1de7c78
chore: add notices throughout app about development 2023-03-14 21:48:13 +00:00
Paul Makles b733e13ec2
merge: branch 'master' of https://github.com/revoltchat/revite 2023-03-14 21:16:16 +00:00
Declan Chidlow e68ceac4f6
fix: overflow management in voice menus (#849) 2023-03-14 21:16:05 +00:00
Paul Makles 3f536589e5
chore: update languages submodule 2023-03-14 21:15:04 +00:00
Paul Makles 21175dffda
fix: check if content actually exists
chore: update lang submodule
2023-02-24 13:53:56 +01:00
Paul Makles 5b31ce494e
merge: remote-tracking branch 'origin/production' 2023-02-23 20:08:51 +01:00
Paul Makles 056ec623cb
chore: move date forwards 2023-02-23 19:12:26 +01:00
Paul Makles ff0330ec1b
feat(modal): add report success modal (also allows blocking user) 2023-02-23 17:52:47 +01:00
Paul Makles 58f35a2813
feat: add changelog entry for in-app reporting 2023-02-22 18:34:35 +01:00
Paul Makles a00d554f80
feat: render shadows and markdown in changelogs 2023-02-22 18:33:38 +01:00
Paul Makles 7f911f5d2c
chore: always build highmem 2023-02-22 17:59:38 +01:00
Paul Makles 122f047c85
fix(modal): trigger on keydown instead of keyup to prevent interference 2023-02-22 17:59:26 +01:00
Paul Makles 7d544c82ab
feat: add reporting UI 2023-02-22 17:13:43 +01:00
Paul Makles 46eec5a132
fix: do not trigger modal submission on <select> 2023-02-22 17:00:47 +01:00
Paul Makles de85527cd8
chore: bump submodules 2023-02-22 15:01:17 +01:00
kate 6062a361f1
fix: allow dash char in autocorrect content validation (#838) 2023-02-05 13:45:32 +00:00
Paul Makles 95d4f61d7f
feat: support Streamable embeds 2023-01-29 17:34:07 +00:00
Paul Makles 34ce1d1a86
chore: bump submodule dependencies 2023-01-29 17:33:42 +00:00
insertish a9f23fe0e3 ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2023-01-24 19:43:32 +00:00
insertish 6c08ccff52 ci: synced local '.github/workflows/triage_issue.yml' with remote 'workflows/triage_issue.yml'
[skip ci]
2023-01-24 19:43:32 +00:00
Leda 2f43a2c32d
fix: category title input reverts to span on empty string (#699)
Fixes https://github.com/revoltchat/revite/issues/693
2023-01-24 17:59:31 +00:00
cheneyni-451 e34c5c99fe
fix(ui): add margin to delete role button (#802)
Co-authored-by: Cheney Ni <cheneyni@umich.edu>
2023-01-24 17:56:50 +00:00
kate 89b3c9c098
fix: dash ("-" char) in emoji names (#816) 2023-01-24 17:55:17 +00:00
Sophie L dadf0b6329
fix: update draft check (#830) 2023-01-24 17:55:04 +00:00
Lea fcf6812151
feat: allow rolt.chat for relative navigation (#814) 2022-12-05 14:47:04 +00:00
Lea 09be8c9e4f
feat: list custom emojis in autocomplete (#809)
* feat: list custom emojis in autocomplete

* fix: properly align emoji name in autocompletion
2022-12-05 14:44:47 +00:00
kate 6767ea1853
fix(ci): typing issues; broken submodules (#826)
* Remove broken submodules

* Fix yarn typecheck

* Add build:deps before typecheck to fix missing dependencies

* fix: minor linting nitpick

Co-authored-by: Sophie L <beartechtalks@gmail.com>
2022-12-05 14:42:43 +00:00
Paul Makles 9be4afe241
fix: allow setting custom remote for publish 2022-11-06 13:26:41 +00:00
Paul Makles 7e89dcfb13
chore: add notice for iCloud users 2022-11-06 13:21:06 +00:00
Paul Makles 3836156f3d merge: branch 'master' into production 2022-10-23 21:09:49 +01:00
Paul Makles 9f3e47d327 chore: remove captcha from login form 2022-10-23 21:08:36 +01:00
Paul Makles e29678a6a3 fix: add load suspense on master branch 2022-10-23 21:06:53 +01:00
Paul Makles 40d3356c1b merge: branch 'master' into production 2022-09-20 19:12:25 +01:00
Paul Makles eb4670ec43
revert: "fix: display server identity (if present) in typing indicator" (#789) 2022-09-20 19:11:45 +01:00
Paul Makles df20ab7407 merge: branch 'master' into production 2022-09-20 18:44:51 +01:00
Paul Makles 22b2bc0a5e fix: use new lightspeed embed URL 2022-09-20 18:31:12 +01:00
Paul Makles 2a12b79ef7 chore: bump lang submodule 2022-09-20 18:31:05 +01:00
4444dogs 9b6abe374a
feat: add seasonal halloween theme (#784)
Co-authored-by: Sophie L <beartechtalks@gmail.com>
2022-09-20 18:26:20 +01:00
Compey b649b2a923
Use rvlt.gg instead of app.revolt.chat/invite for invites (#783) 2022-09-20 18:25:14 +01:00
Ed L b64c316dc9 chore: lay groundwork for masquerades 2022-09-20 18:24:59 +01:00
Ed L bfeefb4c73 feat: use Button component for dev page, linting 2022-09-20 18:24:59 +01:00
Ed L 734fa06425 chore: use ios icon for sessions, update links 2022-09-20 18:24:59 +01:00
Ed L 5a738b7c68 fix: grammar, update command for dev mode 2022-09-20 18:24:59 +01:00
Ed L f03a88bd78 fix: move leave/delete server option to the bottom 2022-09-20 18:24:59 +01:00
Ed L e53059ee08 fix: let users message/hide mutuals tab for bots 2022-09-20 18:24:59 +01:00
Ed L 099f7a3116 feat: show role id 2022-09-20 18:24:59 +01:00
Ed L d2264a2a43 fix: use --monospace-font 2022-09-20 18:24:59 +01:00
Ed L ef26f67c8e chore: deduplicate imports 2022-09-20 18:24:59 +01:00
Ed L 5b7ec655eb chore: bump deps, remove old login background 2022-09-20 18:24:59 +01:00
Ed L a381ba6320 feat: revamp onboarding 2022-09-20 18:24:59 +01:00
Jan 4f2fc267f9
feat: display account age next to join messages (#750) 2022-09-20 18:23:06 +01:00
Leda 2687f98952
fix: display server identity (if present) in typing indicator (#703) 2022-09-20 18:22:25 +01:00
Paul Makles 0a27632f05 chore: bump components dep 2022-09-19 10:53:36 +01:00
Paul Makles 1621e3b17d merge: branch 'master' into production 2022-09-18 12:32:36 +01:00
Paul Makles 6b9106c975 fix: correctly match protocols 2022-09-18 12:32:29 +01:00
Paul Makles e6ad6a552e feat: add focus mode 2022-09-18 12:05:48 +01:00
Paul Makles 7ba2388de4 merge: branch 'master' into production 2022-09-18 10:24:23 +01:00
Paul Makles 47bfaad508 fix: sanitise links passed to react-router
fix: flip protocol sanitisation to use a whitelist
2022-09-18 10:24:15 +01:00
Paul Makles 1a9a2786bb merge: branch 'master' into production 2022-09-17 13:02:15 +01:00
Paul Makles 61a06c3f1a fix: re-write blockquote regex to include lists 2022-09-17 13:01:02 +01:00
Paul Makles b42c24295f fix: null assertion has a chance of throwing error here 2022-09-17 10:50:26 +01:00
Paul Makles 8fd6963f38 merge: branch 'master' into production 2022-09-16 18:47:09 +01:00
Paul Makles 71e689ece3 chore: update language defns 2022-09-16 18:46:54 +01:00
Paul Makles 7bc806ec63 feat: add indication if language is not fully translated
chore: update language definitions
2022-09-16 14:55:55 +01:00
Paul Makles 1016d80e56 merge: branch 'master' into production 2022-09-08 21:28:25 +01:00
Paul Makles eab5ed033f merge: branch 'master' of https://github.com/revoltchat/revite 2022-09-08 21:24:29 +01:00
Paul Makles 263d97853a merge: branch 'master' into production 2022-09-08 17:22:00 +01:00
Paul Makles ff57dbddba fix: correctly specify headers when removing MFA 2022-09-08 11:19:26 +01:00
Paul Makles de207f0fa7 chore: add revolt.js as a submodule 2022-09-03 14:06:29 +01:00
Paul Makles 1c69756383 merge: branch 'master' into production 2022-09-03 09:19:04 +01:00
Paul Makles 02eb7d83f6 feat: use wxp logo for Windows 7 2022-09-03 09:18:56 +01:00
Paul Makles 14037ef0c7 merge: branch 'master' into production 2022-09-02 16:29:27 +01:00
Paul Makles 2c50d9be6b chore: disable list behaviour if line starts with plus 2022-09-02 16:29:20 +01:00
Paul Makles 594ef00d09 feat: add ability to leave groups / servers silently 2022-09-02 16:20:25 +01:00
Paul Makles 8608257066 merge: branch 'master' into production 2022-09-02 14:42:47 +01:00
Paul Makles 7626a1f461 fix: remove stray text 2022-09-02 14:40:17 +01:00
Paul Makles 83ca6f489e feat: add new reaction button to list 2022-09-02 14:35:16 +01:00
Paul Makles b7a10bb9ab fix: ensure blockquotes are broken 2022-09-02 14:17:56 +01:00
Paul Makles f6be6d7cf8 feat: change emojis page from grid to list 2022-09-02 14:17:42 +01:00
Paul Makles dfbba41be4 feat: redesign emoji uploader 2022-09-02 13:42:34 +01:00
Paul Makles 88bc8f93b6 merge: branch 'master' into production 2022-09-01 14:04:27 +01:00
Paul Makles 024fc6853b chore: bump language definitions 2022-09-01 14:04:09 +01:00
Paul Makles 5bd2d24c56 feat: display timeout status on client 2022-09-01 14:00:43 +01:00
Paul Makles 5c50bed33d chore: update language definitions 2022-08-18 12:56:26 +02:00
Paul Makles 07fd598bf9 fix: don't collapse whitespace / newlines 2022-08-18 12:56:21 +02:00
Paul Makles a234e3a582 chore: update form components to be more reliable 2022-08-15 20:20:25 +02:00
Paul Makles a77f2f9b4d chore: remove stray log 2022-08-15 19:37:29 +02:00
Paul Makles 787d5840d2 chore: hide discovery button if not pointing to Revolt 2022-08-15 19:32:02 +02:00
Paul Makles 353507e17a fix: build components during build 2022-08-09 12:52:20 +02:00
Paul Makles 1b41ca03d9 chore: revert back to yarn portals 2022-08-09 12:40:03 +02:00
Paul Makles 3ca2b12dfc chore: add @revoltchat/ui as a submodule 2022-08-09 12:36:16 +02:00
Paul Makles ff41219cf4 Merge branch 'master' into production 2022-08-08 16:08:12 +01:00
Sophie L 7cf7402cea
fix: styling fixes (#721) 2022-08-08 16:06:21 +01:00
Leda c2a729a5e0 fix: render markdown inside/after HTML tags
fixes #733
2022-08-08 16:02:57 +01:00
Alice Harris e9977b2a76 fix: large images in website previews escape embed box 2022-08-08 16:02:04 +01:00
Paul Makles f76e53649b Merge branch 'master' into production 2022-08-08 15:30:21 +01:00
Paul Makles a4b8fb5fc2 chore: update strings and permission lists 2022-08-08 15:30:10 +01:00
Paul Makles c9264e1a49 Merge branch 'master' into production 2022-08-08 15:22:17 +01:00
Paul Makles 9a5653bc02 chore: update lang submodule 2022-08-08 15:22:03 +01:00
Paul Makles 50fd3b2d11 chore: clean up repository 2022-08-08 15:21:46 +01:00
Paul Makles 58f294b790 feat: add reaction button to overlay 2022-08-08 15:15:20 +01:00
Paul Makles e1d3ad1675 chore: update language submodule 2022-08-08 14:36:34 +01:00
Paul Makles b2e0b5a035 merge: branch 'master' into production 2022-07-31 12:20:41 +02:00
Paul Makles 11a17feaae chore: handle all updates to messages 2022-07-31 12:20:25 +02:00
Paul Makles ca69a0b4c5 fix: remove eye_speech_bubble (cursed) 2022-07-31 11:38:22 +02:00
Paul Makles 11e8cd652d merge: branch 'master' into production 2022-07-30 12:30:07 +02:00
Paul Makles 6e70b55d02 chore: update lang 2022-07-30 12:29:50 +02:00
Paul Makles dedc1e0666 feat: add corresponding UI for `interactions` reactions 2022-07-30 12:23:56 +02:00
Paul Makles 9a70bb0eff merge: branch 'master' into production 2022-07-18 13:07:04 +01:00
Paul Makles 791f4dfd89 chore: bump lang submodule 2022-07-18 13:04:03 +01:00
Paul Makles 3fbf315587 fix: add LoadSuspense around verification page 2022-07-18 13:02:40 +01:00
Paul Makles 9807ef9c9a fix: match optional whitespace in quote regex 2022-07-18 12:59:17 +01:00
Paul Makles 96017c5f33 merge: branch 'master' into production 2022-07-16 15:18:06 +01:00
Paul Makles 084c90613f feat: add reactions 2022-07-16 15:17:02 +01:00
Paul Makles dbe8a64ffc fix: pass-through to color / unset if no gradient 2022-07-16 12:59:59 +01:00
Paul Makles 176c7883c8 merge: branch 'master' into production 2022-07-15 21:51:24 +01:00
Paul Makles 73fd35bf46 feat: add change group ownership / text system msg 2022-07-15 21:47:32 +01:00
Paul Makles 1a0b4b5703 feat: render masquerade colour 2022-07-15 21:38:11 +01:00
Paul Makles 62a427b7a7 feat: add separator to recovery codes when copying 2022-07-15 16:41:02 +01:00
Paul Makles 64e7038532 chore: bump revolt.js dependency 2022-07-15 16:25:51 +01:00
Paul Makles 7e88e733d5 feat: change colour rendering 2022-07-15 16:17:15 +01:00
Paul Makles 70bda88383 merge: branch 'master' into production 2022-07-14 17:14:12 +01:00
Paul Makles f3822b625d feat: make emoji picker close on select / interact elsewhere 2022-07-14 17:13:51 +01:00
Paul Makles df1b39256d feat: show message that user can't message another 2022-07-14 15:06:37 +01:00
Paul Makles c9066aba2d merge: branch 'master' into production 2022-07-13 13:06:47 +01:00
Paul Makles fc8cfa5419 fix: actually log out invalidate sessions 2022-07-13 13:05:43 +01:00
Paul Makles 3204d176de merge: branch 'master' into production 2022-07-13 12:57:07 +01:00
Paul Makles e3a526e2d7 fix: type errors with markdown content 2022-07-13 12:57:01 +01:00
Paul Makles 57887dc86f merge: branch 'master' into production 2022-07-13 12:53:58 +01:00
Paul Makles 4f3f6e26cf feat: convert html AST nodes to text 2022-07-13 12:32:39 +01:00
Paul Makles 2214efe1bc chore: hide emoji settings if no perm 2022-07-12 14:21:44 +01:00
Paul Makles 030c211230 fix: internal links would not redirect properly 2022-07-12 14:15:53 +01:00
Paul Makles 7f6db77c4f chore: change quote depth limit to 5 from 3 2022-07-11 15:33:57 +01:00
Paul Makles 924448dc2c fix: remove html entities using AST plugin 2022-07-11 15:33:34 +01:00
Paul Makles 77c3f8d1bc chore: bump @revoltchat/ui to 1.0.76 2022-07-10 15:30:34 +01:00
Paul Makles 80943afcf6 fix: don't hide settings button behind bottom nav
fixes #691
2022-07-10 14:37:11 +01:00
Paul Makles f792888268 chore: bump UI library 2022-07-10 14:27:00 +01:00
Paul Makles aad9a30266 fix: correctly pass-through preview URLs 2022-07-10 13:57:47 +01:00
Paul Makles 0ec7e5e116 feat: try to load any 'valid' emoji 2022-07-10 13:53:19 +01:00
Paul Makles 2b65e98cd3
merge: pull request #730 from revoltchat/feat/emojis 2022-07-09 17:56:49 +01:00
Paul Makles 448722225e fix: change quote matching Regex 2022-07-09 17:55:13 +01:00
Paul Makles cb6d5a3828 feat: update emoji picker; move settings bhnd expr 2022-07-09 17:53:40 +01:00
Paul Makles 354c22108e feat: it's morbing time 2022-07-08 18:38:39 +01:00
Paul Makles ec96dde694 feat: render custom emoji 2022-07-08 17:14:15 +01:00
Paul Makles 445e9537d4 merge: remote-tracking branch 'origin/master' into feat/emojis 2022-07-08 17:02:39 +01:00
Paul Makles c12d40d0da fix: correct mention styling 2022-07-08 16:58:21 +01:00
Paul Makles 7e20d5029e fix: underline anchor; prevent jitter on render 2022-07-08 15:45:16 +01:00
Paul Makles 262b931810 fix: actually render HTML out instead of obliterating it 2022-07-08 15:36:18 +01:00
Paul Makles 4a85dd69cf fix: limit maximum quote depth to 3 2022-07-08 15:19:16 +01:00
Paul Makles 34bb2bbc13 feat: switch to `remark` from `markdown-it` (big)
* replaces old mentions with avatar and display name
* renders things directly through React
* replaces most of the markdown hacks with custom AST components
* adds a tooltip to codeblock "copy to clipboard"
2022-07-08 14:24:48 +01:00
Paul Makles b541301cb1 feat: add test emoji page 2022-07-07 17:33:33 +01:00
Paul Makles a766183f01 feat: port `CreateBot` to modal form
fixes #723
2022-07-06 13:15:33 +01:00
Paul Makles 47e3d0bdb5 chore: update README to reflect project changes [skip ci] 2022-07-06 13:09:17 +01:00
Paul Makles c51b024329 chore(refactor): delete `context/revoltjs` folder 2022-07-06 13:08:03 +01:00
Paul Makles e37140dcd0 chore: minor styling change for clipboard modal 2022-07-06 12:50:38 +01:00
Paul Makles 705dcd001b feat: add correct submit buttons to form modals 2022-07-06 12:49:24 +01:00
Paul Makles f9c6f5cd9d chore: delete intermediate 2022-07-05 21:13:42 +01:00
Paul Makles f7ff7d0dfe feat: port `CreateCategory` / fix `Channel` 2022-07-05 20:55:24 +01:00
Paul Makles 29fb8e0064 feat: port `CreateChannel` modal 2022-07-05 20:37:40 +01:00
Paul Makles 4009b19f9c feat: port `BanMember`, `KickMember` modals 2022-07-05 20:25:00 +01:00
Paul Makles ec347f585d feat: port `CreateInvite` 2022-07-05 20:11:32 +01:00
Paul Makles 160d71684f feat: port `DeleteMessage` and `Confirmation` 2022-07-05 18:46:13 +01:00
Paul Makles 79c90c1b00 feat: port input modals to new system 2022-07-05 17:53:41 +01:00
Paul Makles 23dec32476 feat(refactor): partially rewrite appearance settings 2022-07-05 15:49:08 +01:00
Paul Makles a24e027a48 chore: clean up picker / prism imports 2022-07-04 19:04:27 +01:00
Paul Makles 6e70937825 chore: turn emoji picker into an experiment 2022-07-01 19:32:27 +01:00
Paul Makles e4b61819d3 feat: mogus vented 2022-07-01 19:23:50 +01:00
Paul Makles 6bf58e8379 feat: test emoji picker design 2022-07-01 18:09:53 +01:00
Paul Makles 5dfe72c093
merge: pull request #717 from revoltchat/chore/client-fsm 2022-07-01 15:12:04 +01:00
Paul Makles c4ac7a1b6d
Merge branch 'master' into chore/client-fsm 2022-07-01 15:11:39 +01:00
Paul Makles 401b2d4990 feat: port `UserProfile`, `Onboarding`, `CreateBot` to legacy 2022-06-30 20:39:00 +01:00
Paul Makles 0d3f29515e feat: port `ImageViewer` 2022-06-30 19:34:04 +01:00
Paul Makles 1664aaee15 feat: add `ServerInfo`, port `ChannelInfo` 2022-06-30 19:06:49 +01:00
Paul Makles 8501e33103 chore(refactor): move `RequiresOnline` into controllers 2022-06-29 17:33:23 +01:00
Paul Makles a2a52e237d chore(refactor): remove `Notifications` component 2022-06-29 17:31:59 +01:00
Paul Makles 05516c5823 fix: hide push notifications on electron app 2022-06-29 16:46:25 +01:00
Paul Makles 45692999bf chore(refactor): remove `SyncManager` 2022-06-29 16:41:26 +01:00
Paul Makles 1fcb3cedc1 feat: consistent authentication flow
fix: missing suspense on login
feat: re-prompt MFA if fail on login
2022-06-29 16:27:57 +01:00
Paul Makles 0261fec676 chore: deprecate `RevoltClient` context 2022-06-29 16:02:35 +01:00
Paul Makles 0e86f19da2 chore(doc): document client controller 2022-06-29 14:49:48 +01:00
Paul Makles 31220db8fe feat: fully working onboarding on login 2022-06-29 11:48:48 +01:00
Paul Makles 66ae518e51 feat: make login functional again 2022-06-29 10:52:42 +01:00
Paul Makles 8d505c9564 chore: clean up FSM code 2022-06-29 10:28:24 +01:00
Paul Makles 5f2311b09c feat: implement `useClient` from client controller 2022-06-28 19:59:58 +01:00
Paul Makles ce88fab714 feat: get fsm to a working testing state 2022-06-28 13:49:50 +01:00
Paul Makles b53d3bce13 fix: don't display date on ("new") message divider 2022-06-28 13:23:45 +01:00
Paul Makles c997286340 patch: temporarily fix issue with public invites 2022-06-28 13:22:10 +01:00
Paul Makles 80f4bb3d98 feat: build finite state machine for sessions 2022-06-28 13:20:08 +01:00
Paul Makles 1cfcb20d4d chore(refactor): rename `context/modals` to `controllers/modals` 2022-06-27 17:56:06 +01:00
Paul Makles 95ebd935ed fix: duct-tape fix the bot edit issues
fixes #629
2022-06-21 11:14:51 +01:00
Paul Makles 3b7c1cbe20
chore(ci): allow any tag [skip ci] 2022-06-21 11:14:04 +01:00
Paul Makles cb0a521473 fix: no client context on ModifyAccount
fix: no reactivity on account settings

closes #706
fixes #683
fixes #702
2022-06-21 10:57:58 +01:00
Paul Makles aa9974149c fix: bump @revoltchat/ui 2022-06-18 17:13:00 +01:00
Paul Makles 569864167e fix: consider whether alert is present in height calc 2022-06-18 17:05:05 +01:00
Paul Makles f185dec461 feat: handle system alerts and poll rate changes 2022-06-18 17:03:04 +01:00
Paul Makles 03e177f865 feat(modal): implement new server identity modal
closes #172
2022-06-18 15:54:17 +01:00
Paul Makles f685352963 fix: temporarily hack in mention to profile flow 2022-06-18 15:06:24 +01:00
Paul Makles 6755217ad2 feat(modal): port ModifyAccount and PendingRequest 2022-06-18 15:02:59 +01:00
Paul Makles 211ff2058a chore: remove legacy Redux migration 2022-06-18 14:49:59 +01:00
Paul Makles b7be9f8c03 feat(modal): port LinkWarning 2022-06-18 14:19:31 +01:00
Paul Makles 241b9cd27b feat(modal): port Clipboard 2022-06-18 12:33:22 +01:00
Paul Makles d10bd96900 feat(modal): port Error and ShowToken 2022-06-18 12:25:56 +01:00
Paul Makles 374be319c4 feat(modals): port SignedOut and SignOutSessions 2022-06-18 11:56:05 +01:00
Paul Makles 0ee7b73d61 feat: re-work modal behaviour to be more natural 2022-06-18 11:22:37 +01:00
Paul Makles 63d5f6bb7d fix: show update button on native if frame off 2022-06-17 16:57:13 +01:00
Leda 34e6995d86
fix: modify account, and create bot forms not being submitted (#697) 2022-06-17 16:51:10 +01:00
Paul Makles a1ef1dce5e fix: remove status request
closes #685
closes #643
2022-06-17 16:50:48 +01:00
Paul Makles a190a51d0b fix(qr): render the QR code consistently 2022-06-14 18:01:19 +01:00
Paul Makles c9127d6cf3 fix(css): please let the torture stop 2022-06-14 17:21:52 +01:00
div2005 f0d2e31b17
fix: bottom nav alignment is wrong (#684)
Co-authored-by: div2005 <div2005@tuta.io>
Co-authored-by: Paul Makles <paulmakles@gmail.com>
2022-06-14 17:19:18 +01:00
Paul Makles 220a28a151 fix: bottom nav button alignment
fixes #679
2022-06-14 17:16:18 +01:00
Paul Makles b44779c89a fix: user picker checkbox + width
fixes #657
fixes #681
2022-06-14 17:10:59 +01:00
Paul Makles 5835064219 chore: fix merge conflicts 2022-06-14 16:30:42 +01:00
Paul Makles b9da79bc11 chore: bump `preact-context-menu` 2022-06-14 16:27:46 +01:00
Leda ba99cbaf2a
fix: bug in user/channel mention when query text is empty (#659) 2022-06-14 15:24:13 +01:00
Leda fc0c7611d4
fix: bug where channel icon scales with channel name (#661)
Co-authored-by: Paul Makles <paulmakles@gmail.com>
2022-06-14 15:23:58 +01:00
Leda 2e9c013ed8
fix: display voice channel as link in messages (#658)
* fix: display voice channel as link in messages

* chore: format
2022-06-14 15:17:00 +01:00
Leda 3e40a61624
fix: bug in join server button when in light theme (#660) 2022-06-14 15:13:31 +01:00
Paul Makles 4e22ccb2f7 chore: duct-tape fix for semver library breaking 2022-06-12 22:24:29 +01:00
Paul Makles 00718245f9 fix: make the changelog button in settings work 2022-06-12 22:21:23 +01:00
Paul Makles cd8ab6739b feat: add changelog modal 2022-06-12 22:19:41 +01:00
Paul Makles 5eabd2861f chore: unlink components 2022-06-12 21:19:27 +01:00
Paul Makles a404ff7fe0 feat: add auto-update and out-of-date indicator 2022-06-12 21:16:42 +01:00
Paul Makles 7680931f5f chore: i18n 2022-06-12 21:11:23 +01:00
Paul Makles c1324108e3 fix(eslint): rules included deprecated plugin 2022-06-12 19:38:29 +01:00
Paul Makles 56770d40df chore: bump language submodule 2022-06-12 19:29:17 +01:00
Paul Makles 64f19ec2c0 chore: display error on load if present 2022-06-12 19:27:18 +01:00
Paul Makles dbb1c1e8fa feat: finalise 2FA login 2022-06-12 19:24:59 +01:00
Paul Makles c686e85d37 feat: add MFA recovery codes 2022-06-12 16:30:37 +01:00
Paul Makles 8eefc87b05 chore(refactor): clean up component folder structure 2022-06-12 15:24:00 +01:00
Paul Makles 645e1af6db chore: refactor account UI 2022-06-12 15:07:30 +01:00
Paul Makles 8103cc03cf chore: move "resend verification" button 2022-06-12 12:16:15 +01:00
Paul Makles bdc527ebbe
chore(ci): re-enable mirroring 2022-06-10 18:50:14 +01:00
Paul Makles 8a2826da91 fix: use `class` in markdown rendering 2022-06-10 17:36:59 +01:00
Paul Makles bd50378234 feat: add account deletion confirmation route 2022-06-10 17:20:31 +01:00
Paul Makles 71f8fc86a4 chore: fix build errors 2022-06-10 17:00:37 +01:00
Paul Makles 277eaa685d
chore(ci): stop mirroring to GitLab [skip ci] 2022-06-10 16:53:02 +01:00
Paul Makles e81b8ed472 feat: new modal renderer + mfa flow modal 2022-06-10 16:52:12 +01:00
Paul Makles 6be0807433 feat: add disable / delete funct; bump revolt-api 2022-06-10 14:32:21 +01:00
Paul Makles e0ca1681bd chore(refactor): move and re-organise types folder 2022-06-10 14:11:38 +01:00
Paul Makles ebcbe4bd4b chore: read version from package.json 2022-06-10 12:18:02 +01:00
Paul Makles ec8b51f559 chore: repository clean-up 2022-06-09 14:58:39 +01:00
Paul Makles 72e60b7528 chore: clean up build errors 2022-06-09 14:50:05 +01:00
Paul Makles 3399991824
merge: pull request #650 from revoltchat/chore/component-migration 2022-06-09 14:49:17 +01:00
Paul Makles 6497e11fb0
Merge branch 'master' into chore/component-migration 2022-06-09 14:48:45 +01:00
Paul Makles 7544c78360 chore: prepare account management buttons 2022-06-09 14:47:53 +01:00
Paul Makles 040c4367f7 chore: clean up code 2022-06-09 14:47:33 +01:00
Paul Makles ee80dfd3c8 fix: pull `Client` into state early
(should fix empty server list)
2022-06-09 14:40:54 +01:00
Paul Makles 67c8418c31 fix: actually resolve the error from requests 2022-06-07 17:00:11 +01:00
Paul Makles 2056232759 fix: build errors 2022-05-30 16:46:07 +01:00
Paul Makles 81bf325990 feat: add column element 2022-05-30 16:15:52 +01:00
Paul Makles 906f15f103 fix: use shape="circle" for friend component 2022-05-30 15:54:55 +01:00
Paul Makles ea106a3902 chore: bump @revoltchat/ui 2022-05-30 15:48:33 +01:00
Paul Makles 41e533ab59 feat(@ui): port Modal component 2022-05-30 15:45:14 +01:00
Paul Makles 68b9d5ea79 feat(@ui): migrate category / overline and header 2022-05-30 14:42:09 +01:00
Paul Makles 673efc0586 fix: make context menu line divider compact 2022-05-30 12:57:30 +01:00
Paul Makles 74f8c552ed feat(@ui): port category button 2022-05-30 12:56:47 +01:00
Paul Makles f3bdbe52d9 feat(@ui): migrate textarea and tip 2022-05-30 12:47:13 +01:00
Paul Makles b4777e9816 feat(@ui): migrate line divider, preloader and save status 2022-05-30 12:40:01 +01:00
Paul Makles ab77d4a812 feat(@ui): migrate radio 2022-05-30 12:29:56 +01:00
Paul Makles 1d243d4762 feat(@ui): migrate input box 2022-05-30 12:26:16 +01:00
Paul Makles 2f9bfbf83f chore: remove Masks component 2022-05-30 12:19:32 +01:00
Paul Makles c2547b3ead feat(@ui): migrate icon button 2022-05-30 12:01:47 +01:00
Paul Makles a64fe61199 feat(@ui): migrate date divider and details 2022-05-29 16:40:02 +01:00
Paul Makles 1bd138d6ef feat(@ui): migrate colour swatches and combo box 2022-05-29 16:38:09 +01:00
Paul Makles 4bcfa601a5 feat(@ui): migrate checkbox component 2022-05-29 16:34:54 +01:00
Paul Makles 20d31babce feat(@ui): migrate Banner component 2022-05-29 15:43:36 +01:00
Paul Makles 12b9716043 fix: open last opened server channel instead of first 2022-05-27 22:32:06 +01:00
Paul Makles e2d9e41a58 chore: bump revolt.js and @revoltchat/ui 2022-05-27 22:31:12 +01:00
Paul Makles bdf741e0ee feat: add "ordering" data store 2022-05-27 21:21:42 +01:00
Paul Makles 588cb7c019 feat: finish reimplementation of server list 2022-05-27 21:21:42 +01:00
Paul Makles 94dd4b464a chore: server list integration test 2022-05-27 21:21:42 +01:00
Paul Makles 4541a34cef Merge branch 'master' into production 2022-05-19 13:42:15 +01:00
Paul Makles 83a3585940 chore: add notice 2022-05-07 10:45:18 +01:00
325 changed files with 12457 additions and 9072 deletions

5
.env
View File

@ -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
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

View File

@ -1,6 +1,6 @@
## Please make sure to check the following tasks before opening and submitting a PR
* [ ] I understand and have followed the [contribution guide](https://github.com/revoltchat/revolt/discussions/282)
* [ ] I understand and have followed the [contribution guide](https://developers.revolt.chat/contrib.html)
* [ ] I have tested my changes locally and they are working as intended
* [ ] These changes do not have any notable side effects on other Revolt projects
* [ ] (optional) I have opened a pull request on [the translation repository](https://github.com/revoltchat/translations)

View File

@ -3,109 +3,66 @@ name: Docker
on:
push:
branches:
- "master"
- "handmade"
tags:
- "v*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
- "*"
# TODO: Bring back once gitea is updated past 1.21
# paths-ignore:
# - ".github/**"
# - "!.github/workflows/docker.yml"
# - ".vscode/**"
# - ".gitignore"
# - ".gitlab-ci.yml"
# - "LICENSE"
# - "README"
pull_request:
branches:
- "master"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
- "handmade"
# TODO: Bring back once gitea is updated past 1.21
# paths-ignore:
# - ".github/**"
# - "!.github/workflows/docker.yml"
# - "!.github/workflows/preview_*.yml"
# - ".vscode/**"
# - ".gitignore"
# - ".gitlab-ci.yml"
# - "LICENSE"
# - "README"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [linux/amd64]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/${{ matrix.architecture }}
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
- name: Build
uses: docker/build-push-action@v2
with:
context: .
platforms: ${{ matrix.architecture }}
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
publish:
needs: [test]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
# NOTE: Running on pull requests for now, but without pushing.
# if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache amd64 Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/linux/amd64
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
images: revoltchat/client, ghcr.io/revoltchat/client
images: handmadecities/handmade-revolt-web-client
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Login to DockerHub
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

View File

@ -1,17 +0,0 @@
name: Mirroring
on:
push:
branches:
- "master"
- "production"
jobs:
to_gitlab:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitlab.com:insert/revolt-vite.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}

View File

@ -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

View File

@ -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

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ public/assets_*
!public/assets_default
.vscode/chrome_data
.direnv

View File

@ -1,40 +0,0 @@
image: node:16-buster
variables:
GIT_SUBMODULE_STRATEGY: recursive
cache:
paths:
- node_modules
# Fetch dependencies and setup project for compilation.
install:
stage: prepare
script:
- yarn
# Type check the project
typecheck:
stage: test
needs:
- install
dependencies:
- install
script:
- yarn typecheck
# Lint the project and check prettier output.
lint:
stage: test
allow_failure: true
needs:
- install
dependencies:
- install
script:
- yarn lint
- yarn --check 'src/**/*.{js,jsx,ts,tsx}'
stages:
- prepare
- test

6
.gitmodules vendored
View File

@ -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

View File

@ -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" ]

View File

@ -1,9 +1,44 @@
# Deprecation Notice
This project is deprecated, however it still may receive maintenance updates.
PRs for small fixes are more than welcome.
## Deploying a new release
Ensure `.env.local` points to `https://app.revolt.chat/api`.
```bash
cd ~/deployments/revite
git pull
git submodule update
# check:
git status
export REVOLT_SAAS_BRANCH=revite/main
export REMOTE=root@production
scripts/publish.sh
# SSH in and restart revite:
ssh $REMOTE
tmux a -t 4
```
# Revite
## Description
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
## Pending Rewrite
The following code is pending a partial or full rewrite:
- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components)
- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
- `src/lib`: this needs to be organised
## Stack
- [Preact](https://preactjs.com/)
@ -35,6 +70,7 @@ Get revite up and running locally.
git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn build:deps
yarn dev
```
@ -42,17 +78,19 @@ You can now access the client at http://local.revolt.chat:3000.
## CLI Commands
| Command | Description |
| ------------------- | -------------------------------------------- |
| `yarn pull` | Setup assets required for Revite. |
| `yarn dev` | Start the Revolt client in development mode. |
| `yarn build` | Build the Revolt client. |
| `yarn preview` | Start a local server with the built client. |
| `yarn lint` | Run ESLint on the client. |
| `yarn fmt` | Run Prettier on the client. |
| `yarn typecheck` | Run TypeScript type checking on the client. |
| `yarn start` | Start a local sirv server with built client. |
| `yarn start:inject` | Inject a given API URL and start server. |
| Command | Description |
| --------------------------------------- | -------------------------------------------- |
| `yarn pull` | Setup assets required for Revite. |
| `yarn dev` | Start the Revolt client in development mode. |
| `yarn build` | Build the Revolt client. |
| `yarn build:deps` | Build external dependencies. |
| `yarn preview` | Start a local server with the built client. |
| `yarn lint` | Run ESLint on the client. |
| `yarn fmt` | Run Prettier on the client. |
| `yarn typecheck` | Run TypeScript type checking on the client. |
| `yarn start` | Start a local sirv server with built client. |
| `yarn start:inject` | Inject a given API URL and start server. |
| `yarn lint \| egrep "no-literals" -B 1` | Scan for untranslated strings. |
## License

View File

@ -1 +0,0 @@
0.5.3-1

8
default.nix Normal file
View File

@ -0,0 +1,8 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell rec {
buildInputs = [
pkgs.nodejs
pkgs.nodejs.pkgs.yarn
];
}

16
docker/package.json Normal file
View File

@ -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"
}
}

116
docker/yarn.lock Normal file
View File

@ -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
external/components vendored Submodule

@ -0,0 +1 @@
Subproject commit 4be02430c73bb4c69013a3b20b811c1391e1666d

2
external/lang vendored

@ -1 +1 @@
Subproject commit e010e46ee9f226373a253c351b50ccdeea1c8b50
Subproject commit 788261ef41e083ec917ee9bee37bc5b0e4f8d153

1
external/revolt.js vendored Submodule

@ -0,0 +1 @@
Subproject commit a45710f80cd7b4424a2114d2a32cbb83a4d95761

64
flake.lock Normal file
View File

@ -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
}

28
flake.nix Normal file
View File

@ -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
];
};
}
);
}

View File

@ -1,9 +1,10 @@
{
"version": "0.0.0",
"version": "1.0.1",
"scripts": {
"dev": "node scripts/setup_assets.js --check && vite",
"pull": "node scripts/setup_assets.js",
"build": "rimraf build && node scripts/setup_assets.js --check && vite build",
"build:deps": "cd external && cd components && yarn && yarn build:esm && cd .. && cd revolt.js && yarn && yarn build",
"build": "yarn && rimraf build && node scripts/setup_assets.js --check && yarn build:deps && vite build",
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
"preview": "vite preview",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
@ -39,34 +40,21 @@
"varsIgnorePattern": "^_"
}
],
"require-jsdoc": [
"error",
{
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true,
"ArrowFunctionExpression": false,
"FunctionExpression": false
},
"ignore": {
"MethodDefinition": [
"toJSON",
"hydrate"
]
}
}
]
"react/jsx-no-literals": "warn"
}
},
"dependencies": {
"@revoltchat/rehype-katex": "6.0.3-patch.1",
"fs-extra": "^10.0.0",
"klaw": "^3.0.0",
"lottie-react": "^2.4.0",
"sirv-cli": "^1.0.14",
"vite": "^2.6.14"
"vite": "^3.0.5"
},
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.17.9",
"@floating-ui/react-dom": "^1.0.0",
"@floating-ui/react-dom-interactions": "^0.9.1",
"@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bitter": "^4.5.7",
"@fontsource/comic-neue": "^4.4.5",
@ -87,29 +75,30 @@
"@fontsource/space-mono": "^4.4.5",
"@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@hcaptcha/react-hcaptcha": "^1.4.4",
"@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0",
"@revoltchat/ui": "1.0.33",
"@revoltchat/ui": "^1.0.77",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.38.0",
"@styled-icons/boxicons-regular": "^10.38.0",
"@styled-icons/boxicons-solid": "^10.38.0",
"@styled-icons/simple-icons": "^10.33.0",
"@styled-icons/simple-icons": "^10.45.0",
"@tippyjs/react": "4.2.6",
"@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/lodash": "^4",
"@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2",
"@types/node": "^15.12.4",
"@types/node": "^15.14.9",
"@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5",
"@types/prismjs": "^1.26.0",
"@types/react-beautiful-dnd": "^13",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
"@types/semver": "^7",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
@ -121,22 +110,26 @@
"detect-browser": "^5.2.0",
"eslint": "^7.28.0",
"eslint-config-preact": "^1.1.4",
"eslint-plugin-jsdoc": "^39.3.2",
"eslint-plugin-mobx": "^0.0.8",
"eventemitter3": "^4.0.7",
"history": "4",
"json-stringify-deterministic": "^1.0.2",
"localforage": "^1.9.0",
"lodash": "^4.17.21",
"lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0",
"long": "^5.2.0",
"markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0",
"mdast-util-to-hast": "^12.1.2",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.6.0",
"mobx-react-lite": "3.4.0",
"preact": "^10.5.14",
"preact-context-menu": "0.4.0-patch.0",
"preact-context-menu": "0.4.1",
"preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1",
"prismjs": "^1.23.0",
"prismjs": "^1.28.0",
"qrcode.react": "^3.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-device-detect": "2.2.2",
"react-helmet": "^6.1.0",
@ -145,16 +138,29 @@
"react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2",
"react-virtuoso": "^2.12.0",
"revolt.js": "6.0.1",
"rehype-prism": "^2.1.3",
"rehype-react": "^7.1.1",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"revolt.js": "6.0.17",
"rimraf": "^3.0.2",
"sass": "^1.35.1",
"semver": "^7.3.7",
"shade-blend-color": "^1.0.0",
"slate": "^0.81.1",
"slate-history": "^0.66.0",
"slate-react": "^0.81.0",
"stacktrace-js": "^2.0.2",
"styled-components": "^5.3.0",
"typescript": "^4.4.2",
"ulid": "^2.3.0",
"unified": "^10.1.2",
"unist-util-visit": "^4.1.0",
"use-resize-observer": "^7.0.0",
"vite-plugin-pwa": "^0.11.13",
"vite-plugin-pwa": "^0.12.3",
"workbox-precaching": "^6.1.5"
},
"name": "client",
@ -162,5 +168,9 @@
"repository": "https://github.com/revoltchat/revite.git",
"author": "Paul <paulmakles@gmail.com>",
"license": "MIT",
"packageManager": "yarn@3.2.0"
"packageManager": "yarn@3.2.0",
"resolutions": {
"@revoltchat/ui": "portal:external/components",
"revolt.js": "portal:external/revolt.js"
}
}

View File

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 828 B

View File

@ -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

86
src/assets/changelogs.tsx Normal file
View File

@ -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;

View File

@ -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",
// },
};

14
src/components/README.md Normal file
View File

@ -0,0 +1,14 @@
The following folders should not be added to or modified:
- `common`
- `markdown`
- `native`
- `ui`
The following are part-legacy, will remain in place and will be rewritten to some degree still:
- `navigation`
The following are mostly good to go:
- `settings`

View File

@ -4,17 +4,13 @@ import { Channel } from "revolt.js";
import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui";
import { Button, Checkbox, Preloader } from "@revoltchat/ui";
import { useApplicationState } from "../../mobx/State";
import { SECTION_NSFW } from "../../mobx/stores/Layout";
import Checkbox from "../ui/Checkbox";
import { Children } from "../../types/Preact";
const Base = styled.div`
display: flex;
flex-grow: 1;
@ -49,14 +45,36 @@ type Props = {
channel: Channel;
};
let geoBlock:
| undefined
| {
countryCode: string;
isAgeRestrictedGeo: true;
};
export default observer((props: Props) => {
const history = useHistory();
const layout = useApplicationState().layout;
const [geoLoaded, setGeoLoaded] = useState(typeof geoBlock !== "undefined");
const [ageGate, setAgeGate] = useState(false);
if (ageGate || !props.gated) {
useEffect(() => {
if (!geoLoaded) {
fetch("https://geo.revolt.chat")
.then((res) => res.json())
.then((data) => {
geoBlock = data;
setGeoLoaded(true);
});
}
}, []);
if (!geoBlock) return <Preloader type="spinner" />;
if ((ageGate && !geoBlock.isAgeRestrictedGeo) || !props.gated) {
return <>{props.children}</>;
}
if (
!(
props.channel.channel_type === "Group" ||
@ -80,23 +98,40 @@ export default observer((props: Props) => {
</a>
</span>
<Checkbox
checked={layout.getSectionState(SECTION_NSFW, false)}
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}>
<Text id="app.main.channel.nsfw.confirm" />
</Checkbox>
<div className="actions">
<Button palette="secondary" onClick={() => history.goBack()}>
<Text id="app.special.modals.actions.back" />
</Button>
<Button
palette="secondary"
onClick={() =>
layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
}>
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
</Button>
</div>
{geoBlock.isAgeRestrictedGeo ? (
<div style={{ maxWidth: "420px", textAlign: "center" }}>
{geoBlock.countryCode === "GB"
? "This channel is not available in your region while we review options on legal compliance."
: "This content is not available in your region."}
</div>
) : (
<>
<Checkbox
title={<Text id="app.main.channel.nsfw.confirm" />}
value={layout.getSectionState(SECTION_NSFW, false)}
onChange={() =>
layout.toggleSectionState(SECTION_NSFW, false)
}
/>
<div className="actions">
<Button
palette="secondary"
onClick={() => history.goBack()}>
<Text id="app.special.modals.actions.back" />
</Button>
<Button
palette="secondary"
onClick={() =>
layout.getSectionState(SECTION_NSFW) &&
setAgeGate(true)
}>
<Text
id={`app.main.channel.nsfw.${props.type}.confirm`}
/>
</Button>
</div>
</>
)}
</Base>
);
});

View File

@ -1,13 +1,17 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Link } from "react-router-dom";
import { Channel, User } from "revolt.js";
import { Emoji as CustomEmoji } from "revolt.js/esm/maps/Emojis";
import styled, { css } from "styled-components/macro";
import { StateUpdater, useState } from "preact/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import { emojiDictionary } from "../../assets/emojis";
import { useClient } from "../../controllers/client/ClientController";
import ChannelIcon from "./ChannelIcon";
import Emoji from "./Emoji";
import ServerIcon from "./ServerIcon";
import Tooltip from "./Tooltip";
import UserIcon from "./user/UserIcon";
export type AutoCompleteState =
@ -15,7 +19,7 @@ export type AutoCompleteState =
| ({ selected: number; within: boolean } & (
| {
type: "emoji";
matches: string[];
matches: (string | CustomEmoji)[];
}
| {
type: "user";
@ -60,7 +64,7 @@ export function useAutoComplete(
const cursor = el.selectionStart;
const content = el.value.slice(0, cursor);
const valid = /\w/;
const valid = /[\w\-]/;
let j = content.length - 1;
if (content[j] === "@") {
@ -88,7 +92,7 @@ export function useAutoComplete(
? "emoji"
: "user",
search.toLowerCase(),
j + 1,
current === ":" ? j + 1 : j,
];
}
}
@ -105,16 +109,23 @@ export function useAutoComplete(
if (type === "emoji") {
// ! TODO: we should convert it to a Binary Search Tree and use that
const matches = Object.keys(emojiDictionary)
.filter((emoji: string) => emoji.match(regex))
.splice(0, 5);
const matches = [
...Object.keys(emojiDictionary).filter((emoji: string) =>
emoji.match(regex),
),
...Array.from(client.emojis.values()).filter((emoji) =>
emoji.name.match(regex),
),
].splice(0, 5);
if (matches.length > 0) {
const currentPosition =
state.type !== "none" ? state.selected : 0;
setState({
// @ts-ignore-next-line are you high
type: "emoji",
// @ts-ignore-next-line
matches,
selected: Math.min(currentPosition, matches.length - 1),
within: false,
@ -234,15 +245,18 @@ export function useAutoComplete(
const content = el.value.split("");
if (state.type === "emoji") {
const selected = state.matches[state.selected];
content.splice(
index,
search.length,
state.matches[state.selected],
selected instanceof CustomEmoji
? selected._id
: selected,
": ",
);
} else if (state.type === "user") {
content.splice(
index - 1,
index,
search.length + 1,
"<@",
state.matches[state.selected]._id,
@ -250,7 +264,7 @@ export function useAutoComplete(
);
} else {
content.splice(
index - 1,
index,
search.length + 1,
"<#",
state.matches[state.selected]._id,
@ -389,12 +403,17 @@ export default function AutoComplete({
setState,
onClick,
}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
const client = useClient();
return (
<Base detached={detached}>
<div>
{state.type === "emoji" &&
state.matches.map((match, i) => (
<button
style={{
display: "flex",
justifyContent: "space-between",
}}
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
@ -413,15 +432,61 @@ export default function AutoComplete({
})
}
onClick={onClick}>
<Emoji
emoji={
(emojiDictionary as Record<string, string>)[
match
]
}
size={20}
/>
:{match}:
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
}}>
{match instanceof CustomEmoji ? (
<img
loading="lazy"
src={match.imageURL}
style={{
width: `20px`,
height: `20px`,
}}
/>
) : (
<Emoji
emoji={
(
emojiDictionary as Record<
string,
string
>
)[match]
}
size={20}
/>
)}
<span style={{ paddingLeft: "4px" }}>{`:${
match instanceof CustomEmoji
? match.name
: match
}:`}</span>
</div>
{match instanceof CustomEmoji &&
match.parent.type == "Server" && (
<>
<Tooltip
content={
client.servers.get(
match.parent.id,
)?.name
}>
<Link
to={`/server/${match.parent.id}`}>
<ServerIcon
target={client.servers.get(
match.parent.id,
)}
size={20}
/>
</Link>
</Tooltip>
</>
)}
</button>
))}
{state.type === "user" &&

View File

@ -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,

View File

@ -1,11 +1,9 @@
import { ChevronDown } from "@styled-icons/boxicons-regular";
import { Details } from "@revoltchat/ui";
import { useApplicationState } from "../../mobx/State";
import Details from "../ui/Details";
import { Children } from "../../types/Preact";
interface Props {
id: string;
defaultValue: boolean;
@ -34,7 +32,7 @@ export default function CollapsibleSection({
}
{...detailsProps}>
<summary>
<div class="padding">
<div className="padding">
<ChevronDown size={20} />
{summary}
</div>

View File

@ -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}`;

View File

@ -1,6 +1,6 @@
import { useApplicationState } from "../../mobx/State";
import { ComboBox } from "@revoltchat/ui";
import ComboBox from "../ui/ComboBox";
import { useApplicationState } from "../../mobx/State";
import { Language, Languages } from "../../../external/lang/Languages";

View File

@ -7,8 +7,9 @@ import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n";
import IconButton from "../ui/IconButton";
import { IconButton } from "@revoltchat/ui";
import { modalController } from "../../controllers/modals/ModalController";
import Tooltip from "./Tooltip";
interface Props {
@ -60,6 +61,9 @@ const ServerBanner = styled.div<Omit<Props, "server">>`
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
cursor: pointer;
color: var(--foreground);
}
}
`;
@ -121,7 +125,13 @@ export default observer(({ server }: Props) => {
</svg>
</Tooltip>
) : undefined}
<div className="title">{server.name}</div>
<a
className="title"
onClick={() =>
modalController.push({ type: "server_info", server })
}>
{server.name}
</a>
{server.havePermission("ManageServer") && (
<Link to={`/server/${server._id}/settings`}>
<IconButton>

View File

@ -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;

View File

@ -3,8 +3,6 @@ import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { Children } from "../../types/Preact";
type Props = Omit<TippyProps, "children"> & {
children: Children;
content: Children;

View File

@ -3,13 +3,13 @@ import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
import { useEffect, useState } from "preact/hooks";
import { IconButton } from "@revoltchat/ui";
import { internalSubscribe } from "../../lib/eventEmitter";
import { useApplicationState } from "../../mobx/State";
import IconButton from "../ui/IconButton";
import { updateSW } from "../../main";
import { updateSW } from "../../updateWorker";
import Tooltip from "./Tooltip";
let pendingUpdate = false;
@ -31,7 +31,7 @@ export default function UpdateIndicator({ style }: Props) {
if (style === "titlebar") {
return (
<div class="actions">
<div className="actions">
<Tooltip
content="A new update is available!"
placement="bottom">
@ -46,7 +46,7 @@ export default function UpdateIndicator({ style }: Props) {
);
}
if (window.isNative) return null;
if (window.isNative && window.native.getConfig().frame) return null;
return (
<IconButton onClick={() => updateSW(true)}>

View File

@ -5,17 +5,16 @@ import { useTriggerEvents } from "preact-context-menu";
import { memo } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import { Category } from "@revoltchat/ui";
import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import { I18nError } from "../../../context/Locale";
import Overline from "../../ui/Overline";
import { Children } from "../../../types/Preact";
import { modalController } from "../../../controllers/modals/ModalController";
import Markdown from "../../markdown/Markdown";
import UserIcon from "../user/UserIcon";
import { Username } from "../user/UserShort";
@ -26,6 +25,7 @@ import MessageBase, {
} from "./MessageBase";
import Attachment from "./attachments/Attachment";
import { MessageReply } from "./attachments/MessageReply";
import { Reactions } from "./attachments/Reactions";
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
import Embed from "./embed/Embed";
import InviteList from "./embed/EmbedInvite";
@ -33,7 +33,7 @@ import InviteList from "./embed/EmbedInvite";
interface Props {
attachContext?: boolean;
queued?: QueuedMessage;
message: MessageObject;
message: MessageObject & { webhook: { name: string; avatar?: string } };
highlight?: boolean;
contrast?: boolean;
content?: Children;
@ -52,11 +52,9 @@ const Message = observer(
queued,
hideReply,
}: Props) => {
const client = useClient();
const client = message.client;
const user = message.author;
const { openScreen } = useIntermediate();
const content = message.content;
const head =
preferHead || (message.reply_ids && message.reply_ids.length > 0);
@ -65,12 +63,16 @@ const Message = observer(
? useTriggerEvents("Menu", {
user: message.author_id,
contextualChannel: message.channel_id,
contextualMessage: message._id,
// eslint-disable-next-line
})
: undefined;
const openProfile = () =>
openScreen({ id: "profile", user_id: message.author_id });
modalController.push({
type: "user_profile",
user_id: message.author_id,
});
const handleUserClick = (e: MouseEvent) => {
if (e.shiftKey && user?._id) {
@ -87,6 +89,7 @@ const Message = observer(
// ! FIXME(?): animate on hover
const [mouseHovering, setAnimate] = useState(false);
const [reactionsOpen, setReactionsOpen] = useState(false);
useEffect(() => setAnimate(false), [replacement]);
return (
@ -115,7 +118,11 @@ const Message = observer(
}
contrast={contrast}
sending={typeof queued !== "undefined"}
mention={message.mention_ids?.includes(client.user!._id)}
mention={
message.mention_ids && client.user
? message.mention_ids.includes(client.user._id)
: undefined
}
failed={typeof queued?.error !== "undefined"}
{...(attachContext
? useTriggerEvents("Menu", {
@ -131,6 +138,11 @@ const Message = observer(
<UserIcon
className="avatar"
url={message.generateMasqAvatarURL()}
override={
message.webhook?.avatar
? `https://autumn.revolt.chat/avatars/${message.webhook.avatar}`
: undefined
}
target={user}
size={36}
onClick={handleUserClick}
@ -151,6 +163,7 @@ const Message = observer(
showServerIdentity
onClick={handleUserClick}
masquerade={message.masquerade!}
override={message.webhook?.name}
{...userContext}
/>
<MessageDetail
@ -159,10 +172,13 @@ const Message = observer(
/>
</span>
)}
{replacement ?? <Markdown content={content} />}
{replacement ??
(content && <Markdown content={content} />)}
{!queued && <InviteList message={message} />}
{queued?.error && (
<Overline type="error" error={queued.error} />
<Category>
<I18nError error={queued.error} />
</Category>
)}
{message.attachments?.map((attachment, index) => (
<Attachment
@ -177,10 +193,13 @@ const Message = observer(
{message.embeds?.map((embed, index) => (
<Embed key={index} embed={embed} />
))}
{mouseHovering &&
<Reactions message={message} />
{(mouseHovering || reactionsOpen) &&
!replacement &&
!isTouchscreenDevice && (
<MessageOverlayBar
reactionsOpen={reactionsOpen}
setReactionsOpen={setReactionsOpen}
message={message}
queued={queued}
/>

View File

@ -1,21 +1,15 @@
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
import { HappyBeaming, Send, ShieldX } from "@styled-icons/boxicons-solid";
import Axios, { CancelTokenSource } from "axios";
import Long from "long";
import { observer } from "mobx-react-lite";
import {
Channel,
DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_VIEW_ONLY,
Permission,
Server,
U32_MAX,
UserPermission,
} from "revolt.js";
import { Channel } from "revolt.js";
import styled, { css } from "styled-components/macro";
import { ulid } from "ulid";
import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { memo } from "preact/compat";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { IconButton, Picker } from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { debounce } from "../../../lib/debounce";
@ -28,20 +22,25 @@ import {
SMOOTH_SCROLL_ON_RECEIVE,
} from "../../../lib/renderer/Singleton";
import { useApplicationState } from "../../../mobx/State";
import { state, useApplicationState } from "../../../mobx/State";
import { DraftObject } from "../../../mobx/stores/Draft";
import { Reply } from "../../../mobx/stores/MessageQueue";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { dayjs } from "../../../context/Locale";
import { emojiDictionary } from "../../../assets/emojis";
import {
clientController,
useClient,
} from "../../../controllers/client/ClientController";
import { takeError } from "../../../controllers/client/jsx/error";
import {
FileUploader,
grabFiles,
uploadFile,
} from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { takeError } from "../../../context/revoltjs/util";
import IconButton from "../../ui/IconButton";
} from "../../../controllers/client/jsx/legacy/FileUploads";
import { modalController } from "../../../controllers/modals/ModalController";
import { RenderEmoji } from "../../markdown/plugins/emoji";
import AutoComplete, { useAutoComplete } from "../AutoComplete";
import { PermissionTooltip } from "../Tooltip";
import FilePreview from "./bars/FilePreview";
@ -104,7 +103,7 @@ const Blocked = styled.div`
`;
const Action = styled.div`
> div {
> a {
height: 48px;
width: 48px;
display: flex;
@ -127,7 +126,7 @@ const Action = styled.div`
`;
const FileAction = styled.div`
> div {
> a {
height: 48px;
width: 62px;
display: flex;
@ -136,6 +135,10 @@ const FileAction = styled.div`
}
`;
const FloatingLayer = styled.div`
position: relative;
`;
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
width: 16px;
`;
@ -146,6 +149,67 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
// Tests for code block delimiters (``` at start of line)
const RE_CODE_DELIMITER = new RegExp("^```", "gm");
export const HackAlertThisFileWillBeReplaced = observer(
({
onSelect,
onClose,
}: {
onSelect: (emoji: string) => void;
onClose: () => void;
}) => {
const renderEmoji = useMemo(
() =>
memo(({ emoji }: { emoji: string }) => (
<RenderEmoji match={emoji} {...({} as any)} />
)),
[],
);
const emojis: Record<string, any> = {
default: Object.keys(emojiDictionary).map((id) => ({ id })),
};
// ! FIXME: also expose typing from component
const categories: any[] = [];
for (const server of state.ordering.orderedServers) {
// ! FIXME: add a separate map on each server for emoji
const list = [...clientController.getReadyClient()!.emojis.values()]
.filter(
(emoji) =>
emoji.parent.type !== "Detached" &&
emoji.parent.id === server._id,
)
.map(({ _id, name }) => ({ id: _id, name }));
if (list.length > 0) {
emojis[server._id] = list;
categories.push({
id: server._id,
name: server.name,
iconURL: server.generateIconURL({ max_side: 256 }),
});
}
}
categories.push({
id: "default",
name: "Default",
emoji: "smiley",
});
return (
<Picker
emojis={emojis}
categories={categories}
renderEmoji={renderEmoji}
onSelect={onSelect}
onClose={onClose}
/>
);
},
);
// ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 5;
@ -157,12 +221,42 @@ export default observer(({ channel }: Props) => {
});
const [typing, setTyping] = useState<boolean | number>(false);
const [replies, setReplies] = useState<Reply[]>([]);
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const [picker, setPicker] = useState(false);
const client = useClient();
const translate = useTranslation();
const closePicker = useCallback(() => setPicker(false), []);
const renderer = getRenderer(channel);
if (channel.server?.member?.timeout) {
return (
<Base>
<Blocked>
<Action>
<PermissionTooltip
permission="SendMessages"
placement="top">
<ShieldX size={22} />
</PermissionTooltip>
</Action>
<div className="text">
<Text
id="app.main.channel.misc.timed_out"
fields={{
// TODO: make this reactive
time: dayjs().to(
channel.server.member.timeout,
true,
),
}}
/>
</div>
</Blocked>
</Base>
);
}
if (!channel.havePermission("SendMessage")) {
return (
<Base>
@ -184,7 +278,12 @@ export default observer(({ channel }: Props) => {
// Push message content to draft.
const setMessage = useCallback(
(content?: string) => state.draft.set(channel._id, content),
(content?: string) => {
const dobj: DraftObject = {
content,
};
state.draft.set(channel._id, dobj);
},
[state.draft, channel._id],
);
@ -206,7 +305,7 @@ export default observer(({ channel }: Props) => {
if (!state.draft.has(channel._id)) {
setMessage(text);
} else {
setMessage(`${state.draft.get(channel._id)}\n${text}`);
setMessage(`${state.draft.get(channel._id)?.content}\n${text}`);
}
}
@ -224,8 +323,8 @@ export default observer(({ channel }: Props) => {
if (uploadState.type === "uploading" || uploadState.type === "sending")
return;
const content = state.draft.get(channel._id)?.trim() ?? "";
if (uploadState.type === "attached") return sendFile(content);
const content = state.draft.get(channel._id)?.content?.trim() ?? "";
if (uploadState.type !== "none") return sendFile(content);
if (content.length === 0) return;
internalEmit("NewMessages", "hide");
@ -307,8 +406,11 @@ export default observer(({ channel }: Props) => {
* @returns
*/
async function sendFile(content: string) {
if (uploadState.type !== "attached") return;
if (uploadState.type !== "attached" && uploadState.type !== "failed")
return;
const attachments: string[] = [];
setMessage;
const cancel = Axios.CancelToken.source();
const files = uploadState.files;
@ -432,7 +534,7 @@ export default observer(({ channel }: Props) => {
}
function isInCodeBlock(cursor: number): boolean {
const content = state.draft.get(channel._id) || "";
const content = state.draft.get(channel._id)?.content || "";
const contentBeforeCursor = content.substring(0, cursor);
let delimiterCount = 0;
@ -482,7 +584,10 @@ export default observer(({ channel }: Props) => {
files: [...uploadState.files, ...files],
}),
() =>
openScreen({ id: "error", error: "FileTooLarge" }),
modalController.push({
type: "error",
error: "FileTooLarge",
}),
true,
)
}
@ -505,6 +610,22 @@ export default observer(({ channel }: Props) => {
replies={replies}
setReplies={setReplies}
/>
<FloatingLayer>
{picker && (
<HackAlertThisFileWillBeReplaced
onSelect={(emoji) => {
const v = state.draft.get(channel._id);
const cnt: DraftObject = {
content:
(v?.content ? `${v.content} ` : "") +
`:${emoji}:`,
};
state.draft.set(channel._id, cnt);
}}
onClose={closePicker}
/>
)}
</FloatingLayer>
<Base>
{channel.havePermission("UploadFiles") ? (
<FileAction>
@ -553,7 +674,7 @@ export default observer(({ channel }: Props) => {
id="message"
maxLength={2000}
onKeyUp={onKeyUp}
value={state.draft.get(channel._id) ?? ""}
value={state.draft.get(channel._id)?.content ?? ""}
padding="var(--message-box-padding)"
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Enter") {
@ -625,16 +746,11 @@ export default observer(({ channel }: Props) => {
onFocus={onFocus}
onBlur={onBlur}
/>
{/*<Action>
<IconButton>
<Box size={24} />
</IconButton>
</Action>
<Action>
<IconButton>
<IconButton onClick={() => setPicker(!picker)}>
<HappyBeaming size={24} />
</IconButton>
</Action>*/}
</Action>
<Action>
<IconButton
className={

View File

@ -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 (

View File

@ -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_"));

View File

@ -11,23 +11,23 @@ import styles from "./AttachmentActions.module.scss";
import classNames from "classnames";
import { useContext } from "preact/hooks";
import { IconButton } from "@revoltchat/ui";
import { determineFileSize } from "../../../../lib/fileSize";
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
import IconButton from "../../../ui/IconButton";
import { useClient } from "../../../../controllers/client/ClientController";
interface Props {
attachment: API.File;
}
export default function AttachmentActions({ attachment }: Props) {
const client = useContext(AppContext);
const client = useClient();
const { filename, metadata, size } = attachment;
const url = client.generateFileURL(attachment);
const open_url = `${url}/${filename}`;
const download_url = url?.replace("attachments", "attachments/download");
const open_url = url;
const download_url = `${url}/${filename}`;
const filesize = determineFileSize(size);
@ -49,10 +49,11 @@ export default function AttachmentActions({ attachment }: Props) {
</IconButton>
</a>
<a
target="_blank"
href={download_url}
className={styles.downloadIcon}
download
target={isFirefox || window.native ? "_blank" : "_self"}
// target={isFirefox || window.native ? "_blank" : "_self"}
rel="noreferrer">
<IconButton>
<Download size={24} />

View File

@ -2,8 +2,6 @@ import styled from "styled-components/macro";
import { Ref } from "preact";
import { Children } from "../../../../types/Preact";
const Grid = styled.div<{ width: number; height: number }>`
--width: ${(props) => props.width}px;
--height: ${(props) => props.height}px;

View File

@ -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)}

View File

@ -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>
</>
)}

View File

@ -0,0 +1,239 @@
import {
autoPlacement,
offset,
shift,
useFloating,
} from "@floating-ui/react-dom-interactions";
import { Plus } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Message } from "revolt.js";
import styled, { css } from "styled-components";
import { createPortal } from "preact/compat";
import { useCallback, useRef, useState } from "preact/hooks";
import { IconButton } from "@revoltchat/ui";
import { emojiDictionary } from "../../../../assets/emojis";
import { useClient } from "../../../../controllers/client/ClientController";
import { RenderEmoji } from "../../../markdown/plugins/emoji";
import { HackAlertThisFileWillBeReplaced } from "../MessageBox";
interface Props {
message: Message;
}
/**
* Reaction list element
*/
const List = styled.div`
gap: 0.4em;
display: flex;
flex-wrap: wrap;
margin-top: 0.2em;
align-items: center;
.add {
display: none;
}
&:hover .add {
display: grid;
}
`;
/**
* List divider
*/
const Divider = styled.div`
width: 1px;
height: 14px;
background: var(--tertiary-foreground);
`;
/**
* Reaction styling
*/
const Reaction = styled.div<{ active: boolean }>`
padding: 0.4em;
cursor: pointer;
user-select: none;
vertical-align: middle;
border: 1px solid transparent;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background: var(--secondary-background);
img {
width: 1.2em;
height: 1.2em;
object-fit: contain;
}
&:hover {
filter: brightness(0.9);
}
&:active {
filter: brightness(0.75);
}
${(props) =>
props.active &&
css`
border-color: var(--accent);
`}
`;
/**
* Render reactions on a message
*/
export const Reactions = observer(({ message }: Props) => {
const client = useClient();
const [showPicker, setPicker] = useState(false);
/**
* Render individual reaction entries
*/
const Entry = useCallback(
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
const active = user_ids?.has(client.user!._id) || false;
return (
<Reaction
active={active}
onClick={() =>
active ? message.unreact(id) : message.react(id)
}>
<RenderEmoji match={id} /> {user_ids?.size || 0}
</Reaction>
);
}),
[],
);
/**
* Determine two lists of 'required' and 'optional' reactions
*/
const { required, optional } = (() => {
const required = new Set<string>();
const optional = new Set<string>();
if (message.interactions?.reactions) {
for (const reaction of message.interactions.reactions) {
required.add(reaction);
}
}
for (const key of message.reactions.keys()) {
if (!required.has(key)) {
optional.add(key);
}
}
return {
required,
optional,
};
})();
// Don't render list if nothing is going to show anyways
if (required.size === 0 && optional.size === 0) return null;
return (
<List>
{Array.from(required, (id) => (
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
))}
{required.size !== 0 && optional.size !== 0 && <Divider />}
{Array.from(optional, (id) => (
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
))}
{message.channel?.havePermission("React") && (
<ReactionWrapper
message={message}
open={showPicker}
setOpen={setPicker}>
<IconButton className={showPicker ? "" : "add"}>
<Plus size={20} />
</IconButton>
</ReactionWrapper>
)}
</List>
);
});
const Base = styled.div`
> div {
position: unset;
}
`;
/**
* ! FIXME: rewrite
*/
export const ReactionWrapper: React.FC<{
message: Message;
open: boolean;
setOpen: (v: boolean) => void;
}> = ({ open, setOpen, message, children }) => {
const { x, y, reference, floating, strategy } = useFloating({
open,
middleware: [
offset(4),
shift({ mainAxis: true, crossAxis: true, padding: 4 }),
autoPlacement(),
],
});
const skip = useRef();
const toggle = () => {
if (skip.current) {
skip.current = null;
return;
}
setOpen(!open);
if (!open) {
skip.current = true;
}
};
return (
<>
<div
ref={reference}
onClick={toggle}
style={{ width: "fit-content" }}>
{children}
</div>
{createPortal(
<div id="reaction">
{open && (
<Base
ref={floating}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
}}>
<HackAlertThisFileWillBeReplaced
onSelect={(emoji) =>
message.react(
emojiDictionary[
emoji as keyof typeof emojiDictionary
] ?? emoji,
)
}
onClose={toggle}
/>
</Base>
)}
</div>,
document.body,
)}
</>
);
};

View File

@ -3,16 +3,12 @@ import { API } from "revolt.js";
import styles from "./Attachment.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
import {
AppContext,
StatusContext,
} from "../../../../context/revoltjs/RevoltClient";
import { Button, Preloader } from "@revoltchat/ui";
import Preloader from "../../../ui/Preloader";
import { Button } from "@revoltchat/ui";
import { useClient } from "../../../../controllers/client/ClientController";
import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline";
interface Props {
attachment: API.File;
@ -24,9 +20,8 @@ export default function TextFile({ attachment }: Props) {
const [gated, setGated] = useState(attachment.size > 100_000);
const [content, setContent] = useState<undefined | string>(undefined);
const [loading, setLoading] = useState(false);
const status = useContext(StatusContext);
const client = useContext(AppContext);
const client = useClient();
const url = client.generateFileURL(attachment)!;
useEffect(() => {
@ -57,7 +52,7 @@ export default function TextFile({ attachment }: Props) {
setLoading(false);
});
}
}, [content, loading, gated, status, attachment._id, attachment.size, url]);
}, [content, loading, gated, attachment._id, attachment.size, url]);
return (
<div

View File

@ -149,12 +149,12 @@ function FileEntry({
<EmptyEntry className="icon">
<File size={36} />
</EmptyEntry>
<div class="overlay">
<div className="overlay">
<XCircle size={36} />
</div>
</PreviewBox>
<span class="fn">{file.name}</span>
<span class="size">{determineFileSize(file.size)}</span>
<span className="fn">{file.name}</span>
<span className="size">{determineFileSize(file.size)}</span>
</Entry>
);
@ -169,13 +169,18 @@ function FileEntry({
return (
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
<PreviewBox onClick={remove}>
<img class="icon" src={url} alt={file.name} loading="eager" />
<div class="overlay">
<img
className="icon"
src={url}
alt={file.name}
loading="eager"
/>
<div className="overlay">
<XCircle size={36} />
</div>
</PreviewBox>
<span class="fn">{file.name}</span>
<span class="size">{determineFileSize(file.size)}</span>
<span className="fn">{file.name}</span>
<span className="size">{determineFileSize(file.size)}</span>
</Entry>
);
}

View File

@ -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>
);
},
);

View File

@ -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>

View File

@ -7,6 +7,8 @@ import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { StateUpdater, useEffect } from "preact/hooks";
import { IconButton } from "@revoltchat/ui";
import { internalSubscribe } from "../../../../lib/eventEmitter";
import { useApplicationState } from "../../../../mobx/State";
@ -14,8 +16,6 @@ import { SECTION_MENTION } from "../../../../mobx/stores/Layout";
import { Reply } from "../../../../mobx/stores/MessageQueue";
import Tooltip from "../../../common/Tooltip";
import IconButton from "../../../ui/IconButton";
import Markdown from "../../../markdown/Markdown";
import UserShort from "../../user/UserShort";
import { SystemMessage } from "../SystemMessage";
@ -152,11 +152,11 @@ export default observer(({ channel, replies, setReplies }: Props) => {
return (
<Base key={reply.id}>
<ReplyBase preview>
<div class="replyto">
<div className="replyto">
<Text id="app.main.channel.reply.replying" />
</div>
<div class="content">
<div class="username">
<div className="content">
<div className="username">
<UserShort
size={16}
showServerIdentity
@ -164,7 +164,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
masquerade={message.masquerade!}
/>
</div>
<div class="message">
<div className="message">
{message.attachments && (
<>
<File size={16} />
@ -185,18 +185,20 @@ export default observer(({ channel, replies, setReplies }: Props) => {
hideInfo
/>
) : (
<Markdown
disallowBigEmoji
content={message.content?.replace(
/\n/g,
" ",
)}
/>
message.content && (
<Markdown
disallowBigEmoji
content={message.content.replace(
/\n/g,
" ",
)}
/>
)
)}
</div>
</div>
</ReplyBase>
<span class="actions">
<span className="actions">
{message.author_id !== client.user!._id && (
<IconButton
onClick={() => {
@ -225,7 +227,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
content={
<Text id="app.main.channel.reply.toggle" />
}>
<span class="toggle">
<span className="toggle">
<At size={15} />
<Text
id={

View File

@ -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,
}}
/>
);
}

View File

@ -13,7 +13,6 @@
&.website {
gap: 6px;
display: flex;
flex-direction: row;
> div:nth-child(1) {

View File

@ -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)
}
/>
);
}

View File

@ -1,5 +1,4 @@
import { Group } from "@styled-icons/boxicons-solid";
import { reaction } from "mobx";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Message, API } from "revolt.js";
@ -7,20 +6,18 @@ import styled, { css } from "styled-components/macro";
import { useContext, useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui";
import { Button, Category, Preloader } from "@revoltchat/ui";
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
import {
AppContext,
ClientStatus,
StatusContext,
} from "../../../../context/revoltjs/RevoltClient";
import { takeError } from "../../../../context/revoltjs/util";
import { I18nError } from "../../../../context/Locale";
import ServerIcon from "../../../../components/common/ServerIcon";
import Overline from "../../../ui/Overline";
import Preloader from "../../../ui/Preloader";
import {
useClient,
useSession,
} from "../../../../controllers/client/ClientController";
import { takeError } from "../../../../controllers/client/jsx/error";
const EmbedInviteBase = styled.div`
width: 400px;
@ -79,8 +76,8 @@ type Props = {
export function EmbedInvite({ code }: Props) {
const history = useHistory();
const client = useContext(AppContext);
const status = useContext(StatusContext);
const session = useSession()!;
const client = session.client!;
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [joinError, setJoinError] = useState<string | undefined>(undefined);
@ -91,7 +88,7 @@ export function EmbedInvite({ code }: Props) {
useEffect(() => {
if (
typeof invite === "undefined" &&
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
(session.state === "Online" || session.state === "Ready")
) {
client
.fetchInvite(code)
@ -100,7 +97,7 @@ export function EmbedInvite({ code }: Props) {
)
.catch((err) => setError(takeError(err)));
}
}, [client, code, invite, status]);
}, [client, code, invite, session.state]);
if (typeof invite === "undefined") {
return error ? (
@ -129,8 +126,16 @@ export function EmbedInvite({ code }: Props) {
<EmbedInviteName>{invite.server_name}</EmbedInviteName>
<EmbedInviteMemberCount>
<Group size={12} />
{invite.member_count.toLocaleString()}{" "}
{invite.member_count === 1 ? "member" : "members"}
{invite.member_count != null ? (
<>
{invite.member_count.toLocaleString()}{" "}
{invite.member_count === 1
? "member"
: "members"}
</>
) : (
"N/A"
)}
</EmbedInviteMemberCount>
</EmbedInviteDetails>
{processing ? (
@ -160,7 +165,11 @@ export function EmbedInvite({ code }: Props) {
</Button>
)}
</EmbedInviteBase>
{joinError && <Overline type="error" error={joinError} />}
{joinError && (
<Category>
<I18nError error={joinError} />
</Category>
)}
</>
);
}

View File

@ -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!,
})
}

View File

@ -3,7 +3,7 @@ import { API } from "revolt.js";
import styles from "./Embed.module.scss";
import IconButton from "../../../ui/IconButton";
import { IconButton } from "@revoltchat/ui";
interface Props {
embed: API.Image;
@ -20,7 +20,7 @@ export default function EmbedMediaActions({ embed }: Props) {
</span>
<a
href={embed.url}
class={styles.openIcon}
className={styles.openIcon}
target="_blank"
rel="noreferrer">
<IconButton>

View File

@ -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" />

View File

@ -1,17 +1,24 @@
import { User } from "revolt.js";
import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
import { Checkbox, Row, Column } from "@revoltchat/ui";
import UserIcon from "./UserIcon";
import { Username } from "./UserShort";
type UserProps = Omit<CheckboxProps, "children"> & { user: User };
type UserProps = { value: boolean; onChange: (v: boolean) => void; user: User };
export default function UserCheckbox({ user, ...props }: UserProps) {
return (
<Checkbox {...props}>
<UserIcon target={user} size={32} />
<Username user={user} />
</Checkbox>
<Checkbox
{...props}
title={
<Row centred>
<UserIcon target={user} size={32} />
<Column centred>
<Username user={user} />
</Column>
</Row>
}
/>
);
}

View File

@ -7,13 +7,11 @@ import styled from "styled-components/macro";
import { openContextMenu } from "preact-context-menu";
import { Text, Localizer } from "preact-i18n";
import { Header, IconButton } from "@revoltchat/ui";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Header from "../../ui/Header";
import IconButton from "../../ui/IconButton";
import { modalController } from "../../../controllers/modals/ModalController";
import Tooltip from "../Tooltip";
import UserStatus from "./UserStatus";
@ -31,9 +29,14 @@ const HeaderBase = styled.div`
text-overflow: ellipsis;
}
.new-name {
font-size: 16px;
font-weight: 600;
}
.username {
cursor: pointer;
font-size: 16px;
font-size: 13px;
font-weight: 600;
}
@ -49,17 +52,22 @@ interface Props {
}
export default observer(({ user }: Props) => {
const { writeClipboard } = useIntermediate();
return (
<Header topBorder placement="secondary">
<Header topBorder palette="secondary">
<HeaderBase>
<div className="new-name">
{user.display_name ?? user.username}
</div>
<Localizer>
<Tooltip content={<Text id="app.special.copy_username" />}>
<span
className="username"
onClick={() => writeClipboard(user.username)}>
@{user.username}
onClick={() =>
modalController.writeText(user.username)
}>
{user.username}
{"#"}
{user.discriminator}
</span>
</Tooltip>
</Localizer>

View File

@ -1,7 +1,6 @@
import { User } from "revolt.js";
import styled from "styled-components/macro";
import { Children } from "../../../types/Preact";
import Tooltip from "../Tooltip";
import { Username } from "./UserShort";
import UserStatus from "./UserStatus";

View File

@ -6,15 +6,15 @@ import styled, { css } from "styled-components/macro";
import { useApplicationState } from "../../../mobx/State";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import fallback from "../assets/user.png";
import { useClient } from "../../../controllers/client/ClientController";
import IconBase, { IconBaseProps } from "../IconBase";
type VoiceStatus = "muted" | "deaf";
interface Props extends IconBaseProps<User> {
status?: boolean;
override?: string;
voice?: VoiceStatus;
masquerade?: API.Masquerade;
showServerIdentity?: boolean;
@ -26,6 +26,8 @@ export function useStatusColour(user?: User) {
return user?.online && user?.status?.presence !== "Invisible"
? user?.status?.presence === "Idle"
? theme.getVariable("status-away")
: user?.status?.presence === "Focus"
? theme.getVariable("status-focus")
: user?.status?.presence === "Busy"
? theme.getVariable("status-busy")
: theme.getVariable("status-online")
@ -69,12 +71,15 @@ export default observer(
showServerIdentity,
masquerade,
innerRef,
override,
...svgProps
} = props;
let { url } = props;
if (masquerade?.avatar) {
url = client.proxyFile(masquerade.avatar);
} else if (override) {
url = override;
} else if (!url) {
let override;
if (target && showServerIdentity) {
@ -114,7 +119,7 @@ export default observer(
y="0"
width="32"
height="32"
class="icon"
className="icon"
mask={mask ?? (status ? "url(#user)" : undefined)}>
{<img src={url} draggable={false} loading="lazy" />}
</foreignObject>

View File

@ -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) {

View File

@ -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" />;
}

View File

@ -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;
}
}

View File

@ -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}>

View File

@ -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>;
});

View File

@ -1,292 +0,0 @@
/* eslint-disable react-hooks/rules-of-hooks */
import MarkdownKatex from "@traptitech/markdown-it-katex";
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it";
// @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
import { RE_MENTIONS } from "revolt.js";
import styles from "./Markdown.module.scss";
import { useCallback, useContext } from "preact/hooks";
import { internalEmit } from "../../lib/eventEmitter";
import { determineLink } from "../../lib/links";
import { dayjs } from "../../context/Locale";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { generateEmoji } from "../common/Emoji";
import { emojiDictionary } from "../../assets/emojis";
import { MarkdownProps } from "./Markdown";
import Prism from "./prism";
// TODO: global.d.ts file for defining globals
declare global {
interface Window {
copycode: (element: HTMLDivElement) => void;
}
}
// Handler for code block copy.
if (typeof window !== "undefined") {
window.copycode = function (element: HTMLDivElement) {
try {
const code = element.parentElement?.parentElement?.children[1];
if (code) {
navigator.clipboard.writeText(code.textContent?.trim() ?? "");
}
} catch (e) {}
};
}
export const md: MarkdownIt = MarkdownIt({
breaks: true,
linkify: true,
highlight: (str, lang) => {
const v = Prism.languages[lang];
if (v) {
const out = Prism.highlight(str, v, lang);
return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
}
return `<pre class="code"><code>${md.utils.escapeHtml(
str,
)}</code></pre>`;
},
})
.disable("image")
.use(MarkdownEmoji, { defs: emojiDictionary })
.use(MarkdownSpoilers)
.use(MarkdownKatex, {
throwOnError: false,
maxExpand: 0,
maxSize: 10,
strict: false,
errorColor: "var(--error)",
});
md.linkify.set({ fuzzyLink: false });
// TODO: global.d.ts file for defining globals
declare global {
interface Window {
internalHandleURL: (element: HTMLAnchorElement) => void;
}
}
// Include emojis.
md.renderer.rules.emoji = function (token, idx) {
return generateEmoji(token[idx].content);
};
// Force line breaks.
// https://github.com/markdown-it/markdown-it/issues/211#issuecomment-508380611
const defaultParagraphRenderer =
md.renderer.rules.paragraph_open ||
((tokens, idx, options, env, self) =>
self.renderToken(tokens, idx, options));
md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) {
let result = "";
if (idx > 1) {
const inline = tokens[idx - 2];
const paragraph = tokens[idx];
if (
inline.type === "inline" &&
inline.map &&
inline.map[1] &&
paragraph.map &&
paragraph.map[0]
) {
const diff = paragraph.map[0] - inline.map[1];
if (diff > 0) {
result = "<br>".repeat(diff);
}
}
}
return result + defaultParagraphRenderer(tokens, idx, options, env, self);
};
const RE_TWEMOJI = /:(\w+):/g;
// ! FIXME: Move to library
const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
const RE_TIME = /<t:([0-9]+):(\w)>/g;
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const client = useContext(AppContext);
const { openLink } = useIntermediate();
if (typeof content === "undefined") return null;
if (!content || content.length === 0) return null;
// We replace the message with the mention at the time of render.
// We don't care if the mention changes.
const newContent = content
.replace(RE_TIME, (sub: string, ...args: unknown[]) => {
if (isNaN(args[0] as number)) return sub;
const date = dayjs.unix(args[0] as number);
const format = args[1] as string;
let final = "";
switch (format) {
case "t":
final = date.format("hh:mm");
break;
case "T":
final = date.format("hh:mm:ss");
break;
case "R":
final = date.fromNow();
break;
case "D":
final = date.format("DD MMMM YYYY");
break;
case "F":
final = date.format("dddd, DD MMMM YYYY hh:mm");
break;
default:
final = date.format("DD MMMM YYYY hh:mm");
break;
}
return `\`${final}\``;
})
.replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
const id = args[0] as string,
user = client.users.get(id);
if (user) {
return `[@${user.username}](/@${id})`;
}
return sub;
})
.replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
const id = args[0] as string,
channel = client.channels.get(id);
if (channel?.channel_type === "TextChannel") {
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
}
return sub;
});
const useLargeEmojis = disallowBigEmoji
? false
: content.replace(RE_TWEMOJI, "").trim().length === 0;
const toggle = useCallback((ev: MouseEvent) => {
if (ev.currentTarget) {
const element = ev.currentTarget as HTMLDivElement;
if (element.classList.contains("spoiler")) {
element.classList.add("shown");
}
}
}, []);
const handleLink = useCallback(
(ev: MouseEvent) => {
if (ev.currentTarget) {
const element = ev.currentTarget as HTMLAnchorElement;
if (ev.shiftKey) {
switch (element.dataset.type) {
case "mention": {
internalEmit(
"MessageBox",
"append",
`<@${element.dataset.mentionId}>`,
"mention",
);
ev.preventDefault();
return;
}
case "channel_mention": {
internalEmit(
"MessageBox",
"append",
`<#${element.dataset.mentionId}>`,
"channel_mention",
);
ev.preventDefault();
return;
}
}
}
if (openLink(element.href)) {
ev.preventDefault();
}
}
},
[openLink],
);
return (
<span
ref={(el) => {
if (el) {
el.querySelectorAll<HTMLDivElement>(".spoiler").forEach(
(element) => {
element.removeEventListener("click", toggle);
element.addEventListener("click", toggle);
},
);
el.querySelectorAll<HTMLAnchorElement>("a").forEach(
(element) => {
element.removeEventListener("click", handleLink);
element.addEventListener("click", handleLink);
element.removeAttribute("data-type");
element.removeAttribute("data-mention-id");
element.removeAttribute("target");
const link = determineLink(element.href);
switch (link.type) {
case "profile": {
element.setAttribute(
"data-type",
"mention",
);
element.setAttribute(
"data-mention-id",
link.id,
);
break;
}
case "navigate": {
if (link.navigation_type === "channel") {
element.setAttribute(
"data-type",
"channel_mention",
);
element.setAttribute(
"data-mention-id",
link.channel_id,
);
}
break;
}
case "external": {
element.setAttribute("target", "_blank");
element.setAttribute("rel", "noreferrer");
break;
}
}
},
);
}
}}
className={styles.markdown}
dangerouslySetInnerHTML={{
__html: md.render(newContent),
}}
data-large-emojis={useLargeEmojis}
/>
);
}

View File

@ -0,0 +1,7 @@
import { passThroughComponents } from "./plugins/remarkRegexComponent";
import { timestampHandler } from "./plugins/timestamps";
export const handlers = {
...passThroughComponents("emoji", "spoiler", "mention", "channel"),
timestamp: timestampHandler,
};

View File

@ -0,0 +1,79 @@
import styled from "styled-components";
import { useCallback, useRef } from "preact/hooks";
import { Tooltip } from "@revoltchat/ui";
import { modalController } from "../../../controllers/modals/ModalController";
/**
* Base codeblock styles
*/
const Base = styled.pre`
padding: 1em;
overflow-x: scroll;
background: var(--block);
border-radius: var(--border-radius);
`;
/**
* Copy codeblock contents button styles
*/
const Lang = styled.div`
font-family: var(--monospace-font);
width: fit-content;
padding-bottom: 8px;
a {
color: #111;
cursor: pointer;
padding: 2px 6px;
font-weight: 600;
user-select: none;
display: inline-block;
background: var(--accent);
font-size: 10px;
text-transform: uppercase;
box-shadow: 0 2px #787676;
border-radius: calc(var(--border-radius) / 3);
&:active {
transform: translateY(1px);
box-shadow: 0 1px #787676;
}
}
`;
/**
* Render a codeblock with copy text button
*/
export const RenderCodeblock: React.FC<{ class: string }> = ({
children,
...props
}) => {
const ref = useRef<HTMLPreElement>(null);
let text = "text";
if (props.class) {
text = props.class.split("-")[1];
}
const onCopy = useCallback(() => {
const text = ref.current?.querySelector("code")?.innerText;
text && modalController.writeText(text);
}, [ref]);
return (
<Base ref={ref}>
<Lang>
<Tooltip content="Copy to Clipboard" placement="top">
{/**
// @ts-expect-error Preact-React */}
<a onClick={onCopy}>{text}</a>
</Tooltip>
</Lang>
{children}
</Base>
);
};

View File

@ -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()
}
/>
);
}

View File

@ -0,0 +1,21 @@
import { Link } from "react-router-dom";
import { clientController } from "../../../controllers/client/ClientController";
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
export function RenderChannel({ match }: CustomComponentProps) {
const channel = clientController.getAvailableClient().channels.get(match)!;
return (
<Link
to={`${
channel.server_id ? `/server/${channel.server_id}` : ""
}/channel/${match}`}>{`#${channel.name}`}</Link>
);
}
export const remarkChannels = createComponent(
"channel",
/<#([A-z0-9]{26})>/g,
(match) => clientController.getAvailableClient().channels.has(match),
);

View File

@ -0,0 +1,66 @@
import styled from "styled-components";
import { useState } from "preact/hooks";
import { emojiDictionary } from "../../../assets/emojis";
import { clientController } from "../../../controllers/client/ClientController";
import { parseEmoji } from "../../common/Emoji";
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
const Emoji = styled.img`
object-fit: contain;
height: var(--emoji-size);
width: var(--emoji-size);
margin: 0 0.05em 0 0.1em;
vertical-align: -0.2em;
img:before {
content: " ";
display: block;
position: absolute;
height: 50px;
width: 50px;
background-image: url(ishere.jpg);
}
`;
const RE_EMOJI = /:([a-zA-Z0-9\-_]+):/g;
const RE_ULID = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
export function RenderEmoji({ match }: CustomComponentProps) {
const [fail, setFail] = useState(false);
const url = RE_ULID.test(match)
? `${
clientController.getAvailableClient().configuration?.features
.autumn.url
}/emojis/${match}/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;
}

View File

@ -0,0 +1,10 @@
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
export const remarkHtmlToText: Plugin = () => {
return (tree) => {
visit(tree, "html", (node: { type: string; value: string }) => {
node.type = "text";
});
};
};

View File

@ -0,0 +1,53 @@
import { RE_MENTIONS } from "revolt.js";
import styled from "styled-components";
import { clientController } from "../../../controllers/client/ClientController";
import UserShort from "../../common/user/UserShort";
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
const Mention = styled.a`
gap: 4px;
flex-shrink: 0;
padding-left: 2px;
padding-right: 6px;
align-items: center;
display: inline-flex;
vertical-align: middle;
cursor: pointer;
font-weight: 600;
text-decoration: none !important;
background: var(--secondary-background);
border-radius: calc(var(--border-radius) * 2);
transition: 0.1s ease filter;
&:hover {
filter: brightness(0.75);
}
&:active {
filter: brightness(0.65);
}
svg {
width: 1em;
height: 1em;
}
`;
export function RenderMention({ match }: CustomComponentProps) {
return (
<Mention>
<UserShort
showServerIdentity
user={clientController.getAvailableClient().users.get(match)}
/>
</Mention>
);
}
export const remarkMention = createComponent("mention", RE_MENTIONS, (match) =>
clientController.getAvailableClient().users.has(match),
);

View File

@ -0,0 +1,108 @@
import type { Handler } from "mdast-util-to-hast";
import type { Plugin } from "unified";
import { visit } from "unist-util-visit";
/**
* Props given to custom components
*/
export interface CustomComponentProps {
type?: string;
match: string;
arg1?: string;
}
/**
* Create a new custom component matched by a given RegExp
* @param type hast node type
* @param regex Regex to match (must have one capture group)
* @returns Unified Plugin
*/
export function createComponent(
type: string,
regex: RegExp,
validator?: (match: string) => boolean,
): Plugin {
/**
* Plugin which transforms a given RegExp into a custom component with given name.
*/
return () => {
return (tree) => {
visit(
tree,
"text",
(
node: { value: string },
index: number,
parent: { children: any[] },
) => {
const result = [];
let start = 0;
regex.lastIndex = 0;
let match = regex.exec(node.value);
while (match) {
if (!validator || validator(match[1])) {
const position = match.index;
if (start !== position) {
result.push({
type: "text",
value: node.value.slice(start, position),
});
}
result.push({
type,
match: match[1],
arg1: match[2],
});
start = position + match[0].length;
}
match = regex.exec(node.value);
}
if (
result.length > 0 &&
parent &&
typeof index === "number"
) {
if (start < node.value.length) {
result.push({
type: "text",
value: node.value.slice(start),
});
}
parent.children.splice(index, 1, ...result);
return index + result.length;
}
},
);
};
};
}
/**
* Pass-through a component as-is from remark to rehype
* @param name Tag name
* @returns Handler
*/
export const passThroughRehype: (name: string) => Handler =
(name: string) => (h, node) =>
h(node, name, node);
/**
* Pass-through multiple components at once
* @param keys Tags
* @returns Handlers
*/
export const passThroughComponents = (...keys: string[]) => {
const obj: Record<string, Handler> = {};
for (const key of keys) {
obj[key] = passThroughRehype(key);
}
return obj;
};

View File

@ -0,0 +1,45 @@
import styled, { css } from "styled-components";
import { useState } from "preact/hooks";
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
const Spoiler = styled.span<{ shown: boolean }>`
padding: 0 2px;
cursor: pointer;
user-select: none;
color: transparent;
background: #151515;
border-radius: var(--border-radius);
> * {
opacity: 0;
pointer-events: none;
}
${(props) =>
props.shown &&
css`
cursor: auto;
user-select: all;
color: var(--foreground);
background: var(--secondary-background);
> * {
opacity: 1;
pointer-events: unset;
}
`}
`;
export function RenderSpoiler({ match }: CustomComponentProps) {
const [shown, setShown] = useState(false);
return (
<Spoiler shown={shown} onClick={() => setShown(true)}>
{match}
</Spoiler>
);
}
export const remarkSpoiler = createComponent("spoiler", /!!([^!]+)!!/g);

View File

@ -0,0 +1,39 @@
import type { Handler } from "mdast-util-to-hast";
import { dayjs } from "../../../context/Locale";
import { createComponent } from "./remarkRegexComponent";
export const timestampHandler: Handler = (h, { match, arg1 }) => {
if (isNaN(match)) return { type: "text", value: match };
const date = dayjs.unix(match);
let value = "";
switch (arg1) {
case "t":
value = date.format("hh:mm");
break;
case "T":
value = date.format("hh:mm:ss");
break;
case "R":
value = date.fromNow();
break;
case "D":
value = date.format("DD MMMM YYYY");
break;
case "F":
value = date.format("dddd, DD MMMM YYYY hh:mm");
break;
default:
value = date.format("DD MMMM YYYY hh:mm");
break;
}
return h(null, "code", {}, [{ type: "text", value }]);
};
export const remarkTimestamps = createComponent(
"timestamp",
/<t:([0-9]+)(?::(\w))?>/g,
);

View File

@ -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";

View File

@ -98,7 +98,7 @@ const TitlebarBase = styled.div<Props>`
export function Titlebar(props: Props) {
return (
<TitlebarBase {...props}>
<div class="title">
<div className="title">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 193.733 37.438">
@ -114,7 +114,7 @@ export function Titlebar(props: Props) {
<Wrench size="12.5" />
)}
</div>
{/*<div class="actions quick">
{/*<div className="actions quick">
<Tooltip
content="Mute"
placement="bottom">
@ -130,9 +130,9 @@ export function Titlebar(props: Props) {
</div>
</Tooltip>
</div>*/}
<div class="drag" />
<div className="drag" />
<UpdateIndicator style="titlebar" />
<div class="actions">
<div className="actions">
<div onClick={window.native.min}>
<svg
aria-hidden="false"
@ -164,7 +164,7 @@ export function Titlebar(props: Props) {
/>
</svg>
</div>
<div onClick={window.native.close} class="error">
<div onClick={window.native.close} className="error">
<svg
aria-hidden="false"
width="12"

View File

@ -3,14 +3,14 @@ import { observer } from "mobx-react-lite";
import { useHistory, useLocation } from "react-router";
import styled, { css } from "styled-components/macro";
import { Centred, IconButton } from "@revoltchat/ui";
import ConditionalLink from "../../lib/ConditionalLink";
import { useApplicationState } from "../../mobx/State";
import { useClient } from "../../context/revoltjs/RevoltClient";
import { useClient } from "../../controllers/client/ClientController";
import UserIcon from "../common/user/UserIcon";
import IconButton from "../ui/IconButton";
const Base = styled.div`
background: var(--secondary-background);
@ -24,8 +24,19 @@ const Navbar = styled.div`
height: var(--bottom-navigation-height);
`;
/**
* I've decided that this whole component
* needs to be re-written 👍👍👍👍👍👍
*/
const Button = styled.a<{ active: boolean }>`
flex: 1;
color: var(--foreground);
// ok
* {
color: var(--foreground) !important;
}
> a,
> div,
@ -63,7 +74,7 @@ export default observer(() => {
<Base>
<Navbar>
<Button active={homeActive}>
<IconButton
<Centred
onClick={() => {
if (settingsActive) {
if (history.length > 0) {
@ -80,14 +91,14 @@ export default observer(() => {
}
}}>
<Message size={24} />
</IconButton>
</Centred>
</Button>
<Button active={friendsActive}>
<ConditionalLink active={friendsActive} to="/friends">
<IconButton>
<IconButton>
<ConditionalLink active={friendsActive} to="/friends">
<Group size={25} />
</IconButton>
</ConditionalLink>
</ConditionalLink>
</IconButton>
</Button>
{/*<Button active={searchActive}>
<ConditionalLink active={searchActive} to="/search">
@ -104,20 +115,20 @@ export default observer(() => {
</ConditionalLink>
</Button>*/}
<Button active={discoverActive}>
<ConditionalLink
active={discoverActive}
to="/discover/servers">
<IconButton>
<IconButton>
<ConditionalLink
active={discoverActive}
to="/discover/servers">
<Compass size={24} />
</IconButton>
</ConditionalLink>
</ConditionalLink>
</IconButton>
</Button>
<Button active={settingsActive}>
<ConditionalLink active={settingsActive} to="/settings">
<IconButton>
<IconButton>
<ConditionalLink active={settingsActive} to="/settings">
<UserIcon target={user} size={26} status={true} />
</IconButton>
</ConditionalLink>
</ConditionalLink>
</IconButton>
</Button>
</Navbar>
</Base>

View File

@ -5,23 +5,20 @@ import { User, Channel } from "revolt.js";
import styles from "./Item.module.scss";
import classNames from "classnames";
import { Ref } from "preact";
import { useTriggerEvents } from "preact-context-menu";
import { Localizer, Text } from "preact-i18n";
import { IconButton } from "@revoltchat/ui";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { stopPropagation } from "../../../lib/stopPropagation";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { modalController } from "../../../controllers/modals/ModalController";
import ChannelIcon from "../../common/ChannelIcon";
import Tooltip from "../../common/Tooltip";
import UserIcon from "../../common/user/UserIcon";
import { Username } from "../../common/user/UserShort";
import UserStatus from "../../common/user/UserStatus";
import IconButton from "../../ui/IconButton";
import { Children } from "../../../types/Preact";
type CommonProps = Omit<
JSX.HTMLAttributes<HTMLDivElement>,
@ -52,7 +49,6 @@ export const UserButton = observer((props: UserProps) => {
channel,
...divProps
} = props;
const { openScreen } = useIntermediate();
return (
<div
@ -113,8 +109,7 @@ export const UserButton = observer((props: UserProps) => {
className={styles.icon}
onClick={(e) =>
stopPropagation(e) &&
openScreen({
id: "special_prompt",
modalController.push({
type: "close_dm",
target: channel,
})
@ -151,7 +146,6 @@ export const ChannelButton = observer((props: ChannelProps) => {
return <UserButton {...{ active, alert, channel, user }} />;
}
const { openScreen } = useIntermediate();
const alerting = alert && !muted && !active;
return (
@ -161,16 +155,16 @@ export const ChannelButton = observer((props: ChannelProps) => {
data-alert={alerting}
data-muted={muted}
aria-label={channel.name}
className={classNames(styles.item, { [styles.compact]: compact })}
className={classNames(styles.item, {
[styles.compact]: compact,
})}
{...useTriggerEvents("Menu", {
channel: channel._id,
unread: !!alert,
})}>
<ChannelIcon
className={styles.avatar}
target={channel}
size={compact ? 24 : 32}
/>
<div className={styles.avatar}>
<ChannelIcon target={channel} size={compact ? 24 : 32} />
</div>
<div className={styles.name}>
<div>{channel.name}</div>
{channel.channel_type === "Group" && (
@ -183,7 +177,9 @@ export const ChannelButton = observer((props: ChannelProps) => {
<Text
id="quantities.members"
plural={channel.recipients!.length}
fields={{ count: channel.recipients!.length }}
fields={{
count: channel.recipients!.length,
}}
/>
)}
</div>
@ -199,8 +195,7 @@ export const ChannelButton = observer((props: ChannelProps) => {
<IconButton
className={styles.icon}
onClick={() =>
openScreen({
id: "special_prompt",
modalController.push({
type: "leave_group",
target: channel,
})

View File

@ -1,45 +1,47 @@
import { observer } from "mobx-react-lite";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import {
ClientStatus,
StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient";
import { Banner, Button, Column } from "@revoltchat/ui";
import Banner from "../../ui/Banner";
import { useSession } from "../../../controllers/client/ClientController";
export default function ConnectionStatus() {
const status = useContext(StatusContext);
const client = useClient();
function ConnectionStatus() {
const session = useSession()!;
if (status === ClientStatus.OFFLINE) {
if (session.state === "Offline") {
return (
<Banner>
<Text id="app.special.status.offline" />
</Banner>
);
} else if (status === ClientStatus.DISCONNECTED) {
} else if (session.state === "Disconnected") {
return (
<Banner>
<Text id="app.special.status.disconnected" /> <br />
<a onClick={() => client.websocket.connect()}>
<Text id="app.special.status.reconnect" />
</a>
<Column centred>
<Text id="app.special.status.disconnected" />
<Button
compact
palette="secondary"
onClick={() =>
session.emit({
action: "RETRY",
})
}>
<Text id="app.status.reconnect" />
</Button>
</Column>
</Banner>
);
} else if (status === ClientStatus.CONNECTING) {
return (
<Banner>
<Text id="app.special.status.connecting" />
</Banner>
);
} else if (status === ClientStatus.RECONNECTING) {
} else if (session.state === "Connecting") {
return (
<Banner>
<Text id="app.special.status.reconnecting" />
</Banner>
);
}
return null;
}
export default observer(ConnectionStatus);

View File

@ -1,3 +1,4 @@
import { Plus } from "@styled-icons/boxicons-regular";
import {
Home,
UserDetail,
@ -11,18 +12,18 @@ import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks";
import { Category, IconButton } from "@revoltchat/ui";
import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Category from "../../ui/Category";
import placeholderSVG from "../items/placeholder.svg";
import { useClient } from "../../../controllers/client/ClientController";
import { modalController } from "../../../controllers/modals/ModalController";
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus";
@ -44,10 +45,9 @@ const Navbar = styled.div`
export default observer(() => {
const { pathname } = useLocation();
const client = useContext(AppContext);
const client = useClient();
const state = useApplicationState();
const { channel: channel_id } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate();
const channels = [...client.channels.values()].filter(
(x) =>
@ -125,15 +125,17 @@ export default observer(() => {
</ButtonItem>
</Link>
)}
<Category
text={<Text id="app.main.categories.conversations" />}
action={() =>
openScreen({
id: "special_input",
type: "create_group",
})
}
/>
<Category>
<Text id="app.main.categories.conversations" />
<IconButton
onClick={() =>
modalController.push({
type: "create_group",
})
}>
<Plus size={16} />
</IconButton>
</Category>
{channels.length === 0 && (
<img src={placeholderSVG} loading="eager" />
)}

View File

@ -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}
/>
);
});

View File

@ -1,12 +1,12 @@
import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router";
import { Server } from "revolt.js";
import styled, { css } from "styled-components/macro";
import { Ref } from "preact";
import { useTriggerEvents } from "preact-context-menu";
import { useEffect } from "preact/hooks";
import { Category } from "@revoltchat/ui";
import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter";
import { internalEmit } from "../../../lib/eventEmitter";
@ -14,12 +14,9 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import { useClient } from "../../../controllers/client/ClientController";
import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader";
import Category from "../../ui/Category";
import { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus";
@ -53,8 +50,10 @@ const ServerList = styled.div`
export default observer(() => {
const client = useClient();
const state = useApplicationState();
const { server: server_id, channel: channel_id } =
useParams<{ server: string; channel?: string }>();
const { server: server_id, channel: channel_id } = useParams<{
server: string;
channel?: string;
}>();
const server = client.servers.get(server_id);
if (!server) return <Redirect to="/" />;
@ -122,14 +121,18 @@ export default observer(() => {
channels.push(addChannel(id));
}
elements.push(
<CollapsibleSection
id={`category_${category.id}`}
defaultValue
summary={<Category text={category.title} />}>
{channels}
</CollapsibleSection>,
);
if (category.title === "Default") {
elements.push(...channels);
} else {
elements.push(
<CollapsibleSection
id={`category_${category.id}`}
defaultValue
summary={<Category>{category.title}</Category>}>
{channels}
</CollapsibleSection>,
);
}
}
}

View File

@ -8,11 +8,7 @@ import { memo } from "preact/compat";
import { internalEmit } from "../../../lib/eventEmitter";
import {
Screen,
useIntermediate,
} from "../../../context/intermediate/Intermediate";
import { modalController } from "../../../controllers/modals/ModalController";
import { UserButton } from "../items/ButtonItem";
export type MemberListGroup = {
@ -55,15 +51,7 @@ const NoOomfie = styled.div`
`;
const ItemContent = memo(
({
item,
context,
openScreen,
}: {
item: User;
context: Channel;
openScreen: (screen: Screen) => void;
}) => (
({ item, context }: { item: User; context: Channel }) => (
<UserButton
key={item._id}
user={item}
@ -77,13 +65,12 @@ const ItemContent = memo(
`<@${item._id}>`,
"mention",
);
} else
[
openScreen({
id: "profile",
user_id: item._id,
}),
];
} else {
modalController.push({
type: "user_profile",
user_id: item._id,
});
}
}}
/>
),
@ -96,8 +83,6 @@ export default function MemberList({
entries: MemberListGroup[];
context: Channel;
}) {
const { openScreen } = useIntermediate();
return (
<GroupedVirtuoso
groupCounts={entries.map((x) => x.users.length)}
@ -114,7 +99,7 @@ export default function MemberList({
)}
{entry.type !== "no_offline" && (
<>
{" - "}
{" "}
{entry.users.length}
</>
)}
@ -133,19 +118,20 @@ export default function MemberList({
return (
<NoOomfie>
<div>
Offline users temporarily disabled for this
server, see issue{" "}
Offline users have temporarily been disabled for
larger servers - see{" "}
<a
href="https://github.com/revoltchat/delta/issues/128"
target="_blank">
#128
href="https://github.com/revoltchat/backend/issues/178"
target="_blank"
rel="noreferrer">
issue #178
</a>{" "}
for when this will be resolved.
</div>
<div>
You may re-enable them in{" "}
You may re-enable them{" "}
<Link to="/settings/experiments">
<a>experiments</a>
<a>here</a>
</Link>
.
</div>
@ -158,11 +144,7 @@ export default function MemberList({
return (
<div>
<ItemContent
item={item}
context={context}
openScreen={openScreen}
/>
<ItemContent item={item} context={context} />
</div>
);
}}

View File

@ -4,14 +4,12 @@ import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom";
import { Channel, Server, User, API } from "revolt.js";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
import {
ClientStatus,
StatusContext,
useSession,
useClient,
} from "../../../context/revoltjs/RevoltClient";
} from "../../../controllers/client/ClientController";
import { GenericSidebarBase } from "../SidebarBase";
import MemberList, { MemberListGroup } from "./MemberList";
@ -78,17 +76,25 @@ function useEntries(
keys.forEach((key) => {
let u;
let member;
if (isServer) {
const { server, user } = JSON.parse(key);
if (server !== channel.server_id) return;
u = client.users.get(user);
member = client.members.get(key);
if (!member?.hasPermission(channel, "ViewChannel")) {
return;
}
} else {
u = client.users.get(key);
member = client.members.get(key);
}
if (!u) return;
const member = client.members.get(key);
const sort = member?.nickname ?? u.username;
const entry = [u, sort] as [User, string];
@ -182,7 +188,7 @@ export const GroupMemberSidebar = observer(
);
// ! FIXME: this is temporary code until we get lazy guilds like subscriptions
const FETCHED: Set<String> = new Set();
const FETCHED: Set<string> = new Set();
export function resetMemberSidebarFetched() {
FETCHED.clear();
@ -205,18 +211,18 @@ function shouldSkipOffline(id: string) {
export const ServerMemberSidebar = observer(
({ channel }: { channel: Channel }) => {
const client = useClient();
const status = useContext(StatusContext);
const session = useSession()!;
const client = session.client!;
useEffect(() => {
const server_id = channel.server_id!;
if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) {
if (session.state === "Online" && !FETCHED.has(server_id)) {
FETCHED.add(server_id);
channel
.server!.syncMembers(shouldSkipOffline(server_id))
.catch(() => FETCHED.delete(server_id));
}
}, [status, channel]);
}, [session.state, channel]);
const entries = useEntries(
channel,

View File

@ -5,15 +5,10 @@ import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
import { useClient } from "../../../controllers/client/ClientController";
import Message from "../../common/messaging/Message";
import InputBox from "../../ui/InputBox";
import Overline from "../../ui/Overline";
import Preloader from "../../ui/Preloader";
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
type SearchState =
@ -103,18 +98,20 @@ export function SearchSidebar({ close }: Props) {
<GenericSidebarBase data-scroll-offset="with-padding">
<GenericSidebarList>
<SearchBase>
<Overline type="accent" block hover>
<a onClick={close}>« back to members</a>
</Overline>
<Overline type="subtle" block>
<Category>
<Error
error={<a onClick={close}>« back to members</a>}
/>
</Category>
<Category>
<Text id="app.main.channel.search.title" />
</Overline>
</Category>
<InputBox
value={query}
onKeyDown={(e) => e.key === "Enter" && search()}
onChange={(e) => setQuery(e.currentTarget.value)}
/>
<div class="sort">
<div className="sort">
{["Latest", "Oldest", "Relevance"].map((key) => (
<Button
key={key}
@ -129,7 +126,7 @@ export function SearchSidebar({ close }: Props) {
</div>
{state.type === "loading" && <Preloader type="ring" />}
{state.type === "results" && (
<div class="list">
<div className="list">
{state.results.map((message) => {
let href = "";
if (channel?.channel_type === "TextChannel") {
@ -140,7 +137,7 @@ export function SearchSidebar({ close }: Props) {
return (
<Link to={href} key={message._id}>
<div class="message">
<div className="message">
<Message
message={message}
head

View File

@ -1,279 +0,0 @@
import { Brush } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import { Text } from "preact-i18n";
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
import { useApplicationState } from "../../mobx/State";
import {
Fonts,
FONTS,
FONT_KEYS,
MonospaceFonts,
MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS,
} from "../../context/Theme";
import Checkbox from "../ui/Checkbox";
import ColourSwatches from "../ui/ColourSwatches";
import ComboBox from "../ui/ComboBox";
import Radio from "../ui/Radio";
import CategoryButton from "../ui/fluent/CategoryButton";
import { EmojiSelector } from "./appearance/EmojiSelector";
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
/**
* Component providing a way to switch the base theme being used.
*/
export const ThemeBaseSelectorShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<ThemeBaseSelector
value={theme.isModified() ? undefined : theme.getBase()}
setValue={(base) => {
theme.setBase(base);
theme.reset();
}}
/>
);
});
/**
* Component providing a link to the theme shop.
* Only appears if experiment is enabled.
* TODO: stabilise
*/
export const ThemeShopShim = () => {
return (
<Link to="/discover/themes" replace>
<CategoryButton
icon={<Brush size={24} />}
action="chevron"
description={
<Text id="app.settings.pages.appearance.discover.description" />
}
hover>
<Text id="app.settings.pages.appearance.discover.title" />
</CategoryButton>
</Link>
);
};
/**
* Component providing a way to change current accent colour.
*/
export const ThemeAccentShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.accent_selector" />
</h3>
<ColourSwatches
value={theme.getVariable("accent")}
onChange={(colour) => {
theme.setVariable("accent", colour as string);
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
}}
/>
</>
);
});
/**
* Component providing a way to edit custom CSS.
*/
export const ThemeCustomCSSShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.custom_css" />
</h3>
<TextAreaAutoSize
maxRows={20}
minHeight={480}
code
value={theme.getCSS() ?? ""}
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
/>
</>
);
});
/**
* Component providing a way to switch between compact and normal message view.
*/
export const DisplayCompactShim = () => {
// TODO: WIP feature
return (
<>
<h3>
<Text id="app.settings.pages.appearance.message_display" />
</h3>
<div /* className={styles.display} */>
<Radio
description={
<Text id="app.settings.pages.appearance.display.default_description" />
}
checked>
<Text id="app.settings.pages.appearance.display.default" />
</Radio>
<Radio
description={
<Text id="app.settings.pages.appearance.display.compact_description" />
}
disabled>
<Text id="app.settings.pages.appearance.display.compact" />
</Radio>
</div>
</>
);
};
/**
* Component providing a way to change primary text font.
*/
export const DisplayFontShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.font" />
</h3>
<ComboBox
value={theme.getFont()}
onChange={(e) => theme.setFont(e.currentTarget.value as Fonts)}>
{FONT_KEYS.map((key) => (
<option value={key} key={key}>
{FONTS[key as keyof typeof FONTS].name}
</option>
))}
</ComboBox>
</>
);
});
/**
* Component providing a way to change secondary, monospace text font.
*/
export const DisplayMonospaceFontShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.mono_font" />
</h3>
<ComboBox
value={theme.getMonospaceFont()}
onChange={(e) =>
theme.setMonospaceFont(
e.currentTarget.value as MonospaceFonts,
)
}>
{MONOSPACE_FONT_KEYS.map((key) => (
<option value={key} key={key}>
{
MONOSPACE_FONTS[key as keyof typeof MONOSPACE_FONTS]
.name
}
</option>
))}
</ComboBox>
</>
);
});
/**
* Component providing a way to toggle font ligatures.
*/
export const DisplayLigaturesShim = observer(() => {
const settings = useApplicationState().settings;
if (settings.theme.getFont() !== "Inter") return null;
return (
<>
<Checkbox
checked={settings.get("appearance:ligatures") ?? false}
onChange={(v) => settings.set("appearance:ligatures", v)}
description={
<Text id="app.settings.pages.appearance.ligatures_desc" />
}>
<Text id="app.settings.pages.appearance.ligatures" />
</Checkbox>
</>
);
});
/**
* Component providing a way to toggle showing the send button on desktop.
*/
export const ShowSendButtonShim = observer(() => {
const settings = useApplicationState().settings;
return (
<Checkbox
checked={settings.get("appearance:show_send_button") ?? false}
onChange={(v) => settings.set("appearance:show_send_button", v)}
description={
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
}>
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
</Checkbox>
);
});
/**
* Component providing a way to toggle seasonal themes.
*/
export const DisplaySeasonalShim = observer(() => {
const settings = useApplicationState().settings;
return (
<Checkbox
checked={settings.get("appearance:seasonal") ?? true}
onChange={(v) => settings.set("appearance:seasonal", v)}
description={
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
}>
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
</Checkbox>
);
});
/**
* Component providing a way to toggle transparency effects.
*/
export const DisplayTransparencyShim = observer(() => {
const settings = useApplicationState().settings;
return (
<Checkbox
checked={settings.get("appearance:transparency") ?? true}
onChange={(v) => settings.set("appearance:transparency", v)}
description={
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
}>
<Text id="app.settings.pages.appearance.theme_options.transparency" />
</Checkbox>
);
});
/**
* Component providing a way to change emoji pack.
*/
export const DisplayEmojiShim = observer(() => {
const settings = useApplicationState().settings;
return (
<EmojiSelector
value={settings.get("appearance:emoji")}
setValue={(v) => settings.set("appearance:emoji", v)}
/>
);
});

View File

@ -0,0 +1,60 @@
import { Block } from "@styled-icons/boxicons-regular";
import { Trash } from "@styled-icons/boxicons-solid";
import { Text } from "preact-i18n";
import { CategoryButton } from "@revoltchat/ui";
import {
clientController,
useClient,
} from "../../../controllers/client/ClientController";
import { modalController } from "../../../controllers/modals/ModalController";
export default function AccountManagement() {
const client = useClient();
const callback = (route: "disable" | "delete") => () =>
modalController.mfaFlow(client).then(
(ticket) =>
ticket &&
client.api
.post(`/auth/account/${route}`, undefined, {
headers: {
"X-MFA-Ticket": ticket.token,
},
})
.then(clientController.logoutCurrent),
);
return (
<>
<h3>
<Text id="app.settings.pages.account.manage.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.manage.description" />
</h5>
<CategoryButton
icon={<Block size={24} color="var(--error)" />}
description={
<Text id="app.settings.pages.account.manage.disable_description" />
}
action="chevron"
onClick={callback("disable")}>
<Text id="app.settings.pages.account.manage.disable" />
</CategoryButton>
<CategoryButton
icon={<Trash size={24} color="var(--error)" />}
description={
<Text id="app.settings.pages.account.manage.delete_description" />
}
action="chevron"
onClick={callback("delete")}>
<Text id="app.settings.pages.account.manage.delete" />
</CategoryButton>
</>
);
}

View File

@ -0,0 +1,78 @@
import { At } from "@styled-icons/boxicons-regular";
import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import {
AccountDetail,
CategoryButton,
Column,
HiddenValue,
} from "@revoltchat/ui";
import { useSession } from "../../../controllers/client/ClientController";
import { modalController } from "../../../controllers/modals/ModalController";
export default observer(() => {
const session = useSession()!;
const client = session.client!;
const [email, setEmail] = useState("...");
useEffect(() => {
if (email === "..." && session.state === "Online") {
client.api
.get("/auth/account/")
.then((account) => setEmail(account.email));
}
}, [client, email, session.state]);
return (
<>
<Column group>
<AccountDetail user={client.user!} />
</Column>
{(
[
[
"username",
client.user!.username +
"#" +
client.user!.discriminator,
At,
],
["email", email, Envelope],
["password", "•••••••••", Key],
] as const
).map(([field, value, Icon]) => (
<CategoryButton
key={field}
icon={<Icon size={24} />}
description={
field === "email" ? (
<HiddenValue
value={value}
placeholder={"•••••••••••@••••••.•••"}
/>
) : (
value
)
}
account
action={<Pencil size={20} />}
onClick={() =>
modalController.push({
type: "modify_account",
client,
field,
})
}>
<Text id={`login.${field}`} />
</CategoryButton>
))}
</>
);
});

View File

@ -0,0 +1,222 @@
import { ListOl } from "@styled-icons/boxicons-regular";
import { Lock } from "@styled-icons/boxicons-solid";
import { API } from "revolt.js";
import { Text } from "preact-i18n";
import { useCallback, useEffect, useState } from "preact/hooks";
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
import { useSession } from "../../../controllers/client/ClientController";
import { takeError } from "../../../controllers/client/jsx/error";
import { modalController } from "../../../controllers/modals/ModalController";
/**
* Temporary helper function for Axios config
* @param token Token
* @returns Headers
*/
export function toConfig(token: string) {
return {
headers: {
"X-MFA-Ticket": token,
},
};
}
/**
* Component for configuring MFA on an account.
*/
export default function MultiFactorAuthentication() {
// Pull in prerequisites
const session = useSession()!;
const client = session.client!;
// Keep track of MFA state
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
const [error, setError] = useState<string>();
// Fetch the current MFA status on account
useEffect(() => {
if (!mfa && session.state === "Online") {
client!.api
.get("/auth/mfa/")
.then(setMFA)
.catch((err) => setError(takeError(err)));
}
}, [mfa, client, session.state]);
// Action called when recovery code button is pressed
const recoveryAction = useCallback(async () => {
// Perform MFA flow first
const ticket = await modalController.mfaFlow(client);
// Check whether action was cancelled
if (typeof ticket === "undefined") {
return;
}
// Decide whether to generate or fetch.
let codes;
if (mfa!.recovery_active) {
// Fetch existing recovery codes
codes = await client.api.post(
"/auth/mfa/recovery",
undefined,
toConfig(ticket.token),
);
} else {
// Generate new recovery codes
codes = await client.api.patch(
"/auth/mfa/recovery",
undefined,
toConfig(ticket.token),
);
setMFA({
...mfa!,
recovery_active: true,
});
}
// Display the codes to the user
modalController.push({
type: "mfa_recovery",
client,
codes,
});
}, [mfa]);
// Action called when TOTP button is pressed
const totpAction = useCallback(async () => {
// Perform MFA flow first
const ticket = await modalController.mfaFlow(client);
// Check whether action was cancelled
if (typeof ticket === "undefined") {
return;
}
// Decide whether to disable or enable.
if (mfa!.totp_mfa) {
// Disable TOTP authentication
await client.api.delete(
"/auth/mfa/totp",
{},
toConfig(ticket.token),
);
setMFA({
...mfa!,
totp_mfa: false,
});
} else {
// Generate a TOTP secret
const { secret } = await client.api.post(
"/auth/mfa/totp",
undefined,
toConfig(ticket.token),
);
// Open secret modal
let success;
while (!success) {
try {
// Make the user generator a token
const totp_code = await modalController.mfaEnableTOTP(
secret,
client.user!.username,
);
if (totp_code) {
// Check whether it is valid
await client.api.put(
"/auth/mfa/totp",
{
totp_code,
},
toConfig(ticket.token),
);
// Mark as successful and activated
success = true;
setMFA({
...mfa!,
totp_mfa: true,
});
} else {
break;
}
} catch (err) {}
}
}
}, [mfa]);
const mfaActive = !!mfa?.totp_mfa;
return (
<>
<h3>
<Text id="app.settings.pages.account.2fa.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.2fa.description" />
</h5>
{error && (
<Category compact>
<Error error={error} />
</Category>
)}
<CategoryButton
icon={<ListOl size={24} />}
description={
<Text
id={`app.settings.pages.account.2fa.${
mfa?.recovery_active
? "view_recovery"
: "generate_recovery"
}_long`}
/>
}
disabled={!mfa}
onClick={recoveryAction}>
<Text
id={`app.settings.pages.account.2fa.${
mfa?.recovery_active
? "view_recovery"
: "generate_recovery"
}`}
/>
</CategoryButton>
<CategoryButton
icon={
<Lock
size={24}
color={!mfa?.totp_mfa ? "var(--error)" : undefined}
/>
}
description={"Set up time-based one-time password."}
disabled={!mfa || (!mfa.recovery_active && !mfa.totp_mfa)}
onClick={totpAction}>
<Text
id={`app.settings.pages.account.2fa.${
mfa?.totp_mfa ? "remove" : "add"
}_auth`}
/>
</CategoryButton>
{mfa && (
<Tip palette={mfaActive ? "primary" : "error"}>
<Text
id={`app.settings.pages.account.2fa.two_factor_${
mfaActive ? "on" : "off"
}`}
/>
</Tip>
)}
</>
);
}

View File

@ -0,0 +1,63 @@
import { observer } from "mobx-react-lite";
import { Text } from "preact-i18n";
import { ObservedInputElement } from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useApplicationState } from "../../../mobx/State";
import {
MonospaceFonts,
MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS,
} from "../../../context/Theme";
/**
* ! LEGACY
* Component providing a way to edit custom CSS.
*/
export const ShimThemeCustomCSS = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.custom_css" />
</h3>
<TextAreaAutoSize
maxRows={20}
minHeight={480}
code
value={theme.getCSS() ?? ""}
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
/>
</>
);
});
export default function AdvancedOptions() {
const settings = useApplicationState().settings;
return (
<>
{/** Combo box of available monospaced fonts */}
<h3>
<Text id="app.settings.pages.appearance.mono_font" />
</h3>
<ObservedInputElement
type="combo"
value={() => settings.theme.getMonospaceFont()}
onChange={(value) =>
settings.theme.setMonospaceFont(value as MonospaceFonts)
}
options={MONOSPACE_FONT_KEYS.map((value) => ({
value,
name: MONOSPACE_FONTS[value as keyof typeof MONOSPACE_FONTS]
.name,
}))}
/>
{/** Custom CSS */}
<ShimThemeCustomCSS />
</>
);
}

View File

@ -0,0 +1,77 @@
import { Text } from "preact-i18n";
import { Column, ObservedInputElement } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State";
export default function AppearanceOptions() {
const settings = useApplicationState().settings;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.appearance_options.title" />
</h3>
{/* Option to toggle "send message" button on desktop. */}
<ObservedInputElement
type="checkbox"
value={() =>
settings.get("appearance:show_send_button") ?? false
}
onChange={(v) => settings.set("appearance:show_send_button", v)}
title={
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
}
description={
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
}
/>
{/* Option to always show the account creation age next to join system messages. */}
<ObservedInputElement
type="checkbox"
value={() =>
settings.get("appearance:show_account_age") ?? false
}
onChange={(v) => settings.set("appearance:show_account_age", v)}
title={
<Text id="app.settings.pages.appearance.appearance_options.show_account_age" />
}
description={
<Text id="app.settings.pages.appearance.appearance_options.show_account_age_desc" />
}
/>
<hr />
<h3>
<Text id="app.settings.pages.appearance.theme_options.title" />
</h3>
<Column>
{/* Option to toggle transparency effects in-app. */}
<ObservedInputElement
type="checkbox"
value={() =>
settings.get("appearance:transparency") ?? true
}
onChange={(v) => settings.set("appearance:transparency", v)}
title={
<Text id="app.settings.pages.appearance.theme_options.transparency" />
}
description={
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
}
/>
{/* Option to toggle seasonal effects. */}
<ObservedInputElement
type="checkbox"
value={() => settings.get("appearance:seasonal") ?? true}
onChange={(v) => settings.set("appearance:seasonal", v)}
title={
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
}
description={
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
}
/>
</Column>
</>
);
}

View File

@ -0,0 +1,70 @@
import { observer } from "mobx-react-lite";
import { Text } from "preact-i18n";
import { Column, ObservedInputElement } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State";
import { FONTS, Fonts, FONT_KEYS } from "../../../context/Theme";
import { EmojiSelector } from "./legacy/EmojiSelector";
/**
* ! LEGACY
* Component providing a way to change emoji pack.
*/
export const ShimDisplayEmoji = observer(() => {
const settings = useApplicationState().settings;
return (
<EmojiSelector
value={settings.get("appearance:emoji")}
setValue={(v) => settings.set("appearance:emoji", v)}
/>
);
});
export default observer(() => {
const settings = useApplicationState().settings;
return (
<>
<Column>
{/* Combo box of available fonts. */}
<h3>
<Text id="app.settings.pages.appearance.font" />
</h3>
<ObservedInputElement
type="combo"
value={() => settings.theme.getFont()}
onChange={(value) => settings.theme.setFont(value as Fonts)}
options={FONT_KEYS.map((value) => ({
value,
name: FONTS[value as keyof typeof FONTS].name,
}))}
/>
{/* Option to toggle liagures for supported fonts. */}
{settings.theme.getFont() === "Inter" && (
<ObservedInputElement
type="checkbox"
value={() =>
settings.get("appearance:ligatures") ?? true
}
onChange={(v) =>
settings.set("appearance:ligatures", v)
}
title={
<Text id="app.settings.pages.appearance.ligatures" />
}
description={
<Text id="app.settings.pages.appearance.ligatures_desc" />
}
/>
)}
</Column>
<hr />
{/* Emoji pack selector */}
<ShimDisplayEmoji />
</>
);
});

View File

@ -1,170 +1,11 @@
import { Pencil } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import styled from "styled-components/macro";
import { useDebounceCallback } from "../../../lib/debounce";
import { useApplicationState } from "../../../mobx/State";
import { Variables } from "../../../context/Theme";
import InputBox from "../../ui/InputBox";
const Container = styled.div`
row-gap: 8px;
display: grid;
column-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-bottom: 20px;
.entry {
padding: 12px;
margin-top: 8px;
border: 1px solid black;
border-radius: var(--border-radius);
span {
flex: 1;
display: block;
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 8px;
text-transform: capitalize;
background: inherit;
background-clip: text;
-webkit-background-clip: text;
}
.override {
gap: 8px;
display: flex;
.picker {
width: 38px;
height: 38px;
display: grid;
cursor: pointer;
place-items: center;
border-radius: var(--border-radius);
background: var(--primary-background);
}
input[type="text"] {
width: 0;
min-width: 0;
flex-grow: 1;
}
}
.input {
width: 0;
height: 0;
position: relative;
input {
opacity: 0;
border: none;
display: block;
cursor: pointer;
position: relative;
top: 48px;
}
}
}
`;
export default observer(() => {
const theme = useApplicationState().settings.theme;
const setVariable = useDebounceCallback(
(data) => {
const { key, value } = data as { key: Variables; value: string };
theme.setVariable(key, value);
},
[theme],
100,
);
import Overrides from "./legacy/ThemeOverrides";
import ThemeTools from "./legacy/ThemeTools";
export default function ThemeOverrides() {
return (
<Container>
{(
[
"accent",
"background",
"foreground",
"primary-background",
"primary-header",
"secondary-background",
"secondary-foreground",
"secondary-header",
"tertiary-background",
"tertiary-foreground",
"block",
"message-box",
"mention",
"scrollbar-thumb",
"scrollbar-track",
"status-online",
"status-away",
"status-busy",
"status-streaming",
"status-invisible",
"success",
"warning",
"error",
"hover",
] as const
).map((key) => (
<div
class="entry"
key={key}
style={{ backgroundColor: theme.getVariable(key) }}>
<div class="input">
<input
type="color"
value={theme.getVariable(key)}
onChange={(el) =>
setVariable({
key,
value: el.currentTarget.value,
})
}
/>
</div>
<span
style={{
color: theme.getContrastingVariable(
key,
theme.getVariable("primary-background"),
),
}}>
{key}
</span>
<div class="override">
<div
class="picker"
onClick={(e) =>
e.currentTarget.parentElement?.parentElement
?.querySelector("input")
?.click()
}>
<Pencil size={24} />
</div>
<InputBox
type="text"
class="text"
value={theme.getVariable(key)}
onChange={(el) =>
setVariable({
key,
value: el.currentTarget.value,
})
}
/>
</div>
</div>
))}
</Container>
<>
<ThemeTools />
<Overrides />
</>
);
});
}

View File

@ -0,0 +1,64 @@
import { Brush } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import { Text } from "preact-i18n";
import { CategoryButton, ObservedInputElement } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State";
import { ThemeBaseSelector } from "./legacy/ThemeBaseSelector";
/**
* ! LEGACY
* Component providing a way to switch the base theme being used.
*/
export const ShimThemeBaseSelector = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<ThemeBaseSelector
value={theme.isModified() ? undefined : theme.getBase()}
setValue={(base) => {
theme.setBase(base);
theme.reset();
}}
/>
);
});
export default function ThemeSelection() {
const theme = useApplicationState().settings.theme;
return (
<>
{/** Allow users to change base theme */}
<ShimThemeBaseSelector />
{/** Provide a link to the theme shop */}
<Link to="/discover/themes" replace>
<CategoryButton
icon={<Brush size={24} />}
action="chevron"
description={
<Text id="app.settings.pages.appearance.discover.description" />
}>
<Text id="app.settings.pages.appearance.discover.title" />
</CategoryButton>
</Link>
<hr />
<h3>
<Text id="app.settings.pages.appearance.accent_selector" />
</h3>
<ObservedInputElement
type="colour"
value={theme.getVariable("accent")}
onChange={(colour) => {
theme.setVariable("accent", colour as string);
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
}}
/>
</>
);
}

View File

@ -2,11 +2,12 @@ import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { EmojiPack } from "../../common/Emoji";
import mutantSVG from "./mutant_emoji.svg";
import notoSVG from "./noto_emoji.svg";
import openmojiSVG from "./openmoji_emoji.svg";
import twemojiSVG from "./twemoji_emoji.svg";
import mutantSVG from "./assets/mutant_emoji.svg";
import notoSVG from "./assets/noto_emoji.svg";
import openmojiSVG from "./assets/openmoji_emoji.svg";
import twemojiSVG from "./assets/twemoji_emoji.svg";
import { EmojiPack } from "../../../common/Emoji";
const Container = styled.div`
gap: 12px;
@ -87,10 +88,10 @@ export function EmojiSelector({ value, setValue }: Props) {
<Text id="app.settings.pages.appearance.emoji_pack" />
</h3>
<Container>
<div class="row">
<div className="row">
<div>
<div
class="button"
className="button"
onClick={() => setValue("mutant")}
data-active={!value || value === "mutant"}>
<img
@ -112,7 +113,7 @@ export function EmojiSelector({ value, setValue }: Props) {
</div>
<div>
<div
class="button"
className="button"
onClick={() => setValue("twemoji")}
data-active={value === "twemoji"}>
<img
@ -125,10 +126,10 @@ export function EmojiSelector({ value, setValue }: Props) {
<h4>Twemoji</h4>
</div>
</div>
<div class="row">
<div className="row">
<div>
<div
class="button"
className="button"
onClick={() => setValue("openmoji")}
data-active={value === "openmoji"}>
<img
@ -142,7 +143,7 @@ export function EmojiSelector({ value, setValue }: Props) {
</div>
<div>
<div
class="button"
className="button"
onClick={() => setValue("noto")}
data-active={value === "noto"}>
<img

View File

@ -0,0 +1 @@
These components need to be ported to @revoltchat/ui.

Some files were not shown because too many files have changed in this diff Show More