Compare commits
745 Commits
ui/bot-pro
...
delete-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe0d0501fa | ||
|
|
bc604c0157 | ||
|
|
8c183b530b | ||
|
|
9bbc22b091 | ||
|
|
0d5ffb4df8 | ||
|
|
9d83d7cc1c | ||
|
|
534b9227de | ||
|
|
5363ff2572 | ||
|
|
3f1c2be709 | ||
|
|
04027658e6 | ||
|
|
f1594a7b36 | ||
|
|
fa0fcedce5 | ||
|
|
70f5e6fc7e | ||
|
|
9b9ec867da | ||
|
|
fead4ca879 | ||
|
|
449eee006d | ||
|
|
cf691a8462 | ||
|
|
c5ca579c95 | ||
|
|
636367faea | ||
|
|
f20ada7c49 | ||
|
|
75d07ffe0f | ||
|
|
5b600bec20 | ||
|
|
86218a7272 | ||
|
|
9bc052ea87 | ||
|
|
1862db89a3 | ||
|
|
00708bb8f4 | ||
|
|
7e87265ccb | ||
|
|
d55d86aa3c | ||
|
|
c426b6c184 | ||
|
|
4d58ff06f3 | ||
|
|
9f9902ffb1 | ||
|
|
c13896244c | ||
|
|
e17e556ee6 | ||
|
|
7441569cfa | ||
|
|
eff1de7c78 | ||
|
|
b733e13ec2 | ||
|
|
e68ceac4f6 | ||
|
|
3f536589e5 | ||
|
|
21175dffda | ||
|
|
5b31ce494e | ||
|
|
056ec623cb | ||
|
|
ff0330ec1b | ||
|
|
58f35a2813 | ||
|
|
a00d554f80 | ||
|
|
7f911f5d2c | ||
|
|
122f047c85 | ||
|
|
7d544c82ab | ||
|
|
46eec5a132 | ||
|
|
de85527cd8 | ||
|
|
6062a361f1 | ||
|
|
95d4f61d7f | ||
|
|
34ce1d1a86 | ||
|
|
a9f23fe0e3 | ||
|
|
6c08ccff52 | ||
|
|
2f43a2c32d | ||
|
|
e34c5c99fe | ||
|
|
89b3c9c098 | ||
|
|
dadf0b6329 | ||
|
|
fcf6812151 | ||
|
|
09be8c9e4f | ||
|
|
6767ea1853 | ||
|
|
9be4afe241 | ||
|
|
7e89dcfb13 | ||
|
|
3836156f3d | ||
|
|
9f3e47d327 | ||
|
|
e29678a6a3 | ||
|
|
40d3356c1b | ||
|
|
eb4670ec43 | ||
|
|
df20ab7407 | ||
|
|
22b2bc0a5e | ||
|
|
2a12b79ef7 | ||
|
|
9b6abe374a | ||
|
|
b649b2a923 | ||
|
|
b64c316dc9 | ||
|
|
bfeefb4c73 | ||
|
|
734fa06425 | ||
|
|
5a738b7c68 | ||
|
|
f03a88bd78 | ||
|
|
e53059ee08 | ||
|
|
099f7a3116 | ||
|
|
d2264a2a43 | ||
|
|
ef26f67c8e | ||
|
|
5b7ec655eb | ||
|
|
a381ba6320 | ||
|
|
4f2fc267f9 | ||
|
|
2687f98952 | ||
|
|
0a27632f05 | ||
|
|
1621e3b17d | ||
|
|
6b9106c975 | ||
|
|
e6ad6a552e | ||
|
|
7ba2388de4 | ||
|
|
47bfaad508 | ||
|
|
1a9a2786bb | ||
|
|
61a06c3f1a | ||
|
|
b42c24295f | ||
|
|
8fd6963f38 | ||
|
|
71e689ece3 | ||
|
|
7bc806ec63 | ||
|
|
1016d80e56 | ||
|
|
eab5ed033f | ||
|
|
263d97853a | ||
|
|
ff57dbddba | ||
|
|
de207f0fa7 | ||
|
|
1c69756383 | ||
|
|
02eb7d83f6 | ||
|
|
14037ef0c7 | ||
|
|
2c50d9be6b | ||
|
|
594ef00d09 | ||
|
|
8608257066 | ||
|
|
7626a1f461 | ||
|
|
83ca6f489e | ||
|
|
b7a10bb9ab | ||
|
|
f6be6d7cf8 | ||
|
|
dfbba41be4 | ||
|
|
88bc8f93b6 | ||
|
|
024fc6853b | ||
|
|
5bd2d24c56 | ||
|
|
5c50bed33d | ||
|
|
07fd598bf9 | ||
|
|
a234e3a582 | ||
|
|
a77f2f9b4d | ||
|
|
787d5840d2 | ||
|
|
353507e17a | ||
|
|
1b41ca03d9 | ||
|
|
3ca2b12dfc | ||
|
|
ff41219cf4 | ||
|
|
7cf7402cea | ||
|
|
c2a729a5e0 | ||
|
|
e9977b2a76 | ||
|
|
f76e53649b | ||
|
|
a4b8fb5fc2 | ||
|
|
c9264e1a49 | ||
|
|
9a5653bc02 | ||
|
|
50fd3b2d11 | ||
|
|
58f294b790 | ||
|
|
e1d3ad1675 | ||
|
|
b2e0b5a035 | ||
|
|
11a17feaae | ||
|
|
ca69a0b4c5 | ||
|
|
11e8cd652d | ||
|
|
6e70b55d02 | ||
|
|
dedc1e0666 | ||
|
|
9a70bb0eff | ||
|
|
791f4dfd89 | ||
|
|
3fbf315587 | ||
|
|
9807ef9c9a | ||
|
|
96017c5f33 | ||
|
|
084c90613f | ||
|
|
dbe8a64ffc | ||
|
|
176c7883c8 | ||
|
|
73fd35bf46 | ||
|
|
1a0b4b5703 | ||
|
|
62a427b7a7 | ||
|
|
64e7038532 | ||
|
|
7e88e733d5 | ||
|
|
70bda88383 | ||
|
|
f3822b625d | ||
|
|
df1b39256d | ||
|
|
c9066aba2d | ||
|
|
fc8cfa5419 | ||
|
|
3204d176de | ||
|
|
e3a526e2d7 | ||
|
|
57887dc86f | ||
|
|
4f3f6e26cf | ||
|
|
2214efe1bc | ||
|
|
030c211230 | ||
|
|
7f6db77c4f | ||
|
|
924448dc2c | ||
|
|
77c3f8d1bc | ||
|
|
80943afcf6 | ||
|
|
f792888268 | ||
|
|
aad9a30266 | ||
|
|
0ec7e5e116 | ||
|
|
2b65e98cd3 | ||
|
|
448722225e | ||
|
|
cb6d5a3828 | ||
|
|
354c22108e | ||
|
|
ec96dde694 | ||
|
|
445e9537d4 | ||
|
|
c12d40d0da | ||
|
|
7e20d5029e | ||
|
|
262b931810 | ||
|
|
4a85dd69cf | ||
|
|
34bb2bbc13 | ||
|
|
b541301cb1 | ||
|
|
a766183f01 | ||
|
|
47e3d0bdb5 | ||
|
|
c51b024329 | ||
|
|
e37140dcd0 | ||
|
|
705dcd001b | ||
|
|
f9c6f5cd9d | ||
|
|
f7ff7d0dfe | ||
|
|
29fb8e0064 | ||
|
|
4009b19f9c | ||
|
|
ec347f585d | ||
|
|
160d71684f | ||
|
|
79c90c1b00 | ||
|
|
23dec32476 | ||
|
|
a24e027a48 | ||
|
|
6e70937825 | ||
|
|
e4b61819d3 | ||
|
|
6bf58e8379 | ||
|
|
5dfe72c093 | ||
|
|
c4ac7a1b6d | ||
|
|
401b2d4990 | ||
|
|
0d3f29515e | ||
|
|
1664aaee15 | ||
|
|
8501e33103 | ||
|
|
a2a52e237d | ||
|
|
05516c5823 | ||
|
|
45692999bf | ||
|
|
1fcb3cedc1 | ||
|
|
0261fec676 | ||
|
|
0e86f19da2 | ||
|
|
31220db8fe | ||
|
|
66ae518e51 | ||
|
|
8d505c9564 | ||
|
|
5f2311b09c | ||
|
|
ce88fab714 | ||
|
|
b53d3bce13 | ||
|
|
c997286340 | ||
|
|
80f4bb3d98 | ||
|
|
1cfcb20d4d | ||
|
|
95ebd935ed | ||
|
|
3b7c1cbe20 | ||
|
|
cb0a521473 | ||
|
|
aa9974149c | ||
|
|
569864167e | ||
|
|
f185dec461 | ||
|
|
03e177f865 | ||
|
|
f685352963 | ||
|
|
6755217ad2 | ||
|
|
211ff2058a | ||
|
|
b7be9f8c03 | ||
|
|
241b9cd27b | ||
|
|
d10bd96900 | ||
|
|
374be319c4 | ||
|
|
0ee7b73d61 | ||
|
|
63d5f6bb7d | ||
|
|
34e6995d86 | ||
|
|
a1ef1dce5e | ||
|
|
a190a51d0b | ||
|
|
c9127d6cf3 | ||
|
|
f0d2e31b17 | ||
|
|
220a28a151 | ||
|
|
b44779c89a | ||
|
|
5835064219 | ||
|
|
b9da79bc11 | ||
|
|
ba99cbaf2a | ||
|
|
fc0c7611d4 | ||
|
|
2e9c013ed8 | ||
|
|
3e40a61624 | ||
|
|
4e22ccb2f7 | ||
|
|
00718245f9 | ||
|
|
cd8ab6739b | ||
|
|
5eabd2861f | ||
|
|
a404ff7fe0 | ||
|
|
7680931f5f | ||
|
|
c1324108e3 | ||
|
|
56770d40df | ||
|
|
64f19ec2c0 | ||
|
|
dbb1c1e8fa | ||
|
|
c686e85d37 | ||
|
|
8eefc87b05 | ||
|
|
645e1af6db | ||
|
|
8103cc03cf | ||
|
|
bdc527ebbe | ||
|
|
8a2826da91 | ||
|
|
bd50378234 | ||
|
|
71f8fc86a4 | ||
|
|
277eaa685d | ||
|
|
e81b8ed472 | ||
|
|
6be0807433 | ||
|
|
e0ca1681bd | ||
|
|
ebcbe4bd4b | ||
|
|
ec8b51f559 | ||
|
|
72e60b7528 | ||
|
|
3399991824 | ||
|
|
6497e11fb0 | ||
|
|
7544c78360 | ||
|
|
040c4367f7 | ||
|
|
ee80dfd3c8 | ||
|
|
67c8418c31 | ||
|
|
2056232759 | ||
|
|
81bf325990 | ||
|
|
906f15f103 | ||
|
|
ea106a3902 | ||
|
|
41e533ab59 | ||
|
|
68b9d5ea79 | ||
|
|
673efc0586 | ||
|
|
74f8c552ed | ||
|
|
f3bdbe52d9 | ||
|
|
b4777e9816 | ||
|
|
ab77d4a812 | ||
|
|
1d243d4762 | ||
|
|
2f9bfbf83f | ||
|
|
c2547b3ead | ||
|
|
a64fe61199 | ||
|
|
1bd138d6ef | ||
|
|
4bcfa601a5 | ||
|
|
20d31babce | ||
|
|
12b9716043 | ||
|
|
e2d9e41a58 | ||
|
|
bdf741e0ee | ||
|
|
588cb7c019 | ||
|
|
94dd4b464a | ||
|
|
1e3fe45075 | ||
|
|
9e948175b3 | ||
|
|
6eee71ddab | ||
|
|
7e245df179 | ||
|
|
2e13685998 | ||
|
|
0f3b1b0491 | ||
|
|
8efe3ecbc0 | ||
|
|
a35e68799a | ||
|
|
be12c6da20 | ||
|
|
6fdd701b38 | ||
|
|
e46a6d55d6 | ||
|
|
32d37777f2 | ||
|
|
6fcdbd1cef | ||
|
|
73d51a3e29 | ||
|
|
4541a34cef | ||
|
|
5d7b8f1851 | ||
|
|
dc110ca082 | ||
|
|
97ab6c075d | ||
|
|
744afe2f6a | ||
|
|
e74c3a5f80 | ||
|
|
f8e67af962 | ||
|
|
0e5f64afd6 | ||
|
|
ee899e795c | ||
|
|
8f0c69f528 | ||
|
|
e1e3e55dc7 | ||
|
|
cca97f9bb9 | ||
|
|
9e90ce6854 | ||
|
|
81e9774cc0 | ||
|
|
e9cba5b4e2 | ||
|
|
e9258d7c23 | ||
|
|
7345172271 | ||
|
|
4f48e3d176 | ||
|
|
eb1bd27ec0 | ||
|
|
0ce128b108 | ||
|
|
ac0c100846 | ||
|
|
93ba922536 | ||
|
|
1590c5abab | ||
|
|
dbbe66def6 | ||
|
|
2d5128fca8 | ||
|
|
7ffa0744c0 | ||
|
|
ce5dedcecf | ||
|
|
0818c32895 | ||
|
|
567888a223 | ||
|
|
0de941394d | ||
|
|
4c08483848 | ||
|
|
83a3585940 | ||
|
|
08e1db6d35 | ||
|
|
4ccfaea973 | ||
|
|
a66e96677f | ||
|
|
b2a9cfd987 | ||
|
|
f3cf4f01ca | ||
|
|
37a5e3af5f | ||
|
|
4c87bc54a8 | ||
|
|
2b8b9c959f | ||
|
|
988a703837 | ||
|
|
37d5ba24c5 | ||
|
|
d876f52346 | ||
|
|
14a2f4882e | ||
|
|
b217571325 | ||
|
|
c20f630d10 | ||
|
|
d31237e9e1 | ||
|
|
890cb94acb | ||
|
|
147880a447 | ||
|
|
eee1d4060f | ||
|
|
4aad0493ae | ||
|
|
36051b3374 | ||
|
|
48558dd5e4 | ||
|
|
d129d9816d | ||
|
|
7b7f3c3af8 | ||
|
|
6915186738 | ||
|
|
56f59d132b | ||
|
|
b2f4411850 | ||
|
|
002cb5e6c1 | ||
|
|
777d06f469 | ||
|
|
3f9e6fc2e2 | ||
|
|
5d4ef8b178 | ||
|
|
c3f4ad8f8d | ||
|
|
cdc077ffa1 | ||
|
|
0d6720e3b1 | ||
|
|
07439bd5f6 | ||
|
|
8021169131 | ||
|
|
b4c7fe543f | ||
|
|
28efdd0ea4 | ||
|
|
c8c470d383 | ||
|
|
83c9c36e16 | ||
|
|
ca240fb6d7 | ||
|
|
cf049bd4ee | ||
|
|
b3be822568 | ||
|
|
f9f0d5c55a | ||
|
|
3a12f92867 | ||
|
|
e93db2345f | ||
|
|
ce643a5a09 | ||
|
|
d01b9cae40 | ||
|
|
8e1031cf91 | ||
|
|
f65c9bdd5e | ||
|
|
6a465c1c08 | ||
|
|
009565f0cc | ||
|
|
18761e2181 | ||
|
|
3e045cf8a8 | ||
|
|
4692dda8f3 | ||
|
|
20060a029d | ||
|
|
741c937bb7 | ||
|
|
dc3925c003 | ||
|
|
d95070d8c7 | ||
|
|
498e76b535 | ||
|
|
0f8be1767d | ||
|
|
542c0482c5 | ||
|
|
d83455c759 | ||
|
|
d3220d7c70 | ||
|
|
094be08bbf | ||
|
|
b1a7a94d26 | ||
|
|
041c039827 | ||
|
|
3632b6b351 | ||
|
|
edaf2256d5 | ||
|
|
334e54146b | ||
|
|
b4642b1efe | ||
|
|
27f6420da2 | ||
|
|
c5286071ff | ||
|
|
1089651cb0 | ||
|
|
6020d94c3a | ||
|
|
a81645090e | ||
|
|
e4b1be591c | ||
|
|
50b0e0f5f2 | ||
|
|
2c321bf0f1 | ||
|
|
074057b481 | ||
|
|
4d769f42e1 | ||
|
|
622f2b2bca | ||
|
|
e959f29eae | ||
|
|
344519de00 | ||
|
|
ad40197df9 | ||
|
|
0de078a794 | ||
|
|
6e89e10461 | ||
|
|
68acfe26de | ||
|
|
13f119942c | ||
|
|
4ea97e0209 | ||
|
|
da5d82537d | ||
|
|
59fb8a3c0f | ||
|
|
e6684b8f78 | ||
|
|
2bef311f9a | ||
|
|
cc458453d3 | ||
|
|
0dec90f53d | ||
|
|
79114d72b8 | ||
|
|
8fa6f87dc2 | ||
|
|
c5461f3134 | ||
|
|
196bbe2634 | ||
|
|
8eb2f4e7e3 | ||
|
|
271a126a3f | ||
|
|
05266790ae | ||
|
|
3ce04f543f | ||
|
|
dc527467d5 | ||
|
|
45b9c355a7 | ||
|
|
60cd17d673 | ||
|
|
a58372fc42 | ||
|
|
4996a4d785 | ||
|
|
56f2411652 | ||
|
|
4526a696c6 | ||
|
|
b5189a769f | ||
|
|
2c5467c189 | ||
|
|
afa76f0623 | ||
|
|
8566cd613d | ||
|
|
e67f8f95cd | ||
|
|
bb5f940ad1 | ||
|
|
ba40da2a15 | ||
|
|
2c8bbe7d1f | ||
|
|
7e4f4cf001 | ||
|
|
8bda3123da | ||
|
|
200db35c25 | ||
|
|
b4555fd028 | ||
|
|
a3005cfcd8 | ||
|
|
4a58a4082b | ||
|
|
79550cb2ad | ||
|
|
8029f39c4e | ||
|
|
62335e4902 | ||
|
|
0c9c712ee0 | ||
|
|
ddbef13a12 | ||
|
|
f509acbe80 | ||
|
|
fe382f9532 | ||
|
|
f01943c681 | ||
|
|
3fd56e4fdc | ||
|
|
8dd18c9fb4 | ||
|
|
2dcfc340fa | ||
|
|
c77213f02f | ||
|
|
9c8de2b454 | ||
|
|
d6a541f380 | ||
|
|
43c66a4e22 | ||
|
|
dd39e13623 | ||
|
|
5a46c50b81 | ||
|
|
eea4d49743 | ||
|
|
4224f83035 | ||
|
|
2415e98fbe | ||
|
|
d3ffe8e6f6 | ||
|
|
b93f274db5 | ||
|
|
6aaabb4815 | ||
|
|
5a49bc7d20 | ||
|
|
07870c849d | ||
|
|
59685a961a | ||
|
|
a21436479c | ||
|
|
6c244d2ae7 | ||
|
|
8a1f20615e | ||
|
|
a7c63c639e | ||
|
|
3b4227d4ba | ||
|
|
3d68dab0b4 | ||
|
|
7da7bc8ef7 | ||
|
|
1cdc6b51df | ||
|
|
8fcda5a6de | ||
|
|
c6cc1f3672 | ||
|
|
af8cc984a9 | ||
|
|
90a53452e8 | ||
|
|
5768ff7c2f | ||
|
|
645221b384 | ||
|
|
0255c3167a | ||
|
|
bf82c24281 | ||
|
|
599aab9a31 | ||
|
|
450b404ec5 | ||
|
|
1d26beee10 | ||
|
|
9e48c8f022 | ||
|
|
540c851d8f | ||
|
|
600ea42c0f | ||
|
|
733926dfd3 | ||
|
|
03f44c5bed | ||
|
|
abecd0ec11 | ||
|
|
56925b3ea2 | ||
|
|
fc046936aa | ||
|
|
8d1e26d03e | ||
|
|
81af91fa1f | ||
|
|
cf55921077 | ||
|
|
8c1f5dfb68 | ||
|
|
188f682094 | ||
|
|
76c1f78ba6 | ||
|
|
28c07e7046 | ||
|
|
e40420b7e9 | ||
|
|
e13edcbb76 | ||
|
|
6c6f6396e4 | ||
|
|
c28c6134c1 | ||
|
|
2edf122b34 | ||
|
|
cad1c2fc3a | ||
|
|
a5a6833c2c | ||
|
|
dfd9043ca0 | ||
|
|
62b7ba2e4d | ||
|
|
dbf681c2e9 | ||
|
|
a09fbe685d | ||
|
|
00764b8cc3 | ||
|
|
b745cdd4d7 | ||
|
|
270624d5ec | ||
|
|
0543e56678 | ||
|
|
e7e8dbf23d | ||
|
|
136faadcec | ||
|
|
0609b40f81 | ||
|
|
fc91a75acb | ||
|
|
3baf82ca94 | ||
|
|
d328c9e61b | ||
|
|
21cd3f4558 | ||
|
|
202a70b91f | ||
|
|
42771f7137 | ||
|
|
5735020013 | ||
|
|
69b430d886 | ||
|
|
c26ef4c2b2 | ||
|
|
f3c3017e0c | ||
|
|
67936f53ed | ||
|
|
1d4eed872b | ||
|
|
1b6275d1b5 | ||
|
|
d92c50fdb8 | ||
|
|
a688063d96 | ||
|
|
bdb1d939e7 | ||
|
|
096e0c84c1 | ||
|
|
668963e7a0 | ||
|
|
0a1deb5289 | ||
|
|
73dfc0ace6 | ||
|
|
7c120685d4 | ||
|
|
4773b76ea9 | ||
|
|
b065fba6a4 | ||
|
|
85bd84713e | ||
|
|
504d882df8 | ||
|
|
b936d4462d | ||
|
|
14ee357ac8 | ||
|
|
a921e8ed0d | ||
|
|
b5789126f8 | ||
|
|
bad7458560 | ||
|
|
9387575372 | ||
|
|
e758b23ac3 | ||
|
|
66d3e7c616 | ||
|
|
6693f826fd | ||
|
|
7496f484e1 | ||
|
|
5417632f87 | ||
|
|
4034e17e0d | ||
|
|
bf6b996c09 | ||
|
|
b7ec4a8b78 | ||
|
|
fdbd931ea7 | ||
|
|
c0bf3fbc0b | ||
|
|
ee3f6bede7 | ||
|
|
672b657f58 | ||
|
|
675992bbc1 | ||
|
|
fc0224562f | ||
|
|
b34ed4c24c | ||
|
|
9052ada1c7 | ||
|
|
dec4315892 | ||
|
|
9c78e0912d | ||
|
|
086ab7b3bc | ||
|
|
cc531705b4 | ||
|
|
5974a2b83b | ||
|
|
94520bf064 | ||
|
|
45762e4bf6 | ||
|
|
a4155a1153 | ||
|
|
d20fa9e3ea | ||
|
|
e4159e1c6d | ||
|
|
17a2ca773b | ||
|
|
d3b78ebc48 | ||
|
|
03ec5275ee | ||
|
|
e8784946ae | ||
|
|
37c15eaa3e | ||
|
|
e814064001 | ||
|
|
ef0644074c | ||
|
|
614ec68622 | ||
|
|
b5077b62ff | ||
|
|
855b3739e6 | ||
|
|
bbe1e0d178 | ||
|
|
32182b1803 | ||
|
|
356291cc4f | ||
|
|
850d685175 | ||
|
|
93925834cc | ||
|
|
a396e511ec | ||
|
|
c655a59979 | ||
|
|
3a92cf06c3 | ||
|
|
746d017142 | ||
|
|
7241498f87 | ||
|
|
2ce4136a52 | ||
|
|
ce3be17b74 | ||
|
|
3999dbbe66 | ||
|
|
336e2ddeb7 | ||
|
|
63e52fe69c | ||
|
|
64df7edf9b | ||
|
|
4d59bb96b2 | ||
|
|
2a4d0c3311 | ||
|
|
83f38d71ce | ||
|
|
f0c2829c58 | ||
|
|
e20c3ee518 | ||
|
|
716edb3318 | ||
|
|
5797eb3686 | ||
|
|
bb707fb287 | ||
|
|
301070a3cb | ||
|
|
5737b32271 | ||
|
|
d73772929d | ||
|
|
84c5c3abe9 | ||
|
|
e4440f127a | ||
|
|
55930d30e3 | ||
|
|
ba82ff9f39 | ||
|
|
1b644d450d | ||
|
|
d439d63a91 | ||
|
|
029bff3d68 | ||
|
|
7f586e6f8c | ||
|
|
b14ea73663 | ||
|
|
68e83a4e47 | ||
|
|
a07a17d892 | ||
|
|
896cc13812 | ||
|
|
a23230850a | ||
|
|
064f223c78 | ||
|
|
028a8660c1 | ||
|
|
c96b09bb81 | ||
|
|
b7429e47b0 | ||
|
|
318177b414 | ||
|
|
7844c84fbe | ||
|
|
5c59f187bd | ||
|
|
b955f2820c | ||
|
|
de1b947b7d | ||
|
|
56126fa303 | ||
|
|
d7f52fb6f4 | ||
|
|
8a0033699d | ||
|
|
d1e1449f99 | ||
|
|
7e02979560 | ||
|
|
56ab2a2917 | ||
|
|
3b3ec69edd | ||
|
|
6b11248364 | ||
|
|
245c7a46ed | ||
|
|
9b4619cbd1 | ||
|
|
5169066772 | ||
|
|
6bed278fe8 | ||
|
|
73b6da4e1e | ||
|
|
c53f74abbf | ||
|
|
18f7f91ff0 | ||
|
|
af3eb754f7 | ||
|
|
b2c24d33ed | ||
|
|
bdc84b1d98 | ||
|
|
739dd53637 | ||
|
|
c64906051d | ||
|
|
f1a9c889b8 | ||
|
|
3b6519c530 | ||
|
|
3d723574f4 | ||
|
|
46c652d54c | ||
|
|
fee56d8f54 | ||
|
|
a46fbcf409 | ||
|
|
d8d002cc4a | ||
|
|
e263b627aa | ||
|
|
63164fe2d0 | ||
|
|
5029d0ac86 | ||
|
|
189d0b5ff9 | ||
|
|
ef1ddb7771 | ||
|
|
e380534d2a | ||
|
|
c31bcd0200 | ||
|
|
74430b1a8f | ||
|
|
e89bbb7455 | ||
|
|
cc0e45526c | ||
|
|
6e1bcab92b | ||
|
|
cc76e78db8 | ||
|
|
d2d7083542 | ||
|
|
73d99e4518 | ||
|
|
136238f62e | ||
|
|
f14ef2b78f | ||
|
|
4719150368 | ||
|
|
3d73834bef | ||
|
|
1aff8ef516 | ||
|
|
b80a6a9c3e | ||
|
|
1780d06c97 | ||
|
|
c29efc155b | ||
|
|
71c0a782c1 | ||
|
|
f7be9df980 | ||
|
|
68578d2620 | ||
|
|
89dda8fe82 | ||
|
|
120e6a35d8 | ||
|
|
c7df0088fc | ||
|
|
65be047dc6 | ||
|
|
bd4369cf29 | ||
|
|
26a34032f9 | ||
|
|
fef2c5997f | ||
|
|
faca4ac32b | ||
|
|
ec83230c59 | ||
|
|
413bf6949b | ||
|
|
f8b8d96d3d | ||
|
|
bc799931a8 | ||
|
|
2b55770ecc | ||
|
|
a8491267a4 | ||
|
|
f87ecfcbd7 | ||
|
|
830b24a393 | ||
|
|
49f45aa5aa | ||
|
|
b36cde771e | ||
|
|
87a9841885 | ||
|
|
89748d7044 | ||
|
|
185f76d850 | ||
|
|
5a41c25e3c | ||
|
|
66bfc658c3 |
1
.babelrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] }
|
||||||
@@ -5,3 +5,6 @@ dist_injected
|
|||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|||||||
5
.env
@@ -1,2 +1,5 @@
|
|||||||
VITE_API_URL=https://api.revolt.chat
|
# VITE_API_URL=https://api.revolt.chat
|
||||||
|
VITE_API_URL=https://app.revolt.chat/api
|
||||||
|
# VITE_API_URL=http://local.revolt.chat:8000
|
||||||
|
# VITE_API_URL=https://revolt.chat/api
|
||||||
VITE_THEMES_URL=https://themes.revolt.chat
|
VITE_THEMES_URL=https://themes.revolt.chat
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
|||||||
ko_fi: insertish
|
|
||||||
custom: https://insrt.uk/donate
|
|
||||||
66
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: File a bug report
|
||||||
|
title: "bug: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: branch
|
||||||
|
attributes:
|
||||||
|
label: Branch
|
||||||
|
description: What branch of Revolt are you using?
|
||||||
|
options:
|
||||||
|
- Production (app.revolt.chat)
|
||||||
|
- Nightly (nightly.revolt.chat)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: commit-hash
|
||||||
|
attributes:
|
||||||
|
label: Commit hash
|
||||||
|
description: What is your commit hash? You can find this at the bottom of Settings, next to the branch name.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: What browsers are you seeing the problem on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Firefox
|
||||||
|
- Chrome
|
||||||
|
- Safari
|
||||||
|
- Microsoft Edge
|
||||||
|
- Other (please specify in the "What happened" form)
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output. (To get this, press `CTRL`- or `CMD`-`SHIFT`-`I` and navigate to the "Console" tab.)
|
||||||
|
render: shell
|
||||||
|
- type: checkboxes
|
||||||
|
id: desktop
|
||||||
|
attributes:
|
||||||
|
label: Desktop
|
||||||
|
description: Is this bug specific to [the desktop client](https://github.com/revoltchat/desktop)? (If not, leave this unchecked.)
|
||||||
|
options:
|
||||||
|
- label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself.
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: pwa
|
||||||
|
attributes:
|
||||||
|
label: PWA
|
||||||
|
description: Is this bug specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
|
||||||
|
options:
|
||||||
|
- label: Yes, this bug is specific to the PWA.
|
||||||
|
required: false
|
||||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
contact_links:
|
||||||
|
- name: Lounge Chat
|
||||||
|
url: https://rvlt.gg/Testers
|
||||||
|
about: Ask questions and discuss with others.
|
||||||
|
- name: Discussions
|
||||||
|
url: https://github.com/orgs/revoltchat/discussions
|
||||||
|
about: For larger feature requests and general question & answer.
|
||||||
24
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Make a feature request
|
||||||
|
title: "feature request: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Before you start, a lot of bigger features may be better suited as [API issues](https://github.com/revoltchat/delta/issues/new) or [centralised discussions](https://github.com/revoltchat/revolt/discussions/new).
|
||||||
|
- type: textarea
|
||||||
|
id: your-idea
|
||||||
|
attributes:
|
||||||
|
label: What do you want to see?
|
||||||
|
description: Describe your idea in as much detail as possible - if applicable, screenshots/mockups are really useful.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: pwa
|
||||||
|
attributes:
|
||||||
|
label: PWA
|
||||||
|
description: Is this feature request specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
|
||||||
|
options:
|
||||||
|
- label: Yes, this feature request is specific to the PWA.
|
||||||
|
required: false
|
||||||
24
.github/SECURITY.md
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Security
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you would like to report a security vulnerability,
|
|
||||||
please email **[security@revolt.chat](mailto:security@revolt.chat)**,
|
|
||||||
this will open a new ticket in ticket system, you should receive a response
|
|
||||||
within the next couple of days, potentially within a few minutes if someone
|
|
||||||
is currently active.
|
|
||||||
|
|
||||||
To help us best triage the issue, please provide:
|
|
||||||
|
|
||||||
- The type of issue at hand
|
|
||||||
- The name of the relevant project affected
|
|
||||||
- Reproduction steps
|
|
||||||
- Reference to any relevant source file(s) that you may suspect are causing the issue
|
|
||||||
- Any extra information about your configuration.
|
|
||||||
- Description of potential ways this can be exploited, if you can list any
|
|
||||||
|
|
||||||
For revoltchat/revite in particular:
|
|
||||||
|
|
||||||
- Please include the commit hash of the client, it is visible in settings under the log out button.
|
|
||||||
|
|
||||||
Thank you for helping Revolt.
|
|
||||||
2
.github/actions/build/action.yml
vendored
@@ -19,7 +19,7 @@ runs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 15
|
node-version: 16
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install Dependencies and Build
|
- name: Install Dependencies and Build
|
||||||
|
|||||||
7
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## Please make sure to check the following tasks before opening and submitting a PR
|
||||||
|
|
||||||
|
* [ ] I understand and have followed the [contribution guide](https://github.com/revoltchat/revolt/discussions/282)
|
||||||
|
* [ ] I have tested my changes locally and they are working as intended
|
||||||
|
* [ ] These changes do not have any notable side effects on other Revolt projects
|
||||||
|
* [ ] (optional) I have opened a pull request on [the translation repository](https://github.com/revoltchat/translations)
|
||||||
|
* [ ] I have included screenshots to demonstrate my changes
|
||||||
4
.github/workflows/docker.yml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "*"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "!.github/workflows/docker.yml"
|
- "!.github/workflows/docker.yml"
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||||
|
|||||||
5
.github/workflows/mirroring.yml
vendored
@@ -1,6 +1,9 @@
|
|||||||
name: Mirroring
|
name: Mirroring
|
||||||
|
|
||||||
on: [push, delete]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
to_gitlab:
|
to_gitlab:
|
||||||
|
|||||||
39
.github/workflows/preview_cleanup.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: Clean Preview
|
|
||||||
|
|
||||||
#! Safety:
|
|
||||||
#! this workflow should not execute any untrusted input at all
|
|
||||||
#! see the docs on `pull_request_target` for more
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [unlabeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
clean:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.label.name == 'use-preview'
|
|
||||||
env:
|
|
||||||
BASE: refs/pull/${{ github.event.pull_request.number }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: build-previews
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: clean previews
|
|
||||||
run: rm -rf "$BASE"
|
|
||||||
|
|
||||||
- name: publish cleaned previews
|
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.5
|
|
||||||
with:
|
|
||||||
folder: .
|
|
||||||
branch: build-previews
|
|
||||||
commit-message: "Cleaning up build result for #${{ github.event.pull_request.number }}"
|
|
||||||
|
|
||||||
- name: send comment
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
with:
|
|
||||||
header: Preview environment
|
|
||||||
message: |
|
|
||||||
## Preview environment
|
|
||||||
There is no longer a preview enviroment for this pull request due to the `use-preview` label being removed
|
|
||||||
52
.github/workflows/preview_pull_request.yml
vendored
@@ -1,52 +0,0 @@
|
|||||||
name: Preview Pull Request
|
|
||||||
|
|
||||||
#! Safety:
|
|
||||||
#! this workflow should not execute any untrusted input at all
|
|
||||||
#! see the docs on `pull_request_target` for more
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [synchronize, reopened, labeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
preview:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# make sure the pull request is labeled with 'use-preview'
|
|
||||||
if: github.event.label.name == 'use-preview' || contains(github.event.pull_request.labels.*.name, 'use-preview')
|
|
||||||
env:
|
|
||||||
BASE: refs/pull/${{ github.event.pull_request.number }}/merge
|
|
||||||
REPO: ${{ github.event.repository.name }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
# Head commit of the pull request
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
path: pull
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
base: /${{ env.REPO }}/${{ env.BASE }}/
|
|
||||||
folder: pull
|
|
||||||
|
|
||||||
- name: publish preview
|
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.5
|
|
||||||
with:
|
|
||||||
folder: pull/dist
|
|
||||||
branch: build-previews
|
|
||||||
target-folder: ${{ env.BASE }}
|
|
||||||
single-commit: true
|
|
||||||
commit-message: "Publishing build result from #${{ github.event.pull_request.number }}"
|
|
||||||
|
|
||||||
- name: send comment
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
with:
|
|
||||||
header: Preview environment
|
|
||||||
message: |
|
|
||||||
## Preview environment
|
|
||||||
https://${{ github.repository_owner }}.github.io/${{ env.REPO }}/${{ env.BASE }}/
|
|
||||||
|
|
||||||
This link will remain active until the `use-preview` label is removed.
|
|
||||||
27
.github/workflows/triage_issue.yml
vendored
@@ -15,22 +15,27 @@ jobs:
|
|||||||
gh api graphql -f query='
|
gh api graphql -f query='
|
||||||
query {
|
query {
|
||||||
organization(login: "revoltchat"){
|
organization(login: "revoltchat"){
|
||||||
projectNext(number: 3) {
|
projectV2(number: 3) {
|
||||||
id
|
id
|
||||||
fields(first:20) {
|
fields(first:20) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
... on ProjectV2SingleSelectField {
|
||||||
name
|
id
|
||||||
settings
|
name
|
||||||
|
options {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' > project_data.json
|
}' > project_data.json
|
||||||
|
|
||||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.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 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.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
|
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Add issue to project
|
- name: Add issue to project
|
||||||
env:
|
env:
|
||||||
@@ -39,11 +44,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
item_id="$( gh api graphql -f query='
|
item_id="$( gh api graphql -f query='
|
||||||
mutation($project:ID!, $issue:ID!) {
|
mutation($project:ID!, $issue:ID!) {
|
||||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
||||||
projectNextItem {
|
item {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||||
|
|
||||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
|
|||||||
39
.github/workflows/triage_pr.yml
vendored
@@ -2,7 +2,7 @@ name: Add PR to Board
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened]
|
types: [opened, synchronize, ready_for_review, review_requested]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
track_pr:
|
track_pr:
|
||||||
@@ -15,22 +15,27 @@ jobs:
|
|||||||
gh api graphql -f query='
|
gh api graphql -f query='
|
||||||
query {
|
query {
|
||||||
organization(login: "revoltchat"){
|
organization(login: "revoltchat"){
|
||||||
projectNext(number: 3) {
|
projectV2(number: 3) {
|
||||||
id
|
id
|
||||||
fields(first:20) {
|
fields(first:20) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
... on ProjectV2SingleSelectField {
|
||||||
name
|
id
|
||||||
settings
|
name
|
||||||
|
options {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' > project_data.json
|
}' > project_data.json
|
||||||
|
|
||||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.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 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.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
|
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Add PR to project
|
- name: Add PR to project
|
||||||
env:
|
env:
|
||||||
@@ -39,13 +44,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
item_id="$( gh api graphql -f query='
|
item_id="$( gh api graphql -f query='
|
||||||
mutation($project:ID!, $pr:ID!) {
|
mutation($project:ID!, $pr:ID!) {
|
||||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
||||||
projectNextItem {
|
item {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||||
|
|
||||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set fields
|
- name: Set fields
|
||||||
@@ -59,14 +64,16 @@ jobs:
|
|||||||
$status_field: ID!
|
$status_field: ID!
|
||||||
$status_value: String!
|
$status_value: String!
|
||||||
) {
|
) {
|
||||||
set_status: updateProjectNextItemField(input: {
|
set_status: updateProjectV2ItemFieldValue(input: {
|
||||||
projectId: $project
|
projectId: $project
|
||||||
itemId: $item
|
itemId: $item
|
||||||
fieldId: $status_field
|
fieldId: $status_field
|
||||||
value: $status_value
|
value: {
|
||||||
|
singleSelectOptionId: $status_value
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
projectNextItem {
|
projectV2Item {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -7,6 +7,11 @@ dist-ssr
|
|||||||
*.log
|
*.log
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
public/assets
|
public/assets
|
||||||
public/assets_*
|
public/assets_*
|
||||||
!public/assets_default
|
!public/assets_default
|
||||||
|
|
||||||
|
.vscode/chrome_data
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
image: node:14-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
@@ -1,3 +1,9 @@
|
|||||||
[submodule "external/lang"]
|
[submodule "external/lang"]
|
||||||
path = external/lang
|
path = external/lang
|
||||||
url = https://github.com/revoltchat/translations
|
url = https://github.com/revoltchat/translations
|
||||||
|
[submodule "external/components"]
|
||||||
|
path = external/components
|
||||||
|
url = https://github.com/revoltchat/components
|
||||||
|
[submodule "external/revolt.js"]
|
||||||
|
path = external/revolt.js
|
||||||
|
url = https://github.com/revoltchat/revolt.js
|
||||||
|
|||||||
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
src/components/markdown/prism.ts
|
||||||
@@ -4,10 +4,12 @@ module.exports = {
|
|||||||
jsxBracketSameLine: true,
|
jsxBracketSameLine: true,
|
||||||
importOrder: [
|
importOrder: [
|
||||||
"preact|classnames|.scss$",
|
"preact|classnames|.scss$",
|
||||||
|
"^@revoltchat",
|
||||||
"/(lib)",
|
"/(lib)",
|
||||||
"/(redux|mobx)",
|
"/(redux|mobx)",
|
||||||
"/(context)",
|
"/(context)",
|
||||||
"/(ui|common)|.svg|.webp|.png|.jpg$",
|
"/(ui|common)$",
|
||||||
|
".svg|.webp|.png|.jpg$",
|
||||||
"^[./]",
|
"^[./]",
|
||||||
],
|
],
|
||||||
importOrderSeparation: true,
|
importOrderSeparation: true,
|
||||||
|
|||||||
17
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "pwa-chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://local.revolt.chat:3000",
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
"runtimeExecutable": "/usr/bin/chromium",
|
||||||
|
"userDataDir": "${workspaceFolder}/.vscode/chrome_data"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true
|
||||||
"compile-hero.disable-compile-files-on-did-save-code": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
785
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file
11
.yarnrc.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||||
|
spec: "@yarnpkg/plugin-typescript"
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-workspace-tools"
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-3.2.0.cjs
|
||||||
16
Dockerfile
@@ -1,18 +1,16 @@
|
|||||||
FROM node:16-buster AS builder
|
FROM node:16-buster AS builder
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
RUN yarn --no-cache
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY .env.build .env
|
COPY .env.build .env
|
||||||
RUN yarn add --dev @babel/plugin-proposal-decorators
|
|
||||||
RUN yarn typecheck
|
|
||||||
RUN yarn build
|
|
||||||
RUN npm prune --production
|
|
||||||
|
|
||||||
FROM node:16-buster
|
RUN yarn install --frozen-lockfile
|
||||||
|
RUN yarn build:deps
|
||||||
|
# RUN yarn typecheck # lol no
|
||||||
|
RUN yarn build:highmem
|
||||||
|
RUN yarn workspaces focus --production --all
|
||||||
|
|
||||||
|
FROM node:16-alpine
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY --from=builder /usr/src/app .
|
COPY --from=builder /usr/src/app .
|
||||||
|
|
||||||
|
|||||||
35
README.md
@@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
|
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
|
## Stack
|
||||||
|
|
||||||
- [Preact](https://preactjs.com/)
|
- [Preact](https://preactjs.com/)
|
||||||
@@ -35,22 +43,27 @@ Get revite up and running locally.
|
|||||||
git clone --recursive https://github.com/revoltchat/revite
|
git clone --recursive https://github.com/revoltchat/revite
|
||||||
cd revite
|
cd revite
|
||||||
yarn
|
yarn
|
||||||
|
yarn build:deps
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can now access the client at http://local.revolt.chat:3000.
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------------------- | -------------------------------------------- |
|
| --------------------------------------- | -------------------------------------------- |
|
||||||
| `yarn pull` | Setup assets required for Revite. |
|
| `yarn pull` | Setup assets required for Revite. |
|
||||||
| `yarn dev` | Start the Revolt client in development mode. |
|
| `yarn dev` | Start the Revolt client in development mode. |
|
||||||
| `yarn build` | Build the Revolt client. |
|
| `yarn build` | Build the Revolt client. |
|
||||||
| `yarn preview` | Start a local server with the built client. |
|
| `yarn build:deps` | Build external dependencies. |
|
||||||
| `yarn lint` | Run ESLint on the client. |
|
| `yarn preview` | Start a local server with the built client. |
|
||||||
| `yarn fmt` | Run Prettier on the client. |
|
| `yarn lint` | Run ESLint on the client. |
|
||||||
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
| `yarn fmt` | Run Prettier on the client. |
|
||||||
| `yarn start` | Start a local sirv server with built client. |
|
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
||||||
| `yarn start:inject` | Inject a given API URL and start server. |
|
| `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
|
## License
|
||||||
|
|
||||||
|
|||||||
6
disabled-js.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="225" height="161" viewBox="0 0 225 161" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M69.6951 15.0002L20.9951 63.7002L69.6951 112.4L81.7601 100.325L45.1301 63.6952L81.7601 27.0652L69.6951 15.0002ZM132.955 112.39L181.655 63.6902L132.955 14.9902L120.89 27.0652L157.52 63.6952L120.89 100.325L132.955 112.39Z" fill="white"/>
|
||||||
|
<path d="M197 73H137C133.686 73 131 75.6863 131 79V139C131 142.314 133.686 145 137 145H197C200.314 145 203 142.314 203 139V79C203 75.6863 200.314 73 197 73Z" fill="#242424"/>
|
||||||
|
<path d="M191 79H143C139.686 79 137 81.6863 137 85V133C137 136.314 139.686 139 143 139H191C194.314 139 197 136.314 197 133V85C197 81.6863 194.314 79 191 79Z" fill="#D14F4F"/>
|
||||||
|
<path d="M181.5 95.5L153.5 123.5M153.5 95.5L181.5 123.5L153.5 95.5Z" stroke="white" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 828 B |
1
external/components
vendored
Submodule
2
external/lang
vendored
1
external/revolt.js
vendored
Submodule
75
index.html
@@ -1,10 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" background="#191919">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
|
<!--App Title-->
|
||||||
<title>Revolt</title>
|
<title>Revolt</title>
|
||||||
<meta name="apple-mobile-web-app-title" content="Revolt" />
|
<meta name="apple-mobile-web-app-title" content="Revolt" />
|
||||||
|
|
||||||
|
<!--App Scaling-->
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
||||||
@@ -69,14 +72,72 @@
|
|||||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)"
|
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)"
|
||||||
rel="apple-touch-startup-image"
|
rel="apple-touch-startup-image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!--CSS for noscript screen-->
|
||||||
|
<style>
|
||||||
|
noscript {
|
||||||
|
background: #242424;
|
||||||
|
color: white;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
noscript > div {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
noscript > div > h1 {
|
||||||
|
margin: 8px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
noscript > div > p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
noscript > div > a {
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 80px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #ed5151;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
noscript > div > a:hover {
|
||||||
|
background-color: #cf4848;
|
||||||
|
}
|
||||||
|
noscript > div > a:active {
|
||||||
|
background-color: #b64141;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onContextMenu="return false" ontouchstart="">
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
<noscript>
|
||||||
|
<div>
|
||||||
|
<img src="disabled-js.svg" />
|
||||||
|
<h1>Well, this is really awkward...</h1>
|
||||||
|
<p>Seems like your JavaScript is disabled.</p>
|
||||||
|
<p>You'll need to enable JavaScript to run this app.</p>
|
||||||
|
<a href="https://app.revolt.chat" target="_blank">Reload</a>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
</body>
|
</body>
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
background-color: #191919;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
106
package.json
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.0",
|
"version": "1.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/setup_assets.js --check && vite",
|
"dev": "node scripts/setup_assets.js --check && vite",
|
||||||
"pull": "node scripts/setup_assets.js",
|
"pull": "node scripts/setup_assets.js",
|
||||||
"build": "rimraf build && node scripts/setup_assets.js --check && vite build",
|
"build:deps": "cd external && cd components && yarn && yarn build:esm && cd .. && cd revolt.js && yarn && yarn build",
|
||||||
|
"build": "yarn && rimraf build && node scripts/setup_assets.js --check && yarn build:deps && vite build",
|
||||||
|
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
|
||||||
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"start": "sirv dist --cors --single --host",
|
"start": "sirv dist --cors --single --host",
|
||||||
@@ -37,27 +39,34 @@
|
|||||||
{
|
{
|
||||||
"varsIgnorePattern": "^_"
|
"varsIgnorePattern": "^_"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"react/jsx-no-literals": "warn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@revoltchat/rehype-katex": "6.0.3-patch.1",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"klaw": "^3.0.0",
|
"klaw": "^3.0.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"lottie-react": "^2.4.0",
|
||||||
"sirv-cli": "^1.0.14",
|
"sirv-cli": "^1.0.14",
|
||||||
"vite": "^2.6.14"
|
"vite": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.17.9",
|
||||||
|
"@floating-ui/react-dom": "^1.0.0",
|
||||||
|
"@floating-ui/react-dom-interactions": "^0.9.1",
|
||||||
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
||||||
"@fontsource/bree-serif": "^4.4.5",
|
"@fontsource/bitter": "^4.5.7",
|
||||||
"@fontsource/comic-neue": "^4.4.5",
|
"@fontsource/comic-neue": "^4.4.5",
|
||||||
"@fontsource/fira-code": "^4.4.5",
|
"@fontsource/fira-code": "^4.4.5",
|
||||||
"@fontsource/inter": "^4.4.5",
|
"@fontsource/inter": "^4.4.5",
|
||||||
"@fontsource/jetbrains-mono": "^4.4.5",
|
"@fontsource/jetbrains-mono": "^4.4.5",
|
||||||
"@fontsource/lato": "^4.4.5",
|
"@fontsource/lato": "^4.4.5",
|
||||||
|
"@fontsource/lexend": "^4.5.2",
|
||||||
"@fontsource/montserrat": "^4.4.5",
|
"@fontsource/montserrat": "^4.4.5",
|
||||||
"@fontsource/noto-sans": "^4.4.5",
|
"@fontsource/noto-sans": "^4.4.5",
|
||||||
"@fontsource/open-sans": "^4.4.5",
|
"@fontsource/open-sans": "^4.5.2",
|
||||||
|
"@fontsource/opendyslexic": "^4.5.2",
|
||||||
"@fontsource/poppins": "^4.4.5",
|
"@fontsource/poppins": "^4.4.5",
|
||||||
"@fontsource/raleway": "^4.4.5",
|
"@fontsource/raleway": "^4.4.5",
|
||||||
"@fontsource/roboto": "^4.4.5",
|
"@fontsource/roboto": "^4.4.5",
|
||||||
@@ -66,79 +75,102 @@
|
|||||||
"@fontsource/space-mono": "^4.4.5",
|
"@fontsource/space-mono": "^4.4.5",
|
||||||
"@fontsource/ubuntu": "^4.4.5",
|
"@fontsource/ubuntu": "^4.4.5",
|
||||||
"@fontsource/ubuntu-mono": "^4.4.5",
|
"@fontsource/ubuntu-mono": "^4.4.5",
|
||||||
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
"@hcaptcha/react-hcaptcha": "^1.4.4",
|
||||||
|
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
||||||
"@preact/preset-vite": "^2.0.0",
|
"@preact/preset-vite": "^2.0.0",
|
||||||
|
"@revoltchat/ui": "^1.0.77",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@styled-icons/boxicons-logos": "^10.34.0",
|
"@styled-icons/boxicons-logos": "^10.38.0",
|
||||||
"@styled-icons/boxicons-regular": "^10.34.0",
|
"@styled-icons/boxicons-regular": "^10.38.0",
|
||||||
"@styled-icons/boxicons-solid": "^10.37.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.5",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@traptitech/markdown-it-katex": "^3.4.3",
|
"@traptitech/markdown-it-katex": "^3.4.3",
|
||||||
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
||||||
|
"@types/lodash": "^4",
|
||||||
"@types/lodash.defaultsdeep": "^4.6.6",
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
||||||
"@types/lodash.isequal": "^4.5.5",
|
"@types/lodash.isequal": "^4.5.5",
|
||||||
"@types/markdown-it": "^12.0.2",
|
"@types/node": "^15.14.9",
|
||||||
"@types/node": "^15.12.4",
|
|
||||||
"@types/preact-i18n": "^2.3.0",
|
"@types/preact-i18n": "^2.3.0",
|
||||||
"@types/prismjs": "^1.16.5",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react-beautiful-dnd": "^13.1.2",
|
"@types/react-beautiful-dnd": "^13",
|
||||||
"@types/react-helmet": "^6.1.1",
|
"@types/react-helmet": "^6.1.1",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-scroll": "^1.8.2",
|
"@types/react-scroll": "^1.8.2",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/semver": "^7",
|
||||||
"@types/styled-components": "^5.1.10",
|
"@types/styled-components": "^5.1.10",
|
||||||
"@types/twemoji": "^12.1.1",
|
"@types/twemoji": "^12.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||||
"@typescript-eslint/parser": "^4.27.0",
|
"@typescript-eslint/parser": "^4.27.0",
|
||||||
|
"@vitejs/plugin-legacy": "^1.7.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"color-rgba": "^2.4.0",
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"detect-browser": "^5.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-config-preact": "^1.1.4",
|
"eslint-config-preact": "^1.1.4",
|
||||||
|
"eslint-plugin-jsdoc": "^39.3.2",
|
||||||
|
"eslint-plugin-mobx": "^0.0.8",
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
"highlight.js": "^11.0.1",
|
"history": "4",
|
||||||
|
"json-stringify-deterministic": "^1.0.2",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lodash.defaultsdeep": "^4.6.1",
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"markdown-it": "^12.0.6",
|
"long": "^5.2.0",
|
||||||
"markdown-it-emoji": "^2.0.0",
|
"mdast-util-to-hast": "^12.1.2",
|
||||||
"markdown-it-sub": "^1.0.0",
|
|
||||||
"markdown-it-sup": "^1.0.0",
|
|
||||||
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
||||||
"mobx": "^6.3.2",
|
"mobx": "^6.6.0",
|
||||||
"mobx-react-lite": "^3.2.0",
|
"mobx-react-lite": "3.4.0",
|
||||||
"preact": "^10.5.14",
|
"preact": "^10.5.14",
|
||||||
"preact-context-menu": "^0.2.1",
|
"preact-context-menu": "0.4.1",
|
||||||
"preact-i18n": "^2.4.0-preactx",
|
"preact-i18n": "^2.4.0-preactx",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.28.0",
|
||||||
"react-device-detect": "^1.17.0",
|
"qrcode.react": "^3.0.2",
|
||||||
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
|
"react-device-detect": "2.2.2",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "6.3.0",
|
"react-hook-form": "6.3.0",
|
||||||
"react-overlapping-panels": "1.2.2",
|
"react-overlapping-panels": "1.2.2",
|
||||||
"react-redux": "^7.2.4",
|
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scroll": "^1.8.2",
|
"react-scroll": "^1.8.2",
|
||||||
"react-virtualized-auto-sizer": "^1.0.5",
|
"react-virtuoso": "^2.12.0",
|
||||||
"react-virtuoso": "^1.10.4",
|
"rehype-prism": "^2.1.3",
|
||||||
"redux": "^4.1.0",
|
"rehype-react": "^7.1.1",
|
||||||
"revolt-api": "0.5.3-alpha.10",
|
"remark-breaks": "^3.0.2",
|
||||||
"revolt.js": "^5.1.0-alpha.10",
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-parse": "^10.0.1",
|
||||||
|
"remark-rehype": "^10.1.0",
|
||||||
|
"revolt.js": "6.0.17",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.35.1",
|
"sass": "^1.35.1",
|
||||||
|
"semver": "^7.3.7",
|
||||||
"shade-blend-color": "^1.0.0",
|
"shade-blend-color": "^1.0.0",
|
||||||
|
"slate": "^0.81.1",
|
||||||
|
"slate-history": "^0.66.0",
|
||||||
|
"slate-react": "^0.81.0",
|
||||||
|
"stacktrace-js": "^2.0.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"ulid": "^2.3.0",
|
"ulid": "^2.3.0",
|
||||||
|
"unified": "^10.1.2",
|
||||||
|
"unist-util-visit": "^4.1.0",
|
||||||
"use-resize-observer": "^7.0.0",
|
"use-resize-observer": "^7.0.0",
|
||||||
"vite-plugin-pwa": "^0.8.1",
|
"vite-plugin-pwa": "^0.12.3",
|
||||||
"workbox-precaching": "^6.1.5"
|
"workbox-precaching": "^6.1.5"
|
||||||
},
|
},
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "https://github.com/revoltchat/revite.git",
|
"repository": "https://github.com/revoltchat/revite.git",
|
||||||
"author": "Paul <paulmakles@gmail.com>",
|
"author": "Paul <paulmakles@gmail.com>",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"packageManager": "yarn@3.2.0",
|
||||||
|
"resolutions": {
|
||||||
|
"@revoltchat/ui": "portal:external/components",
|
||||||
|
"revolt.js": "portal:external/revolt.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
[{
|
[
|
||||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
{
|
||||||
"target": {
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
"namespace": "android_app",
|
"target": {
|
||||||
"package_name": "chat.revolt.app.twa",
|
"namespace": "android_app",
|
||||||
"sha256_cert_fingerprints": [
|
"package_name": "chat.revolt.app.twa",
|
||||||
"6E:62:C1:BF:5A:2D:11:31:A3:22:91:8D:22:2B:2C:49:D3:70:F3:A1:45:DF:11:6A:97:DC:4C:A9:3B:C3:AA:FB"
|
"sha256_cert_fingerprints": [
|
||||||
]
|
"6E:62:C1:BF:5A:2D:11:31:A3:22:91:8D:22:2B:2C:49:D3:70:F3:A1:45:DF:11:6A:97:DC:4C:A9:3B:C3:AA:FB"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "chat.revolt.app.twa",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"2B:C7:89:87:BD:62:88:38:7B:C0:D7:5F:D1:10:F4:91:D5:24:A6:B3:25:3A:75:C2:3A:91:07:1B:63:C0:98:67"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}]
|
]
|
||||||
|
|||||||
1
public/assets_default/badges/amog.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
1
public/assets_default/badges/founder.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
1
public/assets_default/badges/moderation.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
1
public/assets_default/badges/paw.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
1
public/assets_default/badges/raccoon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
1
public/assets_default/badges/supporter.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
22
scripts/locale.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const { readdirSync } = require("fs");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"var locale_keys = " +
|
||||||
|
JSON.stringify([
|
||||||
|
...readdirSync("node_modules/dayjs/locale")
|
||||||
|
.filter((x) => x.endsWith(".js"))
|
||||||
|
.map((x) => {
|
||||||
|
v = x.split(".");
|
||||||
|
v.pop();
|
||||||
|
return v.join(".");
|
||||||
|
}),
|
||||||
|
...readdirSync("external/lang")
|
||||||
|
.filter((x) => x.endsWith(".json"))
|
||||||
|
.map((x) => {
|
||||||
|
v = x.split(".");
|
||||||
|
v.pop();
|
||||||
|
return v.join(".");
|
||||||
|
}),
|
||||||
|
]) +
|
||||||
|
";",
|
||||||
|
);
|
||||||
@@ -1,7 +1,35 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
version=$(cat VERSION)
|
# Build and publish release to production server
|
||||||
|
|
||||||
|
# Remote Server
|
||||||
|
if [ -z "$REMOTE" ]; then
|
||||||
|
echo "Please set REMOTE!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remote Directory
|
||||||
|
REMOTE_DIR=/root/revite
|
||||||
|
|
||||||
|
# Post-install script
|
||||||
|
POST_INSTALL="pm2 restart revite"
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
export REVOLT_SAAS=https://github.com/revoltchat/assets
|
||||||
|
|
||||||
|
|
||||||
|
# Exit when any command fails
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 1. Build Revite
|
||||||
|
yarn build:highmem
|
||||||
|
|
||||||
|
# 2. Archive built files
|
||||||
|
tar -czvf build.tar.gz dist
|
||||||
|
|
||||||
|
# 3. Upload built files
|
||||||
|
scp build.tar.gz $REMOTE:$REMOTE_DIR/build.tar.gz
|
||||||
|
rm build.tar.gz
|
||||||
|
|
||||||
|
# 4. Apply changes
|
||||||
|
ssh $REMOTE "cd $REMOTE_DIR; tar -xvzf build.tar.gz; rm build.tar.gz; $POST_INSTALL"
|
||||||
|
|
||||||
docker build -t revoltchat/client:${version} . &&
|
|
||||||
docker tag revoltchat/client:${version} revoltchat/client:latest &&
|
|
||||||
docker push revoltchat/client:${version} &&
|
|
||||||
docker push revoltchat/client:latest
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ const { copy, remove, access } = require("fs-extra");
|
|||||||
const { exec: cexec } = require("child_process");
|
const { exec: cexec } = require("child_process");
|
||||||
const { resolve } = require("path");
|
const { resolve } = require("path");
|
||||||
|
|
||||||
let target = process.env.REVOLT_SASS;
|
let target = process.env.REVOLT_SAAS;
|
||||||
let branch = process.env.REVOLT_SASS_BRANCH;
|
let branch = process.env.REVOLT_SAAS_BRANCH;
|
||||||
let DEFAULT_DIRECTORY = "public/assets_default";
|
let DEFAULT_DIRECTORY = "public/assets_default";
|
||||||
let OUT_DIRECTORY = "public/assets";
|
let OUT_DIRECTORY = "public/assets";
|
||||||
|
|
||||||
|
|||||||
86
src/assets/changelogs.tsx
Normal 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;
|
||||||
@@ -151,7 +151,6 @@ export const emojiDictionary = {
|
|||||||
hole: "🕳️",
|
hole: "🕳️",
|
||||||
bomb: "💣",
|
bomb: "💣",
|
||||||
speech_balloon: "💬",
|
speech_balloon: "💬",
|
||||||
eye_speech_bubble: "👁️🗨️",
|
|
||||||
left_speech_bubble: "🗨️",
|
left_speech_bubble: "🗨️",
|
||||||
right_anger_bubble: "🗯️",
|
right_anger_bubble: "🗯️",
|
||||||
thought_balloon: "💭",
|
thought_balloon: "💭",
|
||||||
@@ -673,6 +672,7 @@ export const emojiDictionary = {
|
|||||||
mandarin: "🍊",
|
mandarin: "🍊",
|
||||||
lemon: "🍋",
|
lemon: "🍋",
|
||||||
banana: "🍌",
|
banana: "🍌",
|
||||||
|
nanner: "🍌",
|
||||||
pineapple: "🍍",
|
pineapple: "🍍",
|
||||||
mango: "🥭",
|
mango: "🥭",
|
||||||
apple: "🍎",
|
apple: "🍎",
|
||||||
@@ -876,6 +876,7 @@ export const emojiDictionary = {
|
|||||||
train: "🚋",
|
train: "🚋",
|
||||||
bus: "🚌",
|
bus: "🚌",
|
||||||
oncoming_bus: "🚍",
|
oncoming_bus: "🚍",
|
||||||
|
trolley: "🚎",
|
||||||
trolleybus: "🚎",
|
trolleybus: "🚎",
|
||||||
minibus: "🚐",
|
minibus: "🚐",
|
||||||
ambulance: "🚑",
|
ambulance: "🚑",
|
||||||
@@ -1847,4 +1848,107 @@ export const emojiDictionary = {
|
|||||||
england: "🏴",
|
england: "🏴",
|
||||||
scotland: "🏴",
|
scotland: "🏴",
|
||||||
wales: "🏴",
|
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",
|
||||||
|
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",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import call_join from "./call_join.mp3";
|
|
||||||
import call_leave from "./call_leave.mp3";
|
|
||||||
import message from "./message.mp3";
|
|
||||||
import outbound from "./outbound.mp3";
|
|
||||||
|
|
||||||
const SoundMap: { [key in Sounds]: string } = {
|
|
||||||
message,
|
|
||||||
outbound,
|
|
||||||
call_join,
|
|
||||||
call_leave,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Sounds = "message" | "outbound" | "call_join" | "call_leave";
|
|
||||||
export const SOUNDS_ARRAY: Sounds[] = [
|
|
||||||
"message",
|
|
||||||
"outbound",
|
|
||||||
"call_join",
|
|
||||||
"call_leave",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function playSound(sound: Sounds) {
|
|
||||||
const file = SoundMap[sound];
|
|
||||||
const el = new Audio(file);
|
|
||||||
try {
|
|
||||||
el.play();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to play audio file", file, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
src/components/README.md
Normal 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`
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { dispatch, getState } from "../../redux";
|
import { Button, Checkbox } from "@revoltchat/ui";
|
||||||
|
|
||||||
import Button from "../ui/Button";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
import Checkbox from "../ui/Checkbox";
|
import { SECTION_NSFW } from "../../mobx/stores/Layout";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -49,9 +47,7 @@ type Props = {
|
|||||||
|
|
||||||
export default observer((props: Props) => {
|
export default observer((props: Props) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [consent, setConsent] = useState(
|
const layout = useApplicationState().layout;
|
||||||
getState().sectionToggle["nsfw"] ?? false,
|
|
||||||
);
|
|
||||||
const [ageGate, setAgeGate] = useState(false);
|
const [ageGate, setAgeGate] = useState(false);
|
||||||
|
|
||||||
if (ageGate || !props.gated) {
|
if (ageGate || !props.gated) {
|
||||||
@@ -81,26 +77,19 @@ export default observer((props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={consent}
|
title={<Text id="app.main.channel.nsfw.confirm" />}
|
||||||
onChange={(v) => {
|
value={layout.getSectionState(SECTION_NSFW, false)}
|
||||||
setConsent(v);
|
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}
|
||||||
if (v) {
|
/>
|
||||||
dispatch({
|
|
||||||
type: "SECTION_TOGGLE_SET",
|
|
||||||
id: "nsfw",
|
|
||||||
state: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch({ type: "SECTION_TOGGLE_UNSET", id: "nsfw" });
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<Text id="app.main.channel.nsfw.confirm" />
|
|
||||||
</Checkbox>
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Button contrast onClick={() => history.goBack()}>
|
<Button palette="secondary" onClick={() => history.goBack()}>
|
||||||
<Text id="app.special.modals.actions.back" />
|
<Text id="app.special.modals.actions.back" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button contrast onClick={() => consent && setAgeGate(true)}>
|
<Button
|
||||||
|
palette="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
|
||||||
|
}>
|
||||||
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
|
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { Link } from "react-router-dom";
|
||||||
import styled, { css } from "styled-components";
|
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 { StateUpdater, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import ChannelIcon from "./ChannelIcon";
|
import ChannelIcon from "./ChannelIcon";
|
||||||
import Emoji from "./Emoji";
|
import Emoji from "./Emoji";
|
||||||
|
import ServerIcon from "./ServerIcon";
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
import UserIcon from "./user/UserIcon";
|
import UserIcon from "./user/UserIcon";
|
||||||
|
|
||||||
export type AutoCompleteState =
|
export type AutoCompleteState =
|
||||||
| { type: "none" }
|
| { type: "none" }
|
||||||
| ({ selected: number; within: boolean } & (
|
| ({ selected: number; within: boolean } & (
|
||||||
| {
|
| {
|
||||||
type: "emoji";
|
type: "emoji";
|
||||||
matches: string[];
|
matches: (string | CustomEmoji)[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "user";
|
type: "user";
|
||||||
matches: User[];
|
matches: User[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "channel";
|
type: "channel";
|
||||||
matches: Channel[];
|
matches: Channel[];
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
export type SearchClues = {
|
export type SearchClues = {
|
||||||
users?: { type: "channel"; id: string } | { type: "all" };
|
users?: { type: "channel"; id: string } | { type: "all" };
|
||||||
@@ -61,7 +64,7 @@ export function useAutoComplete(
|
|||||||
const cursor = el.selectionStart;
|
const cursor = el.selectionStart;
|
||||||
const content = el.value.slice(0, cursor);
|
const content = el.value.slice(0, cursor);
|
||||||
|
|
||||||
const valid = /\w/;
|
const valid = /[\w\-]/;
|
||||||
|
|
||||||
let j = content.length - 1;
|
let j = content.length - 1;
|
||||||
if (content[j] === "@") {
|
if (content[j] === "@") {
|
||||||
@@ -79,17 +82,17 @@ export function useAutoComplete(
|
|||||||
|
|
||||||
if (current === ":" || current === "@" || current === "#") {
|
if (current === ":" || current === "@" || current === "#") {
|
||||||
const search = content.slice(j + 1, content.length);
|
const search = content.slice(j + 1, content.length);
|
||||||
const minLen = current === ":" ? 2 : 1
|
const minLen = current === ":" ? 2 : 1;
|
||||||
|
|
||||||
if (search.length >= minLen) {
|
if (search.length >= minLen) {
|
||||||
return [
|
return [
|
||||||
current === "#"
|
current === "#"
|
||||||
? "channel"
|
? "channel"
|
||||||
: current === ":"
|
: current === ":"
|
||||||
? "emoji"
|
? "emoji"
|
||||||
: "user",
|
: "user",
|
||||||
search.toLowerCase(),
|
search.toLowerCase(),
|
||||||
j + 1,
|
current === ":" ? j + 1 : j,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,16 +109,23 @@ export function useAutoComplete(
|
|||||||
|
|
||||||
if (type === "emoji") {
|
if (type === "emoji") {
|
||||||
// ! TODO: we should convert it to a Binary Search Tree and use that
|
// ! TODO: we should convert it to a Binary Search Tree and use that
|
||||||
const matches = Object.keys(emojiDictionary)
|
const matches = [
|
||||||
.filter((emoji: string) => emoji.match(regex))
|
...Object.keys(emojiDictionary).filter((emoji: string) =>
|
||||||
.splice(0, 5);
|
emoji.match(regex),
|
||||||
|
),
|
||||||
|
...Array.from(client.emojis.values()).filter((emoji) =>
|
||||||
|
emoji.name.match(regex),
|
||||||
|
),
|
||||||
|
].splice(0, 5);
|
||||||
|
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
const currentPosition =
|
const currentPosition =
|
||||||
state.type !== "none" ? state.selected : 0;
|
state.type !== "none" ? state.selected : 0;
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
// @ts-ignore-next-line are you high
|
||||||
type: "emoji",
|
type: "emoji",
|
||||||
|
// @ts-ignore-next-line
|
||||||
matches,
|
matches,
|
||||||
selected: Math.min(currentPosition, matches.length - 1),
|
selected: Math.min(currentPosition, matches.length - 1),
|
||||||
within: false,
|
within: false,
|
||||||
@@ -167,8 +177,8 @@ export function useAutoComplete(
|
|||||||
const matches = (
|
const matches = (
|
||||||
search.length > 0
|
search.length > 0
|
||||||
? users.filter((user) =>
|
? users.filter((user) =>
|
||||||
user.username.toLowerCase().match(regex),
|
user.username.toLowerCase().match(regex),
|
||||||
)
|
)
|
||||||
: users
|
: users
|
||||||
)
|
)
|
||||||
.splice(0, 5)
|
.splice(0, 5)
|
||||||
@@ -199,8 +209,8 @@ export function useAutoComplete(
|
|||||||
const matches = (
|
const matches = (
|
||||||
search.length > 0
|
search.length > 0
|
||||||
? channels.filter((channel) =>
|
? channels.filter((channel) =>
|
||||||
channel.name!.toLowerCase().match(regex),
|
channel.name!.toLowerCase().match(regex),
|
||||||
)
|
)
|
||||||
: channels
|
: channels
|
||||||
)
|
)
|
||||||
.splice(0, 5)
|
.splice(0, 5)
|
||||||
@@ -235,15 +245,18 @@ export function useAutoComplete(
|
|||||||
|
|
||||||
const content = el.value.split("");
|
const content = el.value.split("");
|
||||||
if (state.type === "emoji") {
|
if (state.type === "emoji") {
|
||||||
|
const selected = state.matches[state.selected];
|
||||||
content.splice(
|
content.splice(
|
||||||
index,
|
index,
|
||||||
search.length,
|
search.length,
|
||||||
state.matches[state.selected],
|
selected instanceof CustomEmoji
|
||||||
|
? selected._id
|
||||||
|
: selected,
|
||||||
": ",
|
": ",
|
||||||
);
|
);
|
||||||
} else if (state.type === "user") {
|
} else if (state.type === "user") {
|
||||||
content.splice(
|
content.splice(
|
||||||
index - 1,
|
index,
|
||||||
search.length + 1,
|
search.length + 1,
|
||||||
"<@",
|
"<@",
|
||||||
state.matches[state.selected]._id,
|
state.matches[state.selected]._id,
|
||||||
@@ -251,7 +264,7 @@ export function useAutoComplete(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content.splice(
|
content.splice(
|
||||||
index - 1,
|
index,
|
||||||
search.length + 1,
|
search.length + 1,
|
||||||
"<#",
|
"<#",
|
||||||
state.matches[state.selected]._id,
|
state.matches[state.selected]._id,
|
||||||
@@ -390,12 +403,17 @@ export default function AutoComplete({
|
|||||||
setState,
|
setState,
|
||||||
onClick,
|
onClick,
|
||||||
}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
|
}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
|
||||||
|
const client = useClient();
|
||||||
return (
|
return (
|
||||||
<Base detached={detached}>
|
<Base detached={detached}>
|
||||||
<div>
|
<div>
|
||||||
{state.type === "emoji" &&
|
{state.type === "emoji" &&
|
||||||
state.matches.map((match, i) => (
|
state.matches.map((match, i) => (
|
||||||
<button
|
<button
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
key={match}
|
key={match}
|
||||||
className={i === state.selected ? "active" : ""}
|
className={i === state.selected ? "active" : ""}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
@@ -414,15 +432,61 @@ export default function AutoComplete({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
<Emoji
|
<div
|
||||||
emoji={
|
style={{
|
||||||
(emojiDictionary as Record<string, string>)[
|
display: "flex",
|
||||||
match
|
flexDirection: "row",
|
||||||
]
|
justifyContent: "center",
|
||||||
}
|
}}>
|
||||||
size={20}
|
{match instanceof CustomEmoji ? (
|
||||||
/>
|
<img
|
||||||
:{match}:
|
loading="lazy"
|
||||||
|
src={match.imageURL}
|
||||||
|
style={{
|
||||||
|
width: `20px`,
|
||||||
|
height: `20px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Emoji
|
||||||
|
emoji={
|
||||||
|
(
|
||||||
|
emojiDictionary as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>
|
||||||
|
)[match]
|
||||||
|
}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span style={{ paddingLeft: "4px" }}>{`:${
|
||||||
|
match instanceof CustomEmoji
|
||||||
|
? match.name
|
||||||
|
: match
|
||||||
|
}:`}</span>
|
||||||
|
</div>
|
||||||
|
{match instanceof CustomEmoji &&
|
||||||
|
match.parent.type == "Server" && (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
client.servers.get(
|
||||||
|
match.parent.id,
|
||||||
|
)?.name
|
||||||
|
}>
|
||||||
|
<Link
|
||||||
|
to={`/server/${match.parent.id}`}>
|
||||||
|
<ServerIcon
|
||||||
|
target={client.servers.get(
|
||||||
|
match.parent.id,
|
||||||
|
)}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{state.type === "user" &&
|
{state.type === "user" &&
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
|
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js";
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
|
||||||
import fallback from "./assets/group.png";
|
import fallback from "./assets/group.png";
|
||||||
|
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
||||||
|
|
||||||
interface Props extends IconBaseProps<Channel> {
|
interface Props extends IconBaseProps<Channel> {
|
||||||
isServerChannel?: boolean;
|
isServerChannel?: boolean;
|
||||||
}
|
}
|
||||||
@@ -21,7 +19,7 @@ export default observer(
|
|||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
@@ -32,7 +30,7 @@ export default observer(
|
|||||||
...imgProps
|
...imgProps
|
||||||
} = props;
|
} = props;
|
||||||
const iconURL = client.generateFileURL(
|
const iconURL = client.generateFileURL(
|
||||||
target?.icon ?? attachment,
|
target?.icon ?? attachment ?? undefined,
|
||||||
{ max_side: 256 },
|
{ max_side: 256 },
|
||||||
animate,
|
animate,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
import { State, store } from "../../redux";
|
import { Details } from "@revoltchat/ui";
|
||||||
import { Action } from "../../redux/reducers";
|
|
||||||
|
|
||||||
import Details from "../ui/Details";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -25,30 +22,17 @@ export default function CollapsibleSection({
|
|||||||
children,
|
children,
|
||||||
...detailsProps
|
...detailsProps
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const state: State = store.getState();
|
const layout = useApplicationState().layout;
|
||||||
|
|
||||||
function setState(state: boolean) {
|
|
||||||
if (state === defaultValue) {
|
|
||||||
store.dispatch({
|
|
||||||
type: "SECTION_TOGGLE_UNSET",
|
|
||||||
id,
|
|
||||||
} as Action);
|
|
||||||
} else {
|
|
||||||
store.dispatch({
|
|
||||||
type: "SECTION_TOGGLE_SET",
|
|
||||||
id,
|
|
||||||
state,
|
|
||||||
} as Action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Details
|
<Details
|
||||||
open={state.sectionToggle[id] ?? defaultValue}
|
open={layout.getSectionState(id, defaultValue)}
|
||||||
onToggle={(e) => setState(e.currentTarget.open)}
|
onToggle={(e) =>
|
||||||
|
layout.setSectionState(id, e.currentTarget.open, defaultValue)
|
||||||
|
}
|
||||||
{...detailsProps}>
|
{...detailsProps}>
|
||||||
<summary>
|
<summary>
|
||||||
<div class="padding">
|
<div className="padding">
|
||||||
<ChevronDown size={20} />
|
<ChevronDown size={20} />
|
||||||
{summary}
|
{summary}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { EmojiPacks } from "../../redux/reducers/settings";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
|
||||||
let EMOJI_PACK = "mutant";
|
export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji";
|
||||||
|
|
||||||
|
let EMOJI_PACK: EmojiPack = "mutant";
|
||||||
const REVISION = 3;
|
const REVISION = 3;
|
||||||
|
|
||||||
export function setEmojiPack(pack: EmojiPacks) {
|
export function setGlobalEmojiPack(pack: EmojiPack) {
|
||||||
EMOJI_PACK = pack;
|
EMOJI_PACK = pack;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +42,13 @@ function toCodePoint(rune: string) {
|
|||||||
.join("-");
|
.join("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEmoji(emoji: string) {
|
export function parseEmoji(emoji: string) {
|
||||||
|
if (emoji.startsWith("custom:")) {
|
||||||
|
return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
|
||||||
|
7,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const codepoint = toCodePoint(emoji);
|
const codepoint = toCodePoint(emoji);
|
||||||
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
|
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { Attachment } from "revolt-api/types/Autumn";
|
import { API } from "revolt.js";
|
||||||
import styled, { css } from "styled-components";
|
import { Nullable } from "revolt.js";
|
||||||
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
|
import { Ref } from "preact";
|
||||||
|
|
||||||
export interface IconBaseProps<T> {
|
export interface IconBaseProps<T> {
|
||||||
target?: T;
|
target?: T;
|
||||||
url?: string;
|
url?: string;
|
||||||
attachment?: Attachment;
|
attachment?: Nullable<API.File>;
|
||||||
|
|
||||||
size: number;
|
size: number;
|
||||||
hover?: boolean;
|
hover?: boolean;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
|
|
||||||
|
innerRef?: Ref<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IconModifiers {
|
interface IconModifiers {
|
||||||
@@ -21,7 +26,7 @@ interface IconModifiers {
|
|||||||
|
|
||||||
export default styled.svg<IconModifiers>`
|
export default styled.svg<IconModifiers>`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { dispatch } from "../../redux";
|
import { ComboBox } from "@revoltchat/ui";
|
||||||
import { connectState } from "../../redux/connector";
|
|
||||||
|
|
||||||
import { Language, Languages } from "../../context/Locale";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import ComboBox from "../ui/ComboBox";
|
import { Language, Languages } from "../../../external/lang/Languages";
|
||||||
|
|
||||||
type Props = {
|
/**
|
||||||
locale: string;
|
* Component providing a language selector combobox.
|
||||||
};
|
* Note: this is not an observer but this is fine as we are just using a combobox.
|
||||||
|
*/
|
||||||
|
export default function LocaleSelector() {
|
||||||
|
const locale = useApplicationState().locale;
|
||||||
|
|
||||||
export function LocaleSelector(props: Props) {
|
|
||||||
return (
|
return (
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value={props.locale}
|
value={locale.getLanguage()}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
dispatch({
|
locale.setLanguage(e.currentTarget.value as Language)
|
||||||
type: "SET_LOCALE",
|
|
||||||
locale: e.currentTarget.value as Language,
|
|
||||||
})
|
|
||||||
}>
|
}>
|
||||||
{Object.keys(Languages).map((x) => {
|
{Object.keys(Languages).map((x) => {
|
||||||
const l = Languages[x as keyof typeof Languages];
|
const l = Languages[x as keyof typeof Languages];
|
||||||
@@ -30,9 +28,3 @@ export function LocaleSelector(props: Props) {
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connectState(LocaleSelector, (state) => {
|
|
||||||
return {
|
|
||||||
locale: state.locale,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -2,86 +2,144 @@ import { Check } from "@styled-icons/boxicons-regular";
|
|||||||
import { Cog } from "@styled-icons/boxicons-solid";
|
import { Cog } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ServerPermission } from "revolt.js/dist/api/permissions";
|
import { Server } from "revolt.js";
|
||||||
import { Server } from "revolt.js/dist/maps/Servers";
|
import styled, { css } from "styled-components/macro";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import Header from "../ui/Header";
|
import { IconButton } from "@revoltchat/ui";
|
||||||
import IconButton from "../ui/IconButton";
|
|
||||||
|
|
||||||
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
server: Server;
|
server: Server;
|
||||||
|
background?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerName = styled.div`
|
const ServerBanner = styled.div<Omit<Props, "server">>`
|
||||||
flex-grow: 1;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: norepeat;
|
||||||
|
background-position: center center;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.background
|
||||||
|
? css`
|
||||||
|
height: 120px;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
var(--secondary-background),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
background-color: var(--secondary-header);
|
||||||
|
`}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: var(--header-height);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(({ server }: Props) => {
|
export default observer(({ server }: Props) => {
|
||||||
const bannerURL = server.generateBannerURL({ width: 480 });
|
const bannerURL = server.generateBannerURL({ width: 480 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header
|
<ServerBanner
|
||||||
borders
|
|
||||||
placement="secondary"
|
|
||||||
background={typeof bannerURL !== "undefined"}
|
background={typeof bannerURL !== "undefined"}
|
||||||
style={{
|
style={{
|
||||||
background: bannerURL ? `url('${bannerURL}')` : undefined,
|
backgroundImage: bannerURL ? `url('${bannerURL}')` : undefined,
|
||||||
}}>
|
}}>
|
||||||
{server.flags && server.flags & 1 ? (
|
<div className="container">
|
||||||
<Tooltip
|
{server.flags && server.flags & 1 ? (
|
||||||
content={<Text id="app.special.server-badges.official" />}
|
<Tooltip
|
||||||
placement={"bottom-start"}>
|
content={
|
||||||
<svg width="20" height="20">
|
<Text id="app.special.server-badges.official" />
|
||||||
<image
|
}
|
||||||
xlinkHref="/assets/badges/verified.svg"
|
placement={"bottom-start"}>
|
||||||
height="20"
|
<svg width="20" height="20">
|
||||||
width="20"
|
<image
|
||||||
/>
|
xlinkHref="/assets/badges/verified.svg"
|
||||||
<image
|
height="20"
|
||||||
xlinkHref="/assets/badges/revolt_r.svg"
|
width="20"
|
||||||
height="15"
|
/>
|
||||||
width="15"
|
<image
|
||||||
x="2"
|
xlinkHref="/assets/badges/revolt_r.svg"
|
||||||
y="3"
|
height="15"
|
||||||
style={
|
width="15"
|
||||||
"justify-content: center; align-items: center; filter: brightness(0);"
|
x="2"
|
||||||
}
|
y="3"
|
||||||
/>
|
style={
|
||||||
</svg>
|
"justify-content: center; align-items: center; filter: brightness(0);"
|
||||||
</Tooltip>
|
}
|
||||||
) : undefined}
|
/>
|
||||||
{server.flags && server.flags & 2 ? (
|
</svg>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
content={<Text id="app.special.server-badges.verified" />}
|
) : undefined}
|
||||||
placement={"bottom-start"}>
|
{server.flags && server.flags & 2 ? (
|
||||||
<svg width="20" height="20">
|
<Tooltip
|
||||||
<image
|
content={
|
||||||
xlinkHref="/assets/badges/verified.svg"
|
<Text id="app.special.server-badges.verified" />
|
||||||
height="20"
|
}
|
||||||
width="20"
|
placement={"bottom-start"}>
|
||||||
/>
|
<svg width="20" height="20">
|
||||||
<foreignObject x="2" y="2" width="15" height="15">
|
<image
|
||||||
<Check size={15} color="black" strokeWidth={8} />
|
xlinkHref="/assets/badges/verified.svg"
|
||||||
</foreignObject>
|
height="20"
|
||||||
</svg>
|
width="20"
|
||||||
</Tooltip>
|
/>
|
||||||
) : undefined}
|
<foreignObject x="2" y="2" width="15" height="15">
|
||||||
|
<Check
|
||||||
<ServerName>{server.name}</ServerName>
|
size={15}
|
||||||
{(server.permission & ServerPermission.ManageServer) > 0 && (
|
color="black"
|
||||||
<div className="actions">
|
strokeWidth={8}
|
||||||
|
/>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined}
|
||||||
|
<a
|
||||||
|
className="title"
|
||||||
|
onClick={() =>
|
||||||
|
modalController.push({ type: "server_info", server })
|
||||||
|
}>
|
||||||
|
{server.name}
|
||||||
|
</a>
|
||||||
|
{server.havePermission("ManageServer") && (
|
||||||
<Link to={`/server/${server._id}/settings`}>
|
<Link to={`/server/${server._id}/settings`}>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<Cog size={24} />
|
<Cog size={20} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</Header>
|
</ServerBanner>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Server } from "revolt.js/dist/maps/Servers";
|
import { Server } from "revolt.js";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
|
||||||
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
||||||
|
|
||||||
interface Props extends IconBaseProps<Server> {
|
interface Props extends IconBaseProps<Server> {
|
||||||
@@ -13,10 +12,13 @@ interface Props extends IconBaseProps<Server> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ServerText = styled.div`
|
const ServerText = styled.div`
|
||||||
display: grid;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
place-items: center;
|
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background: var(--primary-background);
|
background: var(--primary-background);
|
||||||
border-radius: var(--border-radius-half);
|
border-radius: var(--border-radius-half);
|
||||||
@@ -31,12 +33,12 @@ export default observer(
|
|||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const { target, attachment, size, animate, server_name, ...imgProps } =
|
const { target, attachment, size, animate, server_name, ...imgProps } =
|
||||||
props;
|
props;
|
||||||
const iconURL = client.generateFileURL(
|
const iconURL = client.generateFileURL(
|
||||||
target?.icon ?? attachment,
|
target?.icon ?? attachment ?? undefined,
|
||||||
{ max_side: 256 },
|
{ max_side: 256 },
|
||||||
animate,
|
animate,
|
||||||
);
|
);
|
||||||
@@ -49,7 +51,9 @@ export default observer(
|
|||||||
{name
|
{name
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((x) => x[0])
|
.map((x) => x[0])
|
||||||
.filter((x) => typeof x !== "undefined")}
|
.filter((x) => typeof x !== "undefined")
|
||||||
|
.join("")
|
||||||
|
.substring(0, 3)}
|
||||||
</ServerText>
|
</ServerText>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import Tippy, { TippyProps } from "@tippyjs/react";
|
import Tippy, { TippyProps } from "@tippyjs/react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
|
||||||
|
|
||||||
type Props = Omit<TippyProps, "children"> & {
|
type Props = Omit<TippyProps, "children"> & {
|
||||||
children: Children;
|
children: Children;
|
||||||
content: Children;
|
content: Children;
|
||||||
@@ -14,7 +12,7 @@ export default function Tooltip(props: Props) {
|
|||||||
const { children, content, ...tippyProps } = props;
|
const { children, content, ...tippyProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tippy content={content} {...tippyProps}>
|
<Tippy content={content} animation="shift-away" {...tippyProps}>
|
||||||
{/*
|
{/*
|
||||||
// @ts-expect-error Type mis-match. */}
|
// @ts-expect-error Type mis-match. */}
|
||||||
<div style={`display: flex;`}>{children}</div>
|
<div style={`display: flex;`}>{children}</div>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
|
import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||||
|
|
||||||
import { ThemeContext } from "../../context/Theme";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import IconButton from "../ui/IconButton";
|
import { updateSW } from "../../updateWorker";
|
||||||
|
|
||||||
import { updateSW } from "../../main";
|
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
let pendingUpdate = false;
|
let pendingUpdate = false;
|
||||||
@@ -27,27 +27,30 @@ export default function UpdateIndicator({ style }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!pending) return null;
|
if (!pending) return null;
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useApplicationState().settings.theme;
|
||||||
|
|
||||||
if (style === "titlebar") {
|
if (style === "titlebar") {
|
||||||
return (
|
return (
|
||||||
<div class="actions">
|
<div className="actions">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="A new update is available!"
|
content="A new update is available!"
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<div onClick={() => updateSW(true)}>
|
<div onClick={() => updateSW(true)}>
|
||||||
<CloudDownload size={22} color={theme.success} />
|
<CloudDownload
|
||||||
|
size={22}
|
||||||
|
color={theme.getVariable("success")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.isNative) return null;
|
if (window.isNative && window.native.getConfig().frame) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton onClick={() => updateSW(true)}>
|
<IconButton onClick={() => updateSW(true)}>
|
||||||
<Download size={22} color={theme.success} />
|
<Download size={22} color={theme.getVariable("success")} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
|
import { Message as MessageObject } from "revolt.js";
|
||||||
|
|
||||||
import { attachContextMenu } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import { useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Category } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { I18nError } from "../../../context/Locale";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import Overline from "../../ui/Overline";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
import Markdown from "../../markdown/Markdown";
|
import Markdown from "../../markdown/Markdown";
|
||||||
import UserIcon from "../user/UserIcon";
|
import UserIcon from "../user/UserIcon";
|
||||||
import { Username } from "../user/UserShort";
|
import { Username } from "../user/UserShort";
|
||||||
@@ -25,13 +25,15 @@ import MessageBase, {
|
|||||||
} from "./MessageBase";
|
} from "./MessageBase";
|
||||||
import Attachment from "./attachments/Attachment";
|
import Attachment from "./attachments/Attachment";
|
||||||
import { MessageReply } from "./attachments/MessageReply";
|
import { MessageReply } from "./attachments/MessageReply";
|
||||||
|
import { Reactions } from "./attachments/Reactions";
|
||||||
|
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
|
||||||
import Embed from "./embed/Embed";
|
import Embed from "./embed/Embed";
|
||||||
import InviteList from "./embed/EmbedInvite";
|
import InviteList from "./embed/EmbedInvite";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachContext?: boolean;
|
attachContext?: boolean;
|
||||||
queued?: QueuedMessage;
|
queued?: QueuedMessage;
|
||||||
message: MessageObject;
|
message: MessageObject & { webhook: { name: string; avatar?: string } };
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
contrast?: boolean;
|
contrast?: boolean;
|
||||||
content?: Children;
|
content?: Children;
|
||||||
@@ -50,27 +52,27 @@ const Message = observer(
|
|||||||
queued,
|
queued,
|
||||||
hideReply,
|
hideReply,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const client = useClient();
|
const client = message.client;
|
||||||
const user = message.author;
|
const user = message.author;
|
||||||
|
|
||||||
const { openScreen } = useIntermediate();
|
const content = message.content;
|
||||||
|
|
||||||
const content = message.content as string;
|
|
||||||
const head =
|
const head =
|
||||||
preferHead || (message.reply_ids && message.reply_ids.length > 0);
|
preferHead || (message.reply_ids && message.reply_ids.length > 0);
|
||||||
|
|
||||||
// ! TODO: tell fatal to make this type generic
|
|
||||||
// bree: Fatal please...
|
|
||||||
const userContext = attachContext
|
const userContext = attachContext
|
||||||
? (attachContextMenu("Menu", {
|
? useTriggerEvents("Menu", {
|
||||||
user: message.author_id,
|
user: message.author_id,
|
||||||
contextualChannel: message.channel_id,
|
contextualChannel: message.channel_id,
|
||||||
|
contextualMessage: message._id,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}) as any)
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const openProfile = () =>
|
const openProfile = () =>
|
||||||
openScreen({ id: "profile", user_id: message.author_id });
|
modalController.push({
|
||||||
|
type: "user_profile",
|
||||||
|
user_id: message.author_id,
|
||||||
|
});
|
||||||
|
|
||||||
const handleUserClick = (e: MouseEvent) => {
|
const handleUserClick = (e: MouseEvent) => {
|
||||||
if (e.shiftKey && user?._id) {
|
if (e.shiftKey && user?._id) {
|
||||||
@@ -86,7 +88,9 @@ const Message = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ! FIXME(?): animate on hover
|
// ! FIXME(?): animate on hover
|
||||||
const [animate, setAnimate] = useState(false);
|
const [mouseHovering, setAnimate] = useState(false);
|
||||||
|
const [reactionsOpen, setReactionsOpen] = useState(false);
|
||||||
|
useEffect(() => setAnimate(false), [replacement]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={message._id}>
|
<div id={message._id}>
|
||||||
@@ -96,7 +100,7 @@ const Message = observer(
|
|||||||
key={message_id}
|
key={message_id}
|
||||||
index={index}
|
index={index}
|
||||||
id={message_id}
|
id={message_id}
|
||||||
channel={message.channel!}
|
channel={message.channel}
|
||||||
parent_mentions={message.mention_ids ?? []}
|
parent_mentions={message.mention_ids ?? []}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -114,28 +118,36 @@ const Message = observer(
|
|||||||
}
|
}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
sending={typeof queued !== "undefined"}
|
sending={typeof queued !== "undefined"}
|
||||||
mention={message.mention_ids?.includes(client.user!._id)}
|
mention={
|
||||||
failed={typeof queued?.error !== "undefined"}
|
message.mention_ids && client.user
|
||||||
onContextMenu={
|
? message.mention_ids.includes(client.user._id)
|
||||||
attachContext
|
|
||||||
? attachContextMenu("Menu", {
|
|
||||||
message,
|
|
||||||
contextualChannel: message.channel_id,
|
|
||||||
queued,
|
|
||||||
})
|
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
failed={typeof queued?.error !== "undefined"}
|
||||||
|
{...(attachContext
|
||||||
|
? useTriggerEvents("Menu", {
|
||||||
|
message,
|
||||||
|
contextualChannel: message.channel_id,
|
||||||
|
queued,
|
||||||
|
})
|
||||||
|
: undefined)}
|
||||||
onMouseEnter={() => setAnimate(true)}
|
onMouseEnter={() => setAnimate(true)}
|
||||||
onMouseLeave={() => setAnimate(false)}>
|
onMouseLeave={() => setAnimate(false)}>
|
||||||
<MessageInfo>
|
<MessageInfo click={typeof head !== "undefined"}>
|
||||||
{head ? (
|
{head ? (
|
||||||
<UserIcon
|
<UserIcon
|
||||||
|
className="avatar"
|
||||||
url={message.generateMasqAvatarURL()}
|
url={message.generateMasqAvatarURL()}
|
||||||
|
override={
|
||||||
|
message.webhook?.avatar
|
||||||
|
? `https://autumn.revolt.chat/avatars/${message.webhook.avatar}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
target={user}
|
target={user}
|
||||||
size={36}
|
size={36}
|
||||||
onContextMenu={userContext}
|
|
||||||
onClick={handleUserClick}
|
onClick={handleUserClick}
|
||||||
animate={animate}
|
animate={mouseHovering}
|
||||||
|
{...(userContext as any)}
|
||||||
showServerIdentity
|
showServerIdentity
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -150,8 +162,9 @@ const Message = observer(
|
|||||||
className="author"
|
className="author"
|
||||||
showServerIdentity
|
showServerIdentity
|
||||||
onClick={handleUserClick}
|
onClick={handleUserClick}
|
||||||
onContextMenu={userContext}
|
|
||||||
masquerade={message.masquerade!}
|
masquerade={message.masquerade!}
|
||||||
|
override={message.webhook?.name}
|
||||||
|
{...userContext}
|
||||||
/>
|
/>
|
||||||
<MessageDetail
|
<MessageDetail
|
||||||
message={message}
|
message={message}
|
||||||
@@ -159,21 +172,38 @@ const Message = observer(
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{replacement ?? <Markdown content={content} />}
|
{replacement ??
|
||||||
|
(content && <Markdown content={content} />)}
|
||||||
{!queued && <InviteList message={message} />}
|
{!queued && <InviteList message={message} />}
|
||||||
{queued?.error && (
|
{queued?.error && (
|
||||||
<Overline type="error" error={queued.error} />
|
<Category>
|
||||||
|
<I18nError error={queued.error} />
|
||||||
|
</Category>
|
||||||
)}
|
)}
|
||||||
{message.attachments?.map((attachment, index) => (
|
{message.attachments?.map((attachment, index) => (
|
||||||
<Attachment
|
<Attachment
|
||||||
key={index}
|
key={index}
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
hasContent={index > 0 || content.length > 0}
|
hasContent={
|
||||||
|
index > 0 ||
|
||||||
|
(content ? content.length > 0 : false)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{message.embeds?.map((embed, index) => (
|
{message.embeds?.map((embed, index) => (
|
||||||
<Embed key={index} embed={embed} />
|
<Embed key={index} embed={embed} />
|
||||||
))}
|
))}
|
||||||
|
<Reactions message={message} />
|
||||||
|
{(mouseHovering || reactionsOpen) &&
|
||||||
|
!replacement &&
|
||||||
|
!isTouchscreenDevice && (
|
||||||
|
<MessageOverlayBar
|
||||||
|
reactionsOpen={reactionsOpen}
|
||||||
|
setReactionsOpen={setReactionsOpen}
|
||||||
|
message={message}
|
||||||
|
queued={queued}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
</MessageBase>
|
</MessageBase>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import { Message } from "revolt.js";
|
||||||
import styled, { css, keyframes } from "styled-components";
|
import styled, { css, keyframes } from "styled-components/macro";
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
@@ -134,7 +134,7 @@ export default styled.div<BaseMessageProps>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageInfo = styled.div`
|
export const MessageInfo = styled.div<{ click: boolean }>`
|
||||||
width: 62px;
|
width: 62px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -142,6 +142,14 @@ export const MessageInfo = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.copyBracket {
|
.copyBracket {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -152,15 +160,6 @@ export const MessageInfo = styled.div`
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time {
|
time {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -192,9 +191,19 @@ export const MessageInfo = styled.div`
|
|||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
color: var(--tertiary-foreground);
|
color: var(--tertiary-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*${(props) =>
|
||||||
|
props.click &&
|
||||||
|
css`
|
||||||
|
cursor: pointer;
|
||||||
|
`}*/
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageContent = styled.div`
|
export const MessageContent = styled.div`
|
||||||
|
// Position relatively so we can put
|
||||||
|
// the overlay in the right place.
|
||||||
|
position: relative;
|
||||||
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,13 +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 Axios, { CancelTokenSource } from "axios";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ChannelPermission } from "revolt.js/dist/api/permissions";
|
import { Channel } from "revolt.js";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import styled, { css } from "styled-components/macro";
|
||||||
import styled, { css } from "styled-components";
|
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
import { memo } from "preact/compat";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton, Picker } from "@revoltchat/ui";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
import { debounce } from "../../../lib/debounce";
|
import { debounce } from "../../../lib/debounce";
|
||||||
@@ -20,21 +22,25 @@ import {
|
|||||||
SMOOTH_SCROLL_ON_RECEIVE,
|
SMOOTH_SCROLL_ON_RECEIVE,
|
||||||
} from "../../../lib/renderer/Singleton";
|
} from "../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
import { dispatch, getState } from "../../../redux";
|
import { state, useApplicationState } from "../../../mobx/State";
|
||||||
import { Reply } from "../../../redux/reducers/queue";
|
import { DraftObject } from "../../../mobx/stores/Draft";
|
||||||
|
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { SoundContext } from "../../../context/Settings";
|
import { dayjs } from "../../../context/Locale";
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
|
||||||
|
import { emojiDictionary } from "../../../assets/emojis";
|
||||||
|
import {
|
||||||
|
clientController,
|
||||||
|
useClient,
|
||||||
|
} from "../../../controllers/client/ClientController";
|
||||||
|
import { takeError } from "../../../controllers/client/jsx/error";
|
||||||
import {
|
import {
|
||||||
FileUploader,
|
FileUploader,
|
||||||
grabFiles,
|
grabFiles,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
} from "../../../context/revoltjs/FileUploads";
|
} from "../../../controllers/client/jsx/legacy/FileUploads";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
import { RenderEmoji } from "../../markdown/plugins/emoji";
|
||||||
|
|
||||||
import IconButton from "../../ui/IconButton";
|
|
||||||
|
|
||||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||||
import { PermissionTooltip } from "../Tooltip";
|
import { PermissionTooltip } from "../Tooltip";
|
||||||
import FilePreview from "./bars/FilePreview";
|
import FilePreview from "./bars/FilePreview";
|
||||||
@@ -57,6 +63,7 @@ export type UploadState =
|
|||||||
| { type: "failed"; files: File[]; error: string };
|
| { type: "failed"; files: File[]; error: string };
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
background: var(--message-box);
|
background: var(--message-box);
|
||||||
@@ -79,9 +86,15 @@ const Blocked = styled.div`
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
font-size: var(--text-size);
|
font-size: var(--text-size);
|
||||||
color: var(--tertiary-foreground);
|
color: var(--tertiary-foreground);
|
||||||
|
flex-grow: 1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
padding: 14px 14px 14px 0;
|
padding: var(--message-box-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
> div > div {
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@@ -90,13 +103,17 @@ const Blocked = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Action = styled.div`
|
const Action = styled.div`
|
||||||
display: flex;
|
> a {
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
padding: 12px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
/*padding: 14px 0 14px 14px;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
width: 62px;
|
||||||
}
|
}
|
||||||
|
|
||||||
${() =>
|
${() =>
|
||||||
@@ -108,28 +125,139 @@ const Action = styled.div`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FileAction = styled.div`
|
||||||
|
> a {
|
||||||
|
height: 48px;
|
||||||
|
width: 62px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FloatingLayer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
|
||||||
|
width: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
// For sed replacement
|
// For sed replacement
|
||||||
const RE_SED = new RegExp("^s/([^])*/([^])*$");
|
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
|
// ! FIXME: add to app config and load from app config
|
||||||
export const CAN_UPLOAD_AT_ONCE = 4;
|
export const CAN_UPLOAD_AT_ONCE = 5;
|
||||||
|
|
||||||
export default observer(({ channel }: Props) => {
|
export default observer(({ channel }: Props) => {
|
||||||
const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
|
const state = useApplicationState();
|
||||||
|
|
||||||
const [uploadState, setUploadState] = useState<UploadState>({
|
const [uploadState, setUploadState] = useState<UploadState>({
|
||||||
type: "none",
|
type: "none",
|
||||||
});
|
});
|
||||||
const [typing, setTyping] = useState<boolean | number>(false);
|
const [typing, setTyping] = useState<boolean | number>(false);
|
||||||
const [replies, setReplies] = useState<Reply[]>([]);
|
const [replies, setReplies] = useState<Reply[]>([]);
|
||||||
const playSound = useContext(SoundContext);
|
const [picker, setPicker] = useState(false);
|
||||||
const { openScreen } = useIntermediate();
|
const client = useClient();
|
||||||
const client = useContext(AppContext);
|
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
|
|
||||||
|
const closePicker = useCallback(() => setPicker(false), []);
|
||||||
|
|
||||||
const renderer = getRenderer(channel);
|
const renderer = getRenderer(channel);
|
||||||
|
|
||||||
if (!(channel.permission & ChannelPermission.SendMessage)) {
|
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 (
|
return (
|
||||||
<Base>
|
<Base>
|
||||||
<Blocked>
|
<Blocked>
|
||||||
@@ -148,27 +276,23 @@ export default observer(({ channel }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push message content to draft.
|
||||||
const setMessage = useCallback(
|
const setMessage = useCallback(
|
||||||
(content?: string) => {
|
(content?: string) => {
|
||||||
setDraft(content ?? "");
|
const dobj: DraftObject = {
|
||||||
|
content,
|
||||||
if (content) {
|
};
|
||||||
dispatch({
|
state.draft.set(channel._id, dobj);
|
||||||
type: "SET_DRAFT",
|
|
||||||
channel: channel._id,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: "CLEAR_DRAFT",
|
|
||||||
channel: channel._id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[channel._id],
|
[state.draft, channel._id],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param content
|
||||||
|
* @param action
|
||||||
|
*/
|
||||||
function append(content: string, action: "quote" | "mention") {
|
function append(content: string, action: "quote" | "mention") {
|
||||||
const text =
|
const text =
|
||||||
action === "quote"
|
action === "quote"
|
||||||
@@ -178,10 +302,10 @@ export default observer(({ channel }: Props) => {
|
|||||||
.join("\n")}\n\n`
|
.join("\n")}\n\n`
|
||||||
: `${content} `;
|
: `${content} `;
|
||||||
|
|
||||||
if (!draft || draft.length === 0) {
|
if (!state.draft.has(channel._id)) {
|
||||||
setMessage(text);
|
setMessage(text);
|
||||||
} else {
|
} else {
|
||||||
setMessage(`${draft}\n${text}`);
|
setMessage(`${state.draft.get(channel._id)}\n${text}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,16 +314,20 @@ export default observer(({ channel }: Props) => {
|
|||||||
"append",
|
"append",
|
||||||
append as (...args: unknown[]) => void,
|
append as (...args: unknown[]) => void,
|
||||||
);
|
);
|
||||||
}, [draft, setMessage]);
|
}, [state.draft, channel._id, setMessage]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger send message.
|
||||||
|
*/
|
||||||
async function send() {
|
async function send() {
|
||||||
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const content = draft?.trim() ?? "";
|
const content = state.draft.get(channel._id)?.content?.trim() ?? "";
|
||||||
if (uploadState.type === "attached") return sendFile(content);
|
if (uploadState.type === "attached") return sendFile(content);
|
||||||
if (content.length === 0) return;
|
if (content.length === 0) return;
|
||||||
|
|
||||||
|
internalEmit("NewMessages", "hide");
|
||||||
stopTyping();
|
stopTyping();
|
||||||
setMessage();
|
setMessage();
|
||||||
setReplies([]);
|
setReplies([]);
|
||||||
@@ -215,7 +343,7 @@ export default observer(({ channel }: Props) => {
|
|||||||
);
|
);
|
||||||
renderer.messages.reverse();
|
renderer.messages.reverse();
|
||||||
|
|
||||||
if (msg) {
|
if (msg?.content) {
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let [_, toReplace, newText, flags] = content.split(/\//);
|
let [_, toReplace, newText, flags] = content.split(/\//);
|
||||||
|
|
||||||
@@ -247,20 +375,15 @@ export default observer(({ channel }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playSound("outbound");
|
state.settings.sounds.playSound("outbound");
|
||||||
|
|
||||||
dispatch({
|
state.queue.add(nonce, channel._id, {
|
||||||
type: "QUEUE_ADD",
|
_id: nonce,
|
||||||
nonce,
|
|
||||||
channel: channel._id,
|
channel: channel._id,
|
||||||
message: {
|
author: client.user!._id,
|
||||||
_id: nonce,
|
|
||||||
channel: channel._id,
|
|
||||||
author: client.user!._id,
|
|
||||||
|
|
||||||
content,
|
content,
|
||||||
replies,
|
replies,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
|
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
|
||||||
@@ -272,18 +395,20 @@ export default observer(({ channel }: Props) => {
|
|||||||
replies,
|
replies,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({
|
state.queue.fail(nonce, takeError(error));
|
||||||
type: "QUEUE_FAIL",
|
|
||||||
error: takeError(error),
|
|
||||||
nonce,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param content
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async function sendFile(content: string) {
|
async function sendFile(content: string) {
|
||||||
if (uploadState.type !== "attached") return;
|
if (uploadState.type !== "attached") return;
|
||||||
const attachments: string[] = [];
|
const attachments: string[] = [];
|
||||||
|
setMessage;
|
||||||
|
|
||||||
const cancel = Axios.CancelToken.source();
|
const cancel = Axios.CancelToken.source();
|
||||||
const files = uploadState.files;
|
const files = uploadState.files;
|
||||||
@@ -360,7 +485,7 @@ export default observer(({ channel }: Props) => {
|
|||||||
|
|
||||||
setMessage();
|
setMessage();
|
||||||
setReplies([]);
|
setReplies([]);
|
||||||
playSound("outbound");
|
state.settings.sounds.playSound("outbound");
|
||||||
|
|
||||||
if (files.length > CAN_UPLOAD_AT_ONCE) {
|
if (files.length > CAN_UPLOAD_AT_ONCE) {
|
||||||
setUploadState({
|
setUploadState({
|
||||||
@@ -372,6 +497,10 @@ export default observer(({ channel }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function startTyping() {
|
function startTyping() {
|
||||||
if (typeof typing === "number" && +new Date() < typing) return;
|
if (typeof typing === "number" && +new Date() < typing) return;
|
||||||
|
|
||||||
@@ -385,6 +514,10 @@ export default observer(({ channel }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param force
|
||||||
|
*/
|
||||||
function stopTyping(force?: boolean) {
|
function stopTyping(force?: boolean) {
|
||||||
if (force || typing) {
|
if (force || typing) {
|
||||||
const ws = client.websocket;
|
const ws = client.websocket;
|
||||||
@@ -398,6 +531,21 @@ export default observer(({ channel }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInCodeBlock(cursor: number): boolean {
|
||||||
|
const content = state.draft.get(channel._id)?.content || "";
|
||||||
|
const contentBeforeCursor = content.substring(0, cursor);
|
||||||
|
|
||||||
|
let delimiterCount = 0;
|
||||||
|
for (const delimiter of contentBeforeCursor.matchAll(
|
||||||
|
RE_CODE_DELIMITER,
|
||||||
|
)) {
|
||||||
|
delimiterCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Odd number of ``` delimiters before cursor => we are in code block
|
||||||
|
return delimiterCount % 2 === 1;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: change to useDebounceCallback
|
// TODO: change to useDebounceCallback
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const debouncedStopTyping = useCallback(
|
const debouncedStopTyping = useCallback(
|
||||||
@@ -434,7 +582,10 @@ export default observer(({ channel }: Props) => {
|
|||||||
files: [...uploadState.files, ...files],
|
files: [...uploadState.files, ...files],
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
openScreen({ id: "error", error: "FileTooLarge" }),
|
modalController.push({
|
||||||
|
type: "error",
|
||||||
|
error: "FileTooLarge",
|
||||||
|
}),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -457,9 +608,25 @@ export default observer(({ channel }: Props) => {
|
|||||||
replies={replies}
|
replies={replies}
|
||||||
setReplies={setReplies}
|
setReplies={setReplies}
|
||||||
/>
|
/>
|
||||||
|
<FloatingLayer>
|
||||||
|
{picker && (
|
||||||
|
<HackAlertThisFileWillBeReplaced
|
||||||
|
onSelect={(emoji) => {
|
||||||
|
const v = state.draft.get(channel._id);
|
||||||
|
const cnt: DraftObject = {
|
||||||
|
content:
|
||||||
|
(v?.content ? `${v.content} ` : "") +
|
||||||
|
`:${emoji}:`,
|
||||||
|
};
|
||||||
|
state.draft.set(channel._id, cnt);
|
||||||
|
}}
|
||||||
|
onClose={closePicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FloatingLayer>
|
||||||
<Base>
|
<Base>
|
||||||
{channel.permission & ChannelPermission.UploadFiles ? (
|
{channel.havePermission("UploadFiles") ? (
|
||||||
<Action>
|
<FileAction>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
size={24}
|
size={24}
|
||||||
behaviour="multi"
|
behaviour="multi"
|
||||||
@@ -494,8 +661,10 @@ export default observer(({ channel }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Action>
|
</FileAction>
|
||||||
) : undefined}
|
) : (
|
||||||
|
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
|
||||||
|
)}
|
||||||
<TextAreaAutoSize
|
<TextAreaAutoSize
|
||||||
autoFocus
|
autoFocus
|
||||||
hideBorder
|
hideBorder
|
||||||
@@ -503,7 +672,7 @@ export default observer(({ channel }: Props) => {
|
|||||||
id="message"
|
id="message"
|
||||||
maxLength={2000}
|
maxLength={2000}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
value={draft ?? ""}
|
value={state.draft.get(channel._id)?.content ?? ""}
|
||||||
padding="var(--message-box-padding)"
|
padding="var(--message-box-padding)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.ctrlKey && e.key === "Enter") {
|
if (e.ctrlKey && e.key === "Enter") {
|
||||||
@@ -515,7 +684,7 @@ export default observer(({ channel }: Props) => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
e.key === "ArrowUp" &&
|
e.key === "ArrowUp" &&
|
||||||
(!draft || draft.length === 0)
|
!state.draft.has(channel._id)
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
internalEmit("MessageRenderer", "edit_last");
|
internalEmit("MessageRenderer", "edit_last");
|
||||||
@@ -526,7 +695,8 @@ export default observer(({ channel }: Props) => {
|
|||||||
!e.shiftKey &&
|
!e.shiftKey &&
|
||||||
!e.isComposing &&
|
!e.isComposing &&
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
!isTouchscreenDevice
|
!isTouchscreenDevice &&
|
||||||
|
!isInCodeBlock(e.currentTarget.selectionStart)
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return send();
|
return send();
|
||||||
@@ -575,11 +745,17 @@ export default observer(({ channel }: Props) => {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
<Action>
|
<Action>
|
||||||
{/*<IconButton onClick={emojiPicker}>
|
<IconButton onClick={() => setPicker(!picker)}>
|
||||||
<HappyAlt size={20} />
|
<HappyBeaming size={24} />
|
||||||
</IconButton>*/}
|
</IconButton>
|
||||||
|
</Action>
|
||||||
|
<Action>
|
||||||
<IconButton
|
<IconButton
|
||||||
className="mobile"
|
className={
|
||||||
|
state.settings.get("appearance:show_send_button")
|
||||||
|
? ""
|
||||||
|
: "mobile"
|
||||||
|
}
|
||||||
onClick={send}
|
onClick={send}
|
||||||
onMouseDown={(e) => e.preventDefault()}>
|
onMouseDown={(e) => e.preventDefault()}>
|
||||||
<Send size={20} />
|
<Send size={20} />
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ import {
|
|||||||
EditAlt,
|
EditAlt,
|
||||||
Edit,
|
Edit,
|
||||||
MessageSquareEdit,
|
MessageSquareEdit,
|
||||||
|
Key,
|
||||||
} from "@styled-icons/boxicons-solid";
|
} from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { SystemMessage as SystemMessageI } from "revolt-api/types/Channels";
|
import { Message, API } from "revolt.js";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import styled from "styled-components/macro";
|
||||||
import styled from "styled-components";
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
import { attachContextMenu } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Row } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { TextReact } from "../../../lib/i18n";
|
import { TextReact } from "../../../lib/i18n";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import { dayjs } from "../../../context/Locale";
|
||||||
|
|
||||||
|
import Markdown from "../../markdown/Markdown";
|
||||||
|
import Tooltip from "../Tooltip";
|
||||||
import UserShort from "../user/UserShort";
|
import UserShort from "../user/UserShort";
|
||||||
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
|
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
|
||||||
|
|
||||||
@@ -29,6 +39,26 @@ const SystemContent = styled.div`
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg,
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -48,20 +78,23 @@ const iconDictionary = {
|
|||||||
channel_renamed: EditAlt,
|
channel_renamed: EditAlt,
|
||||||
channel_description_changed: Edit,
|
channel_description_changed: Edit,
|
||||||
channel_icon_changed: MessageSquareEdit,
|
channel_icon_changed: MessageSquareEdit,
|
||||||
|
channel_ownership_changed: Key,
|
||||||
text: InfoCircle,
|
text: InfoCircle,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SystemMessage = observer(
|
export const SystemMessage = observer(
|
||||||
({ attachContext, message, highlight, hideInfo }: Props) => {
|
({ attachContext, message, highlight, hideInfo }: Props) => {
|
||||||
const data = message.asSystemMessage;
|
const data = message.asSystemMessage;
|
||||||
const SystemMessageIcon =
|
if (!data) return null;
|
||||||
iconDictionary[data.type as SystemMessageI["type"]] ?? InfoCircle;
|
|
||||||
|
|
||||||
let children;
|
const settings = useApplicationState().settings;
|
||||||
|
|
||||||
|
const SystemMessageIcon =
|
||||||
|
iconDictionary[data.type as API.SystemMessage["type"]] ??
|
||||||
|
InfoCircle;
|
||||||
|
|
||||||
|
let children = null;
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "text":
|
|
||||||
children = <span>{data.content}</span>;
|
|
||||||
break;
|
|
||||||
case "user_added":
|
case "user_added":
|
||||||
case "user_remove":
|
case "user_remove":
|
||||||
children = (
|
children = (
|
||||||
@@ -81,16 +114,39 @@ export const SystemMessage = observer(
|
|||||||
case "user_joined":
|
case "user_joined":
|
||||||
case "user_left":
|
case "user_left":
|
||||||
case "user_kicked":
|
case "user_kicked":
|
||||||
case "user_banned":
|
case "user_banned": {
|
||||||
|
const createdAt = data.user ? decodeTime(data.user._id) : null;
|
||||||
children = (
|
children = (
|
||||||
<TextReact
|
<Row centred>
|
||||||
id={`app.main.channel.system.${data.type}`}
|
<TextReact
|
||||||
fields={{
|
id={`app.main.channel.system.${data.type}`}
|
||||||
user: <UserShort user={data.user} />,
|
fields={{
|
||||||
}}
|
user: <UserShort user={data.user} />,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
{data.type == "user_joined" &&
|
||||||
|
createdAt &&
|
||||||
|
(settings.get("appearance:show_account_age") ||
|
||||||
|
Date.now() - createdAt <
|
||||||
|
1000 * 60 * 60 * 24 * 7) && (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Text
|
||||||
|
id="app.main.channel.system.registered_at"
|
||||||
|
fields={{
|
||||||
|
time: dayjs(
|
||||||
|
createdAt,
|
||||||
|
).fromNow(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<InfoCircle size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "channel_renamed":
|
case "channel_renamed":
|
||||||
children = (
|
children = (
|
||||||
<TextReact
|
<TextReact
|
||||||
@@ -113,21 +169,35 @@ export const SystemMessage = observer(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "channel_ownership_changed":
|
||||||
|
children = (
|
||||||
|
<TextReact
|
||||||
|
id={`app.main.channel.system.channel_ownership_changed`}
|
||||||
|
fields={{
|
||||||
|
from: <UserShort user={data.from} />,
|
||||||
|
to: <UserShort user={data.to} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
if (message.system?.type === "text") {
|
||||||
|
children = <Markdown content={message.system?.content} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageBase
|
<MessageBase
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
onContextMenu={
|
{...(attachContext
|
||||||
attachContext
|
? useTriggerEvents("Menu", {
|
||||||
? attachContextMenu("Menu", {
|
message,
|
||||||
message,
|
contextualChannel: message.channel,
|
||||||
contextualChannel: message.channel,
|
})
|
||||||
})
|
: undefined)}>
|
||||||
: undefined
|
|
||||||
}>
|
|
||||||
{!hideInfo && (
|
{!hideInfo && (
|
||||||
<MessageInfo>
|
<MessageInfo click={false}>
|
||||||
<MessageDetail message={message} position="left" />
|
<MessageDetail message={message} position="left" />
|
||||||
<SystemMessageIcon className="systemIcon" />
|
<SystemMessageIcon className="systemIcon" />
|
||||||
</MessageInfo>
|
</MessageInfo>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { Attachment as AttachmentI } from "revolt-api/types/Autumn";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { attachContextMenu } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
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 AttachmentActions from "./AttachmentActions";
|
import AttachmentActions from "./AttachmentActions";
|
||||||
import { SizedGrid } from "./Grid";
|
import { SizedGrid } from "./Grid";
|
||||||
import ImageFile from "./ImageFile";
|
import ImageFile from "./ImageFile";
|
||||||
@@ -15,14 +13,14 @@ import Spoiler from "./Spoiler";
|
|||||||
import TextFile from "./TextFile";
|
import TextFile from "./TextFile";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: AttachmentI;
|
attachment: API.File;
|
||||||
hasContent: boolean;
|
hasContent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_ATTACHMENT_WIDTH = 480;
|
const MAX_ATTACHMENT_WIDTH = 480;
|
||||||
|
|
||||||
export default function Attachment({ attachment, hasContent }: Props) {
|
export default function Attachment({ attachment, hasContent }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { filename, metadata } = attachment;
|
const { filename, metadata } = attachment;
|
||||||
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
||||||
|
|
||||||
@@ -38,8 +36,8 @@ export default function Attachment({ attachment, hasContent }: Props) {
|
|||||||
<SizedGrid
|
<SizedGrid
|
||||||
width={metadata.width}
|
width={metadata.width}
|
||||||
height={metadata.height}
|
height={metadata.height}
|
||||||
onContextMenu={attachContextMenu("Menu", {
|
{...useTriggerEvents("Menu", {
|
||||||
attachment: attachment,
|
attachment,
|
||||||
})}
|
})}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
[styles.margin]: hasContent,
|
[styles.margin]: hasContent,
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import {
|
import {
|
||||||
Download,
|
|
||||||
LinkExternal,
|
LinkExternal,
|
||||||
File,
|
|
||||||
Headphone,
|
Headphone,
|
||||||
Video,
|
Download,
|
||||||
} from "@styled-icons/boxicons-regular";
|
} from "@styled-icons/boxicons-regular";
|
||||||
import { Attachment } from "revolt-api/types/Autumn";
|
import { File, Video } from "@styled-icons/boxicons-solid";
|
||||||
|
import { isFirefox } from "react-device-detect";
|
||||||
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./AttachmentActions.module.scss";
|
import styles from "./AttachmentActions.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { determineFileSize } from "../../../../lib/fileSize";
|
import { determineFileSize } from "../../../../lib/fileSize";
|
||||||
|
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import IconButton from "../../../ui/IconButton";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: Attachment;
|
attachment: API.File;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttachmentActions({ attachment }: Props) {
|
export default function AttachmentActions({ attachment }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { filename, metadata, size } = attachment;
|
const { filename, metadata, size } = attachment;
|
||||||
|
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment);
|
||||||
const open_url = `${url}/${filename}`;
|
const open_url = `${url}/${filename}`;
|
||||||
const download_url = url.replace("attachments", "attachments/download");
|
const download_url = url?.replace("attachments", "attachments/download");
|
||||||
|
|
||||||
const filesize = determineFileSize(size);
|
const filesize = determineFileSize(size);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
|||||||
href={download_url}
|
href={download_url}
|
||||||
className={styles.downloadIcon}
|
className={styles.downloadIcon}
|
||||||
download
|
download
|
||||||
target="_blank"
|
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<Download size={24} />
|
<Download size={24} />
|
||||||
@@ -70,7 +70,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
|||||||
href={download_url}
|
href={download_url}
|
||||||
className={styles.downloadIcon}
|
className={styles.downloadIcon}
|
||||||
download
|
download
|
||||||
target="_blank"
|
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<Download size={24} />
|
<Download size={24} />
|
||||||
@@ -90,7 +90,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
|||||||
href={download_url}
|
href={download_url}
|
||||||
className={styles.downloadIcon}
|
className={styles.downloadIcon}
|
||||||
download
|
download
|
||||||
target="_blank"
|
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<Download size={24} />
|
<Download size={24} />
|
||||||
@@ -119,7 +119,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
|||||||
href={download_url}
|
href={download_url}
|
||||||
className={styles.downloadIcon}
|
className={styles.downloadIcon}
|
||||||
download
|
download
|
||||||
target="_blank"
|
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<Download size={24} />
|
<Download size={24} />
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Children } from "../../../../types/Preact";
|
import { Ref } from "preact";
|
||||||
|
|
||||||
const Grid = styled.div<{ width: number; height: number }>`
|
const Grid = styled.div<{ width: number; height: number }>`
|
||||||
--width: ${props => props.width}px;
|
--width: ${(props) => props.width}px;
|
||||||
--height: ${props => props.height}px;
|
--height: ${(props) => props.height}px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
overflow: hidden;
|
||||||
aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
|
aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
|
||||||
|
|
||||||
max-width: min(var(--width), var(--attachment-max-width));
|
max-width: min(var(--width), var(--attachment-max-width));
|
||||||
@@ -42,7 +43,7 @@ const Grid = styled.div<{ width: number; height: number }>`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
||||||
// It's something
|
// It's something
|
||||||
object-position: left;
|
object-position: left;
|
||||||
}
|
}
|
||||||
@@ -71,13 +72,14 @@ type Props = Omit<
|
|||||||
children?: Children;
|
children?: Children;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
innerRef?: Ref<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SizedGrid(props: Props) {
|
export function SizedGrid(props: Props) {
|
||||||
const { width, height, children, ...divProps } = props;
|
const { width, height, children, innerRef, ...divProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid {...divProps} width={width} height={height}>
|
<Grid {...divProps} width={width} height={height} ref={innerRef}>
|
||||||
{children}
|
{children}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,47 +1,42 @@
|
|||||||
import { Attachment } from "revolt-api/types/Autumn";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
enum ImageLoadingState {
|
enum ImageLoadingState {
|
||||||
Loading,
|
Loading,
|
||||||
Loaded,
|
Loaded,
|
||||||
Error
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||||
attachment: Attachment;
|
attachment: API.File;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function ImageFile({ attachment, ...props }: Props) {
|
export default function ImageFile({ attachment, ...props }: Props) {
|
||||||
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
return <img
|
return (
|
||||||
{...props}
|
<img
|
||||||
src={url}
|
{...props}
|
||||||
alt={attachment.filename}
|
src={url}
|
||||||
loading="lazy"
|
alt={attachment.filename}
|
||||||
className={classNames(styles.image, {
|
loading="lazy"
|
||||||
[styles.loading]: loading !== ImageLoadingState.Loaded
|
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")
|
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||||
}
|
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
||||||
onLoad={() =>
|
onError={() => setLoading(ImageLoadingState.Error)}
|
||||||
setLoading(ImageLoadingState.Loaded)
|
/>
|
||||||
}
|
);
|
||||||
onError={() =>
|
|
||||||
setLoading(ImageLoadingState.Error)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import { Reply } from "@styled-icons/boxicons-regular";
|
|||||||
import { File } from "@styled-icons/boxicons-solid";
|
import { File } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
import { Channel, Message, API } from "revolt.js";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import styled, { css } from "styled-components/macro";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
|
||||||
import styled, { css } from "styled-components";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useLayoutEffect, useState } from "preact/hooks";
|
import { useLayoutEffect, useState } from "preact/hooks";
|
||||||
@@ -18,7 +16,7 @@ import { SystemMessage } from "../SystemMessage";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parent_mentions: string[];
|
parent_mentions: string[];
|
||||||
channel: Channel;
|
channel?: Channel;
|
||||||
index: number;
|
index: number;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
@@ -28,30 +26,25 @@ export const ReplyBase = styled.div<{
|
|||||||
fail?: boolean;
|
fail?: boolean;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
}>`
|
}>`
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-inline-start: 30px;
|
margin-inline-start: 30px;
|
||||||
margin-inline-end: 12px;
|
margin-inline-end: 12px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
align-items: center;
|
align-items: end;
|
||||||
color: var(--secondary-foreground);
|
color: var(--secondary-foreground);
|
||||||
|
|
||||||
/* nizune's Discord replies,
|
|
||||||
does not scale properly with messages,
|
|
||||||
reverted temporarily
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 22px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 28px;
|
border-inline-start: 2px solid var(--message-box);
|
||||||
margin-inline-end: 2px;
|
border-top: 2px solid var(--message-box);
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
display: flex;
|
}
|
||||||
border-top: 2.2px solid var(--tertiary-foreground);
|
|
||||||
border-inline-start: 2.2px solid var(--tertiary-foreground);
|
|
||||||
border-start-start-radius: 6px;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -60,6 +53,7 @@ export const ReplyBase = styled.div<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user {
|
.user {
|
||||||
|
//margin-inline-start: 12px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -74,13 +68,6 @@ export const ReplyBase = styled.div<{
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*&::before {
|
|
||||||
position:relative;
|
|
||||||
width: 50px;
|
|
||||||
height: 2px;
|
|
||||||
background: red;
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -98,6 +85,16 @@ export const ReplyBase = styled.div<{
|
|||||||
transition: transform ease-in-out 0.1s;
|
transition: transform ease-in-out 0.1s;
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span > p {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(2);
|
filter: brightness(2);
|
||||||
}
|
}
|
||||||
@@ -109,10 +106,6 @@ export const ReplyBase = styled.div<{
|
|||||||
> * {
|
> * {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*> span > p {
|
|
||||||
display: flex;
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> svg:first-child {
|
> svg:first-child {
|
||||||
@@ -131,6 +124,10 @@ export const ReplyBase = styled.div<{
|
|||||||
props.head &&
|
props.head &&
|
||||||
css`
|
css`
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
border-start-start-radius: 4px;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
@@ -142,6 +139,8 @@ export const ReplyBase = styled.div<{
|
|||||||
|
|
||||||
export const MessageReply = observer(
|
export const MessageReply = observer(
|
||||||
({ index, channel, id, parent_mentions }: Props) => {
|
({ index, channel, id, parent_mentions }: Props) => {
|
||||||
|
if (!channel) return null;
|
||||||
|
|
||||||
const view = getRenderer(channel);
|
const view = getRenderer(channel);
|
||||||
if (view.state !== "RENDER") return null;
|
if (view.state !== "RENDER") return null;
|
||||||
|
|
||||||
@@ -171,8 +170,9 @@ export const MessageReply = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReplyBase head={index === 0}>
|
<ReplyBase head={index === 0}>
|
||||||
<Reply size={16} />
|
{/*<Reply size={16} />*/}
|
||||||
{message.author?.relationship === RelationshipStatus.Blocked ? (
|
|
||||||
|
{message.author?.relationship === "Blocked" ? (
|
||||||
<Text id="app.main.channel.misc.blocked_user" />
|
<Text id="app.main.channel.misc.blocked_user" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -182,7 +182,7 @@ export const MessageReply = observer(
|
|||||||
<>
|
<>
|
||||||
<div className="user">
|
<div className="user">
|
||||||
<UserShort
|
<UserShort
|
||||||
size={16}
|
size={14}
|
||||||
showServerIdentity
|
showServerIdentity
|
||||||
user={message.author}
|
user={message.author}
|
||||||
masquerade={message.masquerade!}
|
masquerade={message.masquerade!}
|
||||||
@@ -221,12 +221,15 @@ export const MessageReply = observer(
|
|||||||
</em>
|
</em>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Markdown
|
{message.content && (
|
||||||
disallowBigEmoji
|
<Markdown
|
||||||
content={(
|
disallowBigEmoji
|
||||||
message.content as string
|
content={message.content.replace(
|
||||||
).replace(/\n/g, " ")}
|
/\n/g,
|
||||||
/>
|
" ",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
239
src/components/common/messaging/attachments/Reactions.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import {
|
||||||
|
autoPlacement,
|
||||||
|
offset,
|
||||||
|
shift,
|
||||||
|
useFloating,
|
||||||
|
} from "@floating-ui/react-dom-interactions";
|
||||||
|
import { Plus } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Message } from "revolt.js";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
import { createPortal } from "preact/compat";
|
||||||
|
import { useCallback, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { emojiDictionary } from "../../../../assets/emojis";
|
||||||
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { RenderEmoji } from "../../../markdown/plugins/emoji";
|
||||||
|
import { HackAlertThisFileWillBeReplaced } from "../MessageBox";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reaction list element
|
||||||
|
*/
|
||||||
|
const List = styled.div`
|
||||||
|
gap: 0.4em;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.add {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .add {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List divider
|
||||||
|
*/
|
||||||
|
const Divider = styled.div`
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--tertiary-foreground);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reaction styling
|
||||||
|
*/
|
||||||
|
const Reaction = styled.div<{ active: boolean }>`
|
||||||
|
padding: 0.4em;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: var(--secondary-background);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
filter: brightness(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.active &&
|
||||||
|
css`
|
||||||
|
border-color: var(--accent);
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render reactions on a message
|
||||||
|
*/
|
||||||
|
export const Reactions = observer(({ message }: Props) => {
|
||||||
|
const client = useClient();
|
||||||
|
const [showPicker, setPicker] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render individual reaction entries
|
||||||
|
*/
|
||||||
|
const Entry = useCallback(
|
||||||
|
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
|
||||||
|
const active = user_ids?.has(client.user!._id) || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reaction
|
||||||
|
active={active}
|
||||||
|
onClick={() =>
|
||||||
|
active ? message.unreact(id) : message.react(id)
|
||||||
|
}>
|
||||||
|
<RenderEmoji match={id} /> {user_ids?.size || 0}
|
||||||
|
</Reaction>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine two lists of 'required' and 'optional' reactions
|
||||||
|
*/
|
||||||
|
const { required, optional } = (() => {
|
||||||
|
const required = new Set<string>();
|
||||||
|
const optional = new Set<string>();
|
||||||
|
|
||||||
|
if (message.interactions?.reactions) {
|
||||||
|
for (const reaction of message.interactions.reactions) {
|
||||||
|
required.add(reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of message.reactions.keys()) {
|
||||||
|
if (!required.has(key)) {
|
||||||
|
optional.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
required,
|
||||||
|
optional,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Don't render list if nothing is going to show anyways
|
||||||
|
if (required.size === 0 && optional.size === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{Array.from(required, (id) => (
|
||||||
|
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
|
||||||
|
))}
|
||||||
|
{required.size !== 0 && optional.size !== 0 && <Divider />}
|
||||||
|
{Array.from(optional, (id) => (
|
||||||
|
<Entry key={id} id={id} user_ids={message.reactions.get(id)} />
|
||||||
|
))}
|
||||||
|
{message.channel?.havePermission("React") && (
|
||||||
|
<ReactionWrapper
|
||||||
|
message={message}
|
||||||
|
open={showPicker}
|
||||||
|
setOpen={setPicker}>
|
||||||
|
<IconButton className={showPicker ? "" : "add"}>
|
||||||
|
<Plus size={20} />
|
||||||
|
</IconButton>
|
||||||
|
</ReactionWrapper>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Base = styled.div`
|
||||||
|
> div {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! FIXME: rewrite
|
||||||
|
*/
|
||||||
|
export const ReactionWrapper: React.FC<{
|
||||||
|
message: Message;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (v: boolean) => void;
|
||||||
|
}> = ({ open, setOpen, message, children }) => {
|
||||||
|
const { x, y, reference, floating, strategy } = useFloating({
|
||||||
|
open,
|
||||||
|
middleware: [
|
||||||
|
offset(4),
|
||||||
|
shift({ mainAxis: true, crossAxis: true, padding: 4 }),
|
||||||
|
autoPlacement(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const skip = useRef();
|
||||||
|
const toggle = () => {
|
||||||
|
if (skip.current) {
|
||||||
|
skip.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(!open);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
skip.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={reference}
|
||||||
|
onClick={toggle}
|
||||||
|
style={{ width: "fit-content" }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{createPortal(
|
||||||
|
<div id="reaction">
|
||||||
|
{open && (
|
||||||
|
<Base
|
||||||
|
ref={floating}
|
||||||
|
style={{
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
}}>
|
||||||
|
<HackAlertThisFileWillBeReplaced
|
||||||
|
onSelect={(emoji) =>
|
||||||
|
message.react(
|
||||||
|
emojiDictionary[
|
||||||
|
emoji as keyof typeof emojiDictionary
|
||||||
|
] ?? emoji,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClose={toggle}
|
||||||
|
/>
|
||||||
|
</Base>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Attachment } from "revolt-api/types/Autumn";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { Text } from "preact-i18n";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import Preloader from "../../../ui/Preloader";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: Attachment;
|
attachment: API.File;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileCache: { [key: string]: string } = {};
|
const fileCache: { [key: string]: string } = {};
|
||||||
|
|
||||||
export default function TextFile({ attachment }: Props) {
|
export default function TextFile({ attachment }: Props) {
|
||||||
|
const [gated, setGated] = useState(attachment.size > 100_000);
|
||||||
const [content, setContent] = useState<undefined | string>(undefined);
|
const [content, setContent] = useState<undefined | string>(undefined);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const status = useContext(StatusContext);
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
|
|
||||||
|
const client = useClient();
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof content !== "undefined") return;
|
if (typeof content !== "undefined") return;
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
if (gated) return;
|
||||||
if (attachment.size > 100_000) {
|
|
||||||
setContent(
|
|
||||||
"This file is > 100 KB, for your sake I did not load it.\nSee tracking issue here for previews: https://github.com/revoltchat/revite/issues/35",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -60,13 +52,17 @@ export default function TextFile({ attachment }: Props) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [content, loading, status, attachment._id, attachment.size, url]);
|
}, [content, loading, gated, attachment._id, attachment.size, url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.textContent}
|
className={styles.textContent}
|
||||||
data-loading={typeof content === "undefined"}>
|
data-loading={typeof content === "undefined"}>
|
||||||
{content ? (
|
{gated ? (
|
||||||
|
<Button palette="accent" onClick={() => setGated(false)}>
|
||||||
|
<Text id="app.main.channel.misc.load_file" />
|
||||||
|
</Button>
|
||||||
|
) : content ? (
|
||||||
<pre>
|
<pre>
|
||||||
<code>{content}</code>
|
<code>{content}</code>
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
|
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
@@ -149,12 +149,12 @@ function FileEntry({
|
|||||||
<EmptyEntry className="icon">
|
<EmptyEntry className="icon">
|
||||||
<File size={36} />
|
<File size={36} />
|
||||||
</EmptyEntry>
|
</EmptyEntry>
|
||||||
<div class="overlay">
|
<div className="overlay">
|
||||||
<XCircle size={36} />
|
<XCircle size={36} />
|
||||||
</div>
|
</div>
|
||||||
</PreviewBox>
|
</PreviewBox>
|
||||||
<span class="fn">{file.name}</span>
|
<span className="fn">{file.name}</span>
|
||||||
<span class="size">{determineFileSize(file.size)}</span>
|
<span className="size">{determineFileSize(file.size)}</span>
|
||||||
</Entry>
|
</Entry>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,13 +169,18 @@ function FileEntry({
|
|||||||
return (
|
return (
|
||||||
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
|
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
|
||||||
<PreviewBox onClick={remove}>
|
<PreviewBox onClick={remove}>
|
||||||
<img class="icon" src={url} alt={file.name} loading="eager" />
|
<img
|
||||||
<div class="overlay">
|
className="icon"
|
||||||
|
src={url}
|
||||||
|
alt={file.name}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<div className="overlay">
|
||||||
<XCircle size={36} />
|
<XCircle size={36} />
|
||||||
</div>
|
</div>
|
||||||
</PreviewBox>
|
</PreviewBox>
|
||||||
<span class="fn">{file.name}</span>
|
<span className="fn">{file.name}</span>
|
||||||
<span class="size">{determineFileSize(file.size)}</span>
|
<span className="size">{determineFileSize(file.size)}</span>
|
||||||
</Entry>
|
</Entry>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,121 @@
|
|||||||
import { DownArrowAlt } from "@styled-icons/boxicons-regular";
|
import { DownArrowAlt } from "@styled-icons/boxicons-regular";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js";
|
||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { internalEmit } from "../../../../lib/eventEmitter";
|
||||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||||
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
const Bar = styled.div`
|
export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
|
||||||
z-index: 10;
|
z-index: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@keyframes bottomBounce {
|
||||||
|
0% {
|
||||||
|
transform: translateY(33px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes topBounce {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-33px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.position === "top" &&
|
||||||
|
css`
|
||||||
|
top: 0;
|
||||||
|
animation: topBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16)
|
||||||
|
forwards;
|
||||||
|
`}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.position === "bottom" &&
|
||||||
|
css`
|
||||||
|
top: -28px;
|
||||||
|
animation: bottomBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16)
|
||||||
|
forwards;
|
||||||
|
|
||||||
|
${() =>
|
||||||
|
isTouchscreenDevice &&
|
||||||
|
css`
|
||||||
|
top: -90px;
|
||||||
|
`}
|
||||||
|
`}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
top: -26px;
|
|
||||||
height: 28px;
|
height: 28px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: var(--secondary-foreground);
|
|
||||||
transition: color ease-in-out 0.08s;
|
transition: color ease-in-out 0.08s;
|
||||||
background: var(--secondary-background);
|
|
||||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.accent
|
||||||
|
? css`
|
||||||
|
color: var(--accent-contrast);
|
||||||
|
background-color: rgba(
|
||||||
|
var(--accent-rgb),
|
||||||
|
max(var(--min-opacity), 0.9)
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
background-color: rgba(
|
||||||
|
var(--secondary-background-rgb),
|
||||||
|
max(var(--min-opacity), 0.9)
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
`}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.position === "top"
|
||||||
|
? css`
|
||||||
|
top: 48px;
|
||||||
|
border-radius: 0 0 var(--border-radius)
|
||||||
|
var(--border-radius);
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
border-radius: var(--border-radius) var(--border-radius) 0
|
||||||
|
0;
|
||||||
|
`}
|
||||||
|
|
||||||
|
${() =>
|
||||||
|
isTouchscreenDevice &&
|
||||||
|
css`
|
||||||
|
top: 56px;
|
||||||
|
`}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -47,10 +130,15 @@ const Bar = styled.div`
|
|||||||
isTouchscreenDevice &&
|
isTouchscreenDevice &&
|
||||||
css`
|
css`
|
||||||
height: 34px;
|
height: 34px;
|
||||||
top: -32px;
|
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
`}
|
`}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.right > span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(({ channel }: { channel: Channel }) => {
|
export default observer(({ channel }: { channel: Channel }) => {
|
||||||
@@ -58,14 +146,20 @@ export default observer(({ channel }: { channel: Channel }) => {
|
|||||||
if (renderer.state !== "RENDER" || renderer.atBottom) return null;
|
if (renderer.state !== "RENDER" || renderer.atBottom) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bar>
|
<Bar position="bottom">
|
||||||
<div onClick={() => renderer.jumpToBottom(true)}>
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
renderer.jumpToBottom(true);
|
||||||
|
internalEmit("NewMessages", "hide");
|
||||||
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<Text id="app.main.channel.misc.viewing_old" />
|
<Text id="app.main.channel.misc.viewing_old" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="right">
|
||||||
<Text id="app.main.channel.misc.jump_present" />{" "}
|
<span>
|
||||||
<DownArrowAlt size={20} />
|
<Text id="app.main.channel.misc.jump_present" />
|
||||||
|
</span>
|
||||||
|
<DownArrowAlt size={18} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Bar>
|
</Bar>
|
||||||
|
|||||||
236
src/components/common/messaging/bars/MessageOverlayBar.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { DotsVerticalRounded, LinkAlt } from "@styled-icons/boxicons-regular";
|
||||||
|
import {
|
||||||
|
Pencil,
|
||||||
|
Trash,
|
||||||
|
Share,
|
||||||
|
InfoSquare,
|
||||||
|
Notification,
|
||||||
|
HappyBeaming,
|
||||||
|
} from "@styled-icons/boxicons-solid";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Message as MessageObject } from "revolt.js";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { openContextMenu } from "preact-context-menu";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
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 { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OverlayBar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
justify-self: end;
|
||||||
|
align-self: end;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
right: 0;
|
||||||
|
top: -18px;
|
||||||
|
z-index: 0;
|
||||||
|
transition: box-shadow 0.1s ease-out;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--primary-header);
|
||||||
|
border: 1px solid var(--background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: rgb(0 0 0 / 20%) 0px 2px 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Entry = styled.div`
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease background-color;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--secondary-header);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 0 0 2.5pt var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
svg {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Divider = styled.div`
|
||||||
|
margin: 6px 4px;
|
||||||
|
width: 0.5px;
|
||||||
|
background: var(--tertiary-background);
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey);
|
||||||
|
|
||||||
|
document.addEventListener("keyup", handler);
|
||||||
|
document.addEventListener("keydown", handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keyup", handler);
|
||||||
|
document.removeEventListener("keydown", handler);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayBar>
|
||||||
|
{message.channel?.havePermission("SendMessage") && (
|
||||||
|
<Tooltip content="Reply">
|
||||||
|
<Entry
|
||||||
|
onClick={() =>
|
||||||
|
internalEmit("ReplyBar", "add", message)
|
||||||
|
}>
|
||||||
|
<Share size={18} />
|
||||||
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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={() =>
|
||||||
|
openContextMenu("Menu", {
|
||||||
|
message,
|
||||||
|
contextualChannel: message.channel_id,
|
||||||
|
queued,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<DotsVerticalRounded size={18} />
|
||||||
|
</Entry>
|
||||||
|
</Tooltip>
|
||||||
|
{extraActions && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Tooltip content="Mark as Unread">
|
||||||
|
<Entry
|
||||||
|
onClick={() => {
|
||||||
|
// ! FIXME: deduplicate this code with ctx menu
|
||||||
|
const messages = getRenderer(
|
||||||
|
message.channel!,
|
||||||
|
).messages;
|
||||||
|
const index = messages.findIndex(
|
||||||
|
(x) => x._id === message._id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let unread_id = message._id;
|
||||||
|
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");
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
78
src/components/common/messaging/bars/NewMessages.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { UpArrowAlt } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { Channel } from "revolt.js";
|
||||||
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||||
|
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
|
import { dayjs } from "../../../../context/Locale";
|
||||||
|
|
||||||
|
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]);
|
||||||
|
useEffect(() => internalSubscribe("NewMessages", "hide", hide), []);
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) =>
|
||||||
|
e.key === "Escape" && hide();
|
||||||
|
|
||||||
|
document.addEventListener("keydown", onKeyDown);
|
||||||
|
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;
|
||||||
|
if (!last_id) return null;
|
||||||
|
if (hidden) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Bar position="top" accent>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setHidden(true);
|
||||||
|
if (channel.channel_type === "TextChannel") {
|
||||||
|
history.push(
|
||||||
|
`/server/${channel.server_id}/channel/${channel._id}/${last_id}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
history.push(`/channel/${channel._id}/${last_id}`);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
id="app.main.channel.misc.new_messages"
|
||||||
|
fields={{
|
||||||
|
time_ago: timeAgo,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="right">
|
||||||
|
<span>
|
||||||
|
<Text id="app.main.channel.misc.jump_beginning" />
|
||||||
|
</span>
|
||||||
|
<UpArrowAlt size={20} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Bar>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import { At, Reply as ReplyIcon } from "@styled-icons/boxicons-regular";
|
import { At } from "@styled-icons/boxicons-regular";
|
||||||
import { File, XCircle } from "@styled-icons/boxicons-solid";
|
import { File, XCircle } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel, Message } from "revolt.js";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import styled from "styled-components/macro";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { StateUpdater, useEffect } from "preact/hooks";
|
import { StateUpdater, useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||||
|
|
||||||
import { dispatch, getState } from "../../../../redux";
|
import { useApplicationState } from "../../../../mobx/State";
|
||||||
import { Reply } from "../../../../redux/reducers/queue";
|
import { SECTION_MENTION } from "../../../../mobx/stores/Layout";
|
||||||
|
import { Reply } from "../../../../mobx/stores/MessageQueue";
|
||||||
import IconButton from "../../../ui/IconButton";
|
|
||||||
|
|
||||||
|
import Tooltip from "../../../common/Tooltip";
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import UserShort from "../../user/UserShort";
|
import UserShort from "../../user/UserShort";
|
||||||
import { SystemMessage } from "../SystemMessage";
|
import { SystemMessage } from "../SystemMessage";
|
||||||
@@ -27,12 +28,22 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
|
@keyframes bottomBounce {
|
||||||
|
0% {
|
||||||
|
transform: translateY(33px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0 12px;
|
padding: 0 20px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--message-box);
|
background: var(--secondary-background);
|
||||||
|
animation: bottomBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16) forwards;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -48,21 +59,34 @@ const Base = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
min-width: 6ch;
|
min-width: 6ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.replyto {
|
||||||
display: flex;
|
align-self: center;
|
||||||
align-items: center;
|
font-weight: 500;
|
||||||
gap: 6px;
|
flex-shrink: 0;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: 26px;
|
pointer-events: none;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
max-height: 26px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@@ -81,6 +105,7 @@ const Base = styled.div`
|
|||||||
const MAX_REPLIES = 5;
|
const MAX_REPLIES = 5;
|
||||||
export default observer(({ channel, replies, setReplies }: Props) => {
|
export default observer(({ channel, replies, setReplies }: Props) => {
|
||||||
const client = channel.client;
|
const client = channel.client;
|
||||||
|
const layout = useApplicationState().layout;
|
||||||
|
|
||||||
// Event listener for adding new messages to reply bar.
|
// Event listener for adding new messages to reply bar.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,7 +124,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
mention:
|
mention:
|
||||||
message.author_id === client.user!._id
|
message.author_id === client.user!._id
|
||||||
? false
|
? false
|
||||||
: getState().sectionToggle.mention ?? false,
|
: layout.getSectionState(SECTION_MENTION, false),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -127,42 +152,53 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Base key={reply.id}>
|
<Base key={reply.id}>
|
||||||
<ReplyBase preview>
|
<ReplyBase preview>
|
||||||
<ReplyIcon size={22} />
|
<div className="replyto">
|
||||||
<div class="username">
|
<Text id="app.main.channel.reply.replying" />
|
||||||
<UserShort
|
|
||||||
size={16}
|
|
||||||
showServerIdentity
|
|
||||||
user={message.author}
|
|
||||||
masquerade={message.masquerade!}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="message">
|
<div className="content">
|
||||||
{message.attachments && (
|
<div className="username">
|
||||||
<>
|
<UserShort
|
||||||
<File size={16} />
|
size={16}
|
||||||
<em>
|
showServerIdentity
|
||||||
{message.attachments.length > 1 ? (
|
user={message.author}
|
||||||
<Text id="app.main.channel.misc.sent_multiple_files" />
|
masquerade={message.masquerade!}
|
||||||
) : (
|
|
||||||
<Text id="app.main.channel.misc.sent_file" />
|
|
||||||
)}
|
|
||||||
</em>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{message.author_id ===
|
|
||||||
"00000000000000000000000000" ? (
|
|
||||||
<SystemMessage message={message} hideInfo />
|
|
||||||
) : (
|
|
||||||
<Markdown
|
|
||||||
disallowBigEmoji
|
|
||||||
content={(
|
|
||||||
message.content as string
|
|
||||||
).replace(/\n/g, " ")}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
<div className="message">
|
||||||
|
{message.attachments && (
|
||||||
|
<>
|
||||||
|
<File size={16} />
|
||||||
|
<em>
|
||||||
|
{message.attachments.length >
|
||||||
|
1 ? (
|
||||||
|
<Text id="app.main.channel.misc.sent_multiple_files" />
|
||||||
|
) : (
|
||||||
|
<Text id="app.main.channel.misc.sent_file" />
|
||||||
|
)}
|
||||||
|
</em>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{message.author_id ===
|
||||||
|
"00000000000000000000000000" ? (
|
||||||
|
<SystemMessage
|
||||||
|
message={message}
|
||||||
|
hideInfo
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
message.content && (
|
||||||
|
<Markdown
|
||||||
|
disallowBigEmoji
|
||||||
|
content={message.content.replace(
|
||||||
|
/\n/g,
|
||||||
|
" ",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ReplyBase>
|
</ReplyBase>
|
||||||
<span class="actions">
|
<span className="actions">
|
||||||
{message.author_id !== client.user!._id && (
|
{message.author_id !== client.user!._id && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -181,22 +217,27 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch({
|
layout.setSectionState(
|
||||||
type: "SECTION_TOGGLE_SET",
|
SECTION_MENTION,
|
||||||
id: "mention",
|
|
||||||
state,
|
state,
|
||||||
});
|
false,
|
||||||
|
);
|
||||||
}}>
|
}}>
|
||||||
<span class="toggle">
|
<Tooltip
|
||||||
<At size={15} />
|
content={
|
||||||
<Text
|
<Text id="app.main.channel.reply.toggle" />
|
||||||
id={
|
}>
|
||||||
reply.mention
|
<span className="toggle">
|
||||||
? "general.on"
|
<At size={15} />
|
||||||
: "general.off"
|
<Text
|
||||||
}
|
id={
|
||||||
/>
|
reply.mention
|
||||||
</span>
|
? "general.on"
|
||||||
|
: "general.off"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
import { Channel } from "revolt.js";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import styled from "styled-components/macro";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
@@ -13,10 +12,10 @@ const Base = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
height: 24px;
|
height: 26px;
|
||||||
margin-top: -24px;
|
top: -26px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
font-size: 13px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
@@ -25,7 +24,11 @@ const Base = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: calc(100% - var(--scrollbar-thickness));
|
width: calc(100% - var(--scrollbar-thickness));
|
||||||
color: var(--secondary-foreground);
|
color: var(--secondary-foreground);
|
||||||
background: var(--secondary-background);
|
background-color: rgba(
|
||||||
|
var(--secondary-background-rgb),
|
||||||
|
max(var(--min-opacity), 0.75)
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatars {
|
.avatars {
|
||||||
@@ -36,9 +39,12 @@ const Base = styled.div`
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: var(--border-radius-half);
|
border-radius: var(--border-radius-half);
|
||||||
|
background: var(--secondary-background);
|
||||||
|
//background-clip: border-box;
|
||||||
|
border: 2px solid var(--secondary-background);
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
margin-left: -4px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +55,7 @@ const Base = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
//font-weight: 600;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -57,7 +64,7 @@ export default observer(({ channel }: Props) => {
|
|||||||
(x) =>
|
(x) =>
|
||||||
typeof x !== "undefined" &&
|
typeof x !== "undefined" &&
|
||||||
x._id !== x.client.user!._id &&
|
x._id !== x.client.user!._id &&
|
||||||
x.relationship !== RelationshipStatus.Blocked,
|
x.relationship !== "Blocked",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
@@ -69,7 +76,9 @@ export default observer(({ channel }: Props) => {
|
|||||||
if (users.length >= 5) {
|
if (users.length >= 5) {
|
||||||
text = <Text id="app.main.channel.typing.several" />;
|
text = <Text id="app.main.channel.typing.several" />;
|
||||||
} else if (users.length > 1) {
|
} 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();
|
const user = userlist.pop();
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -85,7 +94,9 @@ export default observer(({ channel }: Props) => {
|
|||||||
text = (
|
text = (
|
||||||
<Text
|
<Text
|
||||||
id="app.main.channel.typing.single"
|
id="app.main.channel.typing.single"
|
||||||
fields={{ user: users[0]!.username }}
|
fields={{
|
||||||
|
user: users[0]!.display_name ?? users[0]!.username,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
iframe {
|
iframe {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.image {
|
&.image {
|
||||||
@@ -12,7 +13,6 @@
|
|||||||
|
|
||||||
&.website {
|
&.website {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
> div:nth-child(1) {
|
> div:nth-child(1) {
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { Embed as EmbedI } from "revolt-api/types/January";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
||||||
|
import Markdown from "../../../markdown/Markdown";
|
||||||
|
import Attachment from "../attachments/Attachment";
|
||||||
import EmbedMedia from "./EmbedMedia";
|
import EmbedMedia from "./EmbedMedia";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: EmbedI;
|
embed: API.Embed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_EMBED_WIDTH = 480;
|
const MAX_EMBED_WIDTH = 480;
|
||||||
@@ -22,7 +23,6 @@ const MAX_PREVIEW_SIZE = 150;
|
|||||||
export default function Embed({ embed }: Props) {
|
export default function Embed({ embed }: Props) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const { openScreen, openLink } = useIntermediate();
|
|
||||||
const maxWidth = Math.min(
|
const maxWidth = Math.min(
|
||||||
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
||||||
MAX_EMBED_WIDTH,
|
MAX_EMBED_WIDTH,
|
||||||
@@ -45,39 +45,64 @@ export default function Embed({ embed }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (embed.type) {
|
switch (embed.type) {
|
||||||
|
case "Text":
|
||||||
case "Website": {
|
case "Website": {
|
||||||
// Determine special embed size.
|
// Determine special embed size.
|
||||||
let mw, mh;
|
let mw, mh;
|
||||||
const largeMedia =
|
const largeMedia =
|
||||||
(embed.special && embed.special.type !== "None") ||
|
embed.type === "Text"
|
||||||
embed.image?.size === "Large";
|
? typeof embed.media !== "undefined"
|
||||||
switch (embed.special?.type) {
|
: (embed.special && embed.special.type !== "None") ||
|
||||||
case "YouTube":
|
embed.image?.size === "Large";
|
||||||
case "Bandcamp": {
|
|
||||||
mw = embed.video?.width ?? 1280;
|
if (embed.type === "Text") {
|
||||||
mh = embed.video?.height ?? 720;
|
mw = MAX_EMBED_WIDTH;
|
||||||
break;
|
mh = 1;
|
||||||
}
|
} else {
|
||||||
case "Twitch": {
|
switch (embed.special?.type) {
|
||||||
mw = 1280;
|
case "YouTube":
|
||||||
mh = 720;
|
case "Bandcamp": {
|
||||||
break;
|
mw = embed.video?.width ?? 1280;
|
||||||
}
|
mh = embed.video?.height ?? 720;
|
||||||
default: {
|
break;
|
||||||
if (embed.image?.size === "Preview") {
|
}
|
||||||
mw = MAX_EMBED_WIDTH;
|
case "Twitch":
|
||||||
mh = Math.min(
|
case "Lightspeed":
|
||||||
embed.image.height ?? 0,
|
case "Streamable": {
|
||||||
MAX_PREVIEW_SIZE,
|
mw = 1280;
|
||||||
);
|
mh = 720;
|
||||||
} else {
|
break;
|
||||||
mw = embed.image?.width ?? MAX_EMBED_WIDTH;
|
}
|
||||||
mh = embed.image?.height ?? 0;
|
default: {
|
||||||
|
if (embed.image?.size === "Preview") {
|
||||||
|
mw = MAX_EMBED_WIDTH;
|
||||||
|
mh = Math.min(
|
||||||
|
embed.image.height ?? 0,
|
||||||
|
MAX_PREVIEW_SIZE,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mw = embed.image?.width ?? MAX_EMBED_WIDTH;
|
||||||
|
mh = embed.image?.height ?? 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { width, height } = calculateSize(mw, mh);
|
const { width, height } = calculateSize(mw, mh);
|
||||||
|
if (embed.type === "Website" && embed.special?.type === "GIF") {
|
||||||
|
return (
|
||||||
|
<EmbedMedia
|
||||||
|
embed={embed}
|
||||||
|
width={
|
||||||
|
height *
|
||||||
|
((embed.image?.width ?? 0) /
|
||||||
|
(embed.image?.height ?? 0))
|
||||||
|
}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.embed, styles.website)}
|
className={classNames(styles.embed, styles.website)}
|
||||||
@@ -87,7 +112,9 @@ export default function Embed({ embed }: Props) {
|
|||||||
width: width + CONTAINER_PADDING,
|
width: width + CONTAINER_PADDING,
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
{embed.site_name && (
|
{(embed.type === "Text"
|
||||||
|
? embed.title
|
||||||
|
: embed.site_name) && (
|
||||||
<div className={styles.siteinfo}>
|
<div className={styles.siteinfo}>
|
||||||
{embed.icon_url && (
|
{embed.icon_url && (
|
||||||
<img
|
<img
|
||||||
@@ -102,35 +129,47 @@ export default function Embed({ embed }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={styles.site}>
|
<div className={styles.site}>
|
||||||
{embed.site_name}{" "}
|
{embed.type === "Text"
|
||||||
|
? embed.title
|
||||||
|
: embed.site_name}{" "}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
|
{/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
|
||||||
{embed.title && (
|
{embed.type === "Website" && embed.title && (
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
onMouseDown={(ev) =>
|
onMouseDown={(ev) =>
|
||||||
(ev.button === 0 || ev.button === 1) &&
|
(ev.button === 0 || ev.button === 1) &&
|
||||||
openLink(embed.url)
|
modalController.openLink(
|
||||||
|
embed.url!,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className={styles.title}>
|
className={styles.title}>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{embed.description && (
|
{embed.description &&
|
||||||
<div className={styles.description}>
|
(embed.type === "Text" ? (
|
||||||
{embed.description}
|
<Markdown content={embed.description} />
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className={styles.description}>
|
||||||
|
{embed.description}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{largeMedia && (
|
{largeMedia &&
|
||||||
<EmbedMedia embed={embed} height={height} />
|
(embed.type === "Text" ? (
|
||||||
)}
|
<Attachment attachment={embed.media!} />
|
||||||
|
) : (
|
||||||
|
<EmbedMedia embed={embed} height={height} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!largeMedia && (
|
{!largeMedia && embed.type === "Website" && (
|
||||||
<div>
|
<div>
|
||||||
<EmbedMedia
|
<EmbedMedia
|
||||||
embed={embed}
|
embed={embed}
|
||||||
@@ -155,8 +194,25 @@ export default function Embed({ embed }: Props) {
|
|||||||
type="text/html"
|
type="text/html"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onClick={() => openScreen({ id: "image_viewer", embed })}
|
onClick={() =>
|
||||||
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
modalController.push({ type: "image_viewer", embed })
|
||||||
|
}
|
||||||
|
onMouseDown={(ev) =>
|
||||||
|
ev.button === 1 &&
|
||||||
|
modalController.openLink(embed.url, undefined, true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "Video": {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
className={classNames(styles.embed, styles.image)}
|
||||||
|
style={calculateSize(embed.width, embed.height)}
|
||||||
|
src={client.proxyFile(embed.url)}
|
||||||
|
frameBorder="0"
|
||||||
|
loading="lazy"
|
||||||
|
controls
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,23 @@
|
|||||||
import { autorun } from "mobx";
|
import { Group } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { RetrievedInvite } from "revolt-api/types/Invites";
|
import { Message, API } from "revolt.js";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import styled, { css } from "styled-components/macro";
|
||||||
import styled, { css } from "styled-components";
|
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { defer } from "../../../../lib/defer";
|
import { Button, Category, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { dispatch } from "../../../../redux";
|
import { I18nError } from "../../../../context/Locale";
|
||||||
|
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../../../context/revoltjs/util";
|
|
||||||
|
|
||||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||||
import Button from "../../../../components/ui/Button";
|
import {
|
||||||
import Overline from "../../../ui/Overline";
|
useClient,
|
||||||
import Preloader from "../../../ui/Preloader";
|
useSession,
|
||||||
|
} from "../../../../controllers/client/ClientController";
|
||||||
|
import { takeError } from "../../../../controllers/client/jsx/error";
|
||||||
|
|
||||||
const EmbedInviteBase = styled.div`
|
const EmbedInviteBase = styled.div`
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@@ -33,7 +28,7 @@ const EmbedInviteBase = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
${() =>
|
${() =>
|
||||||
isTouchscreenDevice &&
|
isTouchscreenDevice &&
|
||||||
css`
|
css`
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -44,19 +39,17 @@ const EmbedInviteBase = styled.div`
|
|||||||
> button {
|
> button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
`
|
`}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EmbedInviteDetails = styled.div`
|
const EmbedInviteDetails = styled.div`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-left: 12px;
|
padding-inline-start: 12px;
|
||||||
${() =>
|
${() =>
|
||||||
isTouchscreenDevice &&
|
isTouchscreenDevice &&
|
||||||
css`
|
css`
|
||||||
width: calc(100% - 55px);
|
width: calc(100% - 55px);
|
||||||
`
|
`}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EmbedInviteName = styled.div`
|
const EmbedInviteName = styled.div`
|
||||||
@@ -67,36 +60,44 @@ const EmbedInviteName = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const EmbedInviteMemberCount = styled.div`
|
const EmbedInviteMemberCount = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EmbedInvite(props: Props) {
|
export function EmbedInvite({ code }: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useContext(AppContext);
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
const code = props.code;
|
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
||||||
const [invite, setInvite] = useState<RetrievedInvite | undefined>(
|
const [invite, setInvite] = useState<
|
||||||
undefined,
|
(API.InviteResponse & { type: "Server" }) | undefined
|
||||||
);
|
>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
typeof invite === "undefined" &&
|
typeof invite === "undefined" &&
|
||||||
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
|
(session.state === "Online" || session.state === "Ready")
|
||||||
) {
|
) {
|
||||||
client
|
client
|
||||||
.fetchInvite(code)
|
.fetchInvite(code)
|
||||||
.then((data) => setInvite(data))
|
.then((data) =>
|
||||||
|
setInvite(data as API.InviteResponse & { type: "Server" }),
|
||||||
|
)
|
||||||
.catch((err) => setError(takeError(err)));
|
.catch((err) => setError(takeError(err)));
|
||||||
}
|
}
|
||||||
}, [client, code, invite, status]);
|
}, [client, code, invite, session.state]);
|
||||||
|
|
||||||
if (typeof invite === "undefined") {
|
if (typeof invite === "undefined") {
|
||||||
return error ? (
|
return error ? (
|
||||||
@@ -124,7 +125,9 @@ export function EmbedInvite(props: Props) {
|
|||||||
<EmbedInviteDetails>
|
<EmbedInviteDetails>
|
||||||
<EmbedInviteName>{invite.server_name}</EmbedInviteName>
|
<EmbedInviteName>{invite.server_name}</EmbedInviteName>
|
||||||
<EmbedInviteMemberCount>
|
<EmbedInviteMemberCount>
|
||||||
{invite.member_count.toLocaleString()} {invite.member_count === 1 ? "member" : "members"}
|
<Group size={12} />
|
||||||
|
{invite.member_count.toLocaleString()}{" "}
|
||||||
|
{invite.member_count === 1 ? "member" : "members"}
|
||||||
</EmbedInviteMemberCount>
|
</EmbedInviteMemberCount>
|
||||||
</EmbedInviteDetails>
|
</EmbedInviteDetails>
|
||||||
{processing ? (
|
{processing ? (
|
||||||
@@ -134,49 +137,31 @@ export function EmbedInvite(props: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
setProcessing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setProcessing(true);
|
await client.joinInvite(invite);
|
||||||
|
|
||||||
if (invite.type === "Server") {
|
history.push(
|
||||||
if (client.servers.get(invite.server_id)) {
|
`/server/${invite.server_id}/channel/${invite.channel_id}`,
|
||||||
history.push(
|
);
|
||||||
`/server/${invite.server_id}/channel/${invite.channel_id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dispose = autorun(() => {
|
|
||||||
const server = client.servers.get(
|
|
||||||
invite.server_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
defer(() => {
|
|
||||||
if (server) {
|
|
||||||
dispatch({
|
|
||||||
type: "UNREADS_MARK_MULTIPLE_READ",
|
|
||||||
channels: server.channel_ids,
|
|
||||||
});
|
|
||||||
|
|
||||||
history.push(
|
|
||||||
`/server/${server._id}/channel/${invite.channel_id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dispose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.joinInvite(code);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setJoinError(takeError(err));
|
setJoinError(takeError(err));
|
||||||
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{client.servers.get(invite.server_id) ? "Joined" : "Join"}
|
{client.servers.get(invite.server_id)
|
||||||
|
? "Joined"
|
||||||
|
: "Join"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</EmbedInviteBase>
|
</EmbedInviteBase>
|
||||||
{joinError && <Overline type="error" error={joinError} />}
|
{joinError && (
|
||||||
|
<Category>
|
||||||
|
<I18nError error={joinError} />
|
||||||
|
</Category>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -207,9 +192,12 @@ export default observer(({ message }: { message: Message }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{entries.map((entry) => (
|
{entries.map(
|
||||||
<EmbedInvite key={entry} code={entry} />
|
(entry) =>
|
||||||
))}
|
entry !== "discover" && (
|
||||||
|
<EmbedInvite key={entry} code={entry} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { Embed } from "revolt-api/types/January";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: Embed;
|
embed: API.Embed;
|
||||||
width?: number;
|
width?: number;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmbedMedia({ embed, width, height }: Props) {
|
export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
if (embed.type !== "Website") return null;
|
if (embed.type !== "Website") return null;
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
switch (embed.special?.type) {
|
switch (embed.special?.type) {
|
||||||
@@ -47,6 +46,17 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
|||||||
style={{ height }}
|
style={{ height }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "Lightspeed":
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={`https://new.lightspeed.tv/embed/${embed.special.id}/stream`}
|
||||||
|
frameBorder="0"
|
||||||
|
allowFullScreen
|
||||||
|
scrolling="no"
|
||||||
|
loading="lazy"
|
||||||
|
style={{ height }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "Spotify":
|
case "Spotify":
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
@@ -82,19 +92,43 @@ 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: {
|
default: {
|
||||||
if (embed.image) {
|
if (embed.video) {
|
||||||
|
const url = embed.video.url;
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
loading="lazy"
|
||||||
|
className={styles.image}
|
||||||
|
style={{ width, height }}
|
||||||
|
src={client.proxyFile(url)}
|
||||||
|
loop={embed.special?.type === "GIF"}
|
||||||
|
controls={embed.special?.type !== "GIF"}
|
||||||
|
autoPlay={embed.special?.type === "GIF"}
|
||||||
|
muted={embed.special?.type === "GIF" ? true : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (embed.image) {
|
||||||
const url = embed.image.url;
|
const url = embed.image.url;
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
src={client.proxyFile(url)}
|
src={client.proxyFile(url)}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width, height }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "image_viewer",
|
type: "image_viewer",
|
||||||
embed: embed.image,
|
embed: embed.image!,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onMouseDown={(ev) =>
|
onMouseDown={(ev) =>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { LinkExternal } from "@styled-icons/boxicons-regular";
|
import { LinkExternal } from "@styled-icons/boxicons-regular";
|
||||||
import { EmbedImage } from "revolt-api/types/January";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
||||||
import IconButton from "../../../ui/IconButton";
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: EmbedImage;
|
embed: API.Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmbedMediaActions({ embed }: Props) {
|
export default function EmbedMediaActions({ embed }: Props) {
|
||||||
@@ -20,7 +20,7 @@ export default function EmbedMediaActions({ embed }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href={embed.url}
|
href={embed.url}
|
||||||
class={styles.openIcon}
|
className={styles.openIcon}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
import { Shield } from "@styled-icons/boxicons-regular";
|
import { Shield } from "@styled-icons/boxicons-regular";
|
||||||
import { Badges } from "revolt-api/types/Users";
|
import styled from "styled-components/macro";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import { Localizer, Text } from "preact-i18n";
|
import { Localizer, Text } from "preact-i18n";
|
||||||
|
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
|
|
||||||
|
enum Badges {
|
||||||
|
Developer = 1,
|
||||||
|
Translator = 2,
|
||||||
|
Supporter = 4,
|
||||||
|
ResponsibleDisclosure = 8,
|
||||||
|
Founder = 16,
|
||||||
|
PlatformModeration = 32,
|
||||||
|
ActiveSupporter = 64,
|
||||||
|
Paw = 128,
|
||||||
|
EarlyAdopter = 256,
|
||||||
|
ReservedRelevantJokeBadge1 = 512,
|
||||||
|
ReservedRelevantJokeBadge2 = 1024,
|
||||||
|
}
|
||||||
|
|
||||||
const BadgesBase = styled.div`
|
const BadgesBase = styled.div`
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 4px;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 32px;
|
width: 24px;
|
||||||
height: 32px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -90,7 +102,7 @@ export default function UserBadges({ badges, uid }: Props) {
|
|||||||
content={
|
content={
|
||||||
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
|
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
|
||||||
}>
|
}>
|
||||||
<Shield size={32} color="gray" />
|
<Shield size={24} color="gray" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
@@ -123,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 ? (
|
{badges & Badges.Paw ? (
|
||||||
<Tooltip content="🦊">
|
<Tooltip content="🦊">
|
||||||
<img src="/assets/badges/paw.svg" />
|
<img src="/assets/badges/paw.svg" />
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { User } from "revolt.js";
|
||||||
|
|
||||||
import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
|
import { Checkbox, Row, Column } from "@revoltchat/ui";
|
||||||
|
|
||||||
import UserIcon from "./UserIcon";
|
import UserIcon from "./UserIcon";
|
||||||
import { Username } from "./UserShort";
|
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) {
|
export default function UserCheckbox({ user, ...props }: UserProps) {
|
||||||
return (
|
return (
|
||||||
<Checkbox {...props}>
|
<Checkbox
|
||||||
<UserIcon target={user} size={32} />
|
{...props}
|
||||||
<Username user={user} />
|
title={
|
||||||
</Checkbox>
|
<Row centred>
|
||||||
|
<UserIcon target={user} size={32} />
|
||||||
|
<Column centred>
|
||||||
|
<Username user={user} />
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { Cog } from "@styled-icons/boxicons-solid";
|
import { Cog } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { User } from "revolt.js";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { openContextMenu } from "preact-context-menu";
|
import { openContextMenu } from "preact-context-menu";
|
||||||
import { Text, Localizer } from "preact-i18n";
|
import { Text, Localizer } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Header, IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import Header from "../../ui/Header";
|
|
||||||
import IconButton from "../../ui/IconButton";
|
|
||||||
|
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import UserStatus from "./UserStatus";
|
import UserStatus from "./UserStatus";
|
||||||
|
|
||||||
@@ -31,9 +29,14 @@ const HeaderBase = styled.div`
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,17 +52,22 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default observer(({ user }: Props) => {
|
export default observer(({ user }: Props) => {
|
||||||
const { writeClipboard } = useIntermediate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header borders placement="secondary">
|
<Header topBorder palette="secondary">
|
||||||
<HeaderBase>
|
<HeaderBase>
|
||||||
|
<div className="new-name">
|
||||||
|
{user.display_name ?? user.username}
|
||||||
|
</div>
|
||||||
<Localizer>
|
<Localizer>
|
||||||
<Tooltip content={<Text id="app.special.copy_username" />}>
|
<Tooltip content={<Text id="app.special.copy_username" />}>
|
||||||
<span
|
<span
|
||||||
className="username"
|
className="username"
|
||||||
onClick={() => writeClipboard(user.username)}>
|
onClick={() =>
|
||||||
@{user.username}
|
modalController.writeText(user.username)
|
||||||
|
}>
|
||||||
|
{user.username}
|
||||||
|
{"#"}
|
||||||
|
{user.discriminator}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Localizer>
|
</Localizer>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { User } from "revolt.js";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Children } from "../../../types/Preact";
|
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import { Username } from "./UserShort";
|
import { Username } from "./UserShort";
|
||||||
import UserStatus from "./UserStatus";
|
import UserStatus from "./UserStatus";
|
||||||
@@ -42,10 +41,7 @@ export default function UserHover({ user, children }: Props) {
|
|||||||
placement="right-end"
|
placement="right-end"
|
||||||
content={
|
content={
|
||||||
<Base>
|
<Base>
|
||||||
<Username
|
<Username className="username" user={user} />
|
||||||
className="username"
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
<span className="status">
|
<span className="status">
|
||||||
<UserStatus user={user} />
|
<UserStatus user={user} />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
import { MicrophoneOff } from "@styled-icons/boxicons-regular";
|
import { VolumeMute, MicrophoneOff } from "@styled-icons/boxicons-solid";
|
||||||
import { VolumeMute } from "@styled-icons/boxicons-solid";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Masquerade } from "revolt-api/types/Channels";
|
import { User, API } from "revolt.js";
|
||||||
import { Presence } from "revolt-api/types/Users";
|
import styled, { css } from "styled-components/macro";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
|
||||||
import { Nullable } from "revolt.js/dist/util/null";
|
|
||||||
import styled, { css } from "styled-components";
|
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { ThemeContext } from "../../../context/Theme";
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import fallback from "../assets/user.png";
|
import fallback from "../assets/user.png";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import IconBase, { IconBaseProps } from "../IconBase";
|
import IconBase, { IconBaseProps } from "../IconBase";
|
||||||
|
|
||||||
type VoiceStatus = "muted" | "deaf";
|
type VoiceStatus = "muted" | "deaf";
|
||||||
interface Props extends IconBaseProps<User> {
|
interface Props extends IconBaseProps<User> {
|
||||||
status?: boolean;
|
status?: boolean;
|
||||||
|
override?: string;
|
||||||
voice?: VoiceStatus;
|
voice?: VoiceStatus;
|
||||||
masquerade?: Masquerade;
|
masquerade?: API.Masquerade;
|
||||||
showServerIdentity?: boolean;
|
showServerIdentity?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStatusColour(user?: User) {
|
export function useStatusColour(user?: User) {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useApplicationState().settings.theme;
|
||||||
|
|
||||||
return user?.online && user?.status?.presence !== Presence.Invisible
|
return user?.online && user?.status?.presence !== "Invisible"
|
||||||
? user?.status?.presence === Presence.Idle
|
? user?.status?.presence === "Idle"
|
||||||
? theme["status-away"]
|
? theme.getVariable("status-away")
|
||||||
: user?.status?.presence === Presence.Busy
|
: user?.status?.presence === "Focus"
|
||||||
? theme["status-busy"]
|
? theme.getVariable("status-focus")
|
||||||
: theme["status-online"]
|
: user?.status?.presence === "Busy"
|
||||||
: theme["status-invisible"];
|
? theme.getVariable("status-busy")
|
||||||
|
: theme.getVariable("status-online")
|
||||||
|
: theme.getVariable("status-invisible");
|
||||||
}
|
}
|
||||||
|
|
||||||
const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
||||||
@@ -46,10 +43,6 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
svg {
|
|
||||||
stroke: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
(props.status === "muted" || props.status === "deaf") &&
|
(props.status === "muted" || props.status === "deaf") &&
|
||||||
css`
|
css`
|
||||||
@@ -77,12 +70,16 @@ export default observer(
|
|||||||
hover,
|
hover,
|
||||||
showServerIdentity,
|
showServerIdentity,
|
||||||
masquerade,
|
masquerade,
|
||||||
|
innerRef,
|
||||||
|
override,
|
||||||
...svgProps
|
...svgProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let { url } = props;
|
let { url } = props;
|
||||||
if (masquerade?.avatar) {
|
if (masquerade?.avatar) {
|
||||||
url = masquerade.avatar;
|
url = client.proxyFile(masquerade.avatar);
|
||||||
|
} else if (override) {
|
||||||
|
url = override;
|
||||||
} else if (!url) {
|
} else if (!url) {
|
||||||
let override;
|
let override;
|
||||||
if (target && showServerIdentity) {
|
if (target && showServerIdentity) {
|
||||||
@@ -101,7 +98,7 @@ export default observer(
|
|||||||
|
|
||||||
url =
|
url =
|
||||||
client.generateFileURL(
|
client.generateFileURL(
|
||||||
override ?? target?.avatar ?? attachment,
|
override ?? target?.avatar ?? attachment ?? undefined,
|
||||||
{ max_side: 256 },
|
{ max_side: 256 },
|
||||||
animate,
|
animate,
|
||||||
) ?? (target ? target.defaultAvatarURL : fallback);
|
) ?? (target ? target.defaultAvatarURL : fallback);
|
||||||
@@ -110,6 +107,7 @@ export default observer(
|
|||||||
return (
|
return (
|
||||||
<IconBase
|
<IconBase
|
||||||
{...svgProps}
|
{...svgProps}
|
||||||
|
ref={innerRef}
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
hover={hover}
|
hover={hover}
|
||||||
@@ -121,7 +119,7 @@ export default observer(
|
|||||||
y="0"
|
y="0"
|
||||||
width="32"
|
width="32"
|
||||||
height="32"
|
height="32"
|
||||||
class="icon"
|
className="icon"
|
||||||
mask={mask ?? (status ? "url(#user)" : undefined)}>
|
mask={mask ?? (status ? "url(#user)" : undefined)}>
|
||||||
{<img src={url} draggable={false} loading="lazy" />}
|
{<img src={url} draggable={false} loading="lazy" />}
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
|
import { TimeFive } from "@styled-icons/boxicons-regular";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Masquerade } from "revolt-api/types/Channels";
|
import { User, API } from "revolt.js";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
import styled, { css } from "styled-components/macro";
|
||||||
import { Nullable } from "revolt.js/dist/util/null";
|
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
|
import { Ref } from "preact";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { dayjs } from "../../../context/Locale";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
import Tooltip from "../Tooltip";
|
||||||
import UserIcon from "./UserIcon";
|
import UserIcon from "./UserIcon";
|
||||||
|
|
||||||
const BotBadge = styled.div`
|
const BotBadge = styled.div`
|
||||||
@@ -21,33 +23,60 @@ const BotBadge = styled.div`
|
|||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
font-size: 0.6em;
|
font-size: 0.6em;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin-inline-start: 2px;
|
margin-inline-start: 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
color: var(--accent-contrast);
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-radius: calc(var(--border-radius) / 2);
|
border-radius: calc(var(--border-radius) / 2);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
|
type UsernameProps = Omit<
|
||||||
|
JSX.HTMLAttributes<HTMLElement>,
|
||||||
|
"children" | "as"
|
||||||
|
> & {
|
||||||
user?: User;
|
user?: User;
|
||||||
prefixAt?: boolean;
|
prefixAt?: boolean;
|
||||||
masquerade?: Masquerade;
|
masquerade?: API.Masquerade;
|
||||||
showServerIdentity?: boolean | "both";
|
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(
|
export const Username = observer(
|
||||||
({
|
({
|
||||||
user,
|
user,
|
||||||
prefixAt,
|
prefixAt,
|
||||||
masquerade,
|
masquerade,
|
||||||
showServerIdentity,
|
showServerIdentity,
|
||||||
|
innerRef,
|
||||||
|
override,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: UsernameProps) => {
|
}: UsernameProps) => {
|
||||||
let username = user?.username;
|
let username =
|
||||||
let color;
|
(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 }>();
|
const { server } = useParams<{ server?: string }>();
|
||||||
if (server) {
|
if (server) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
@@ -65,15 +94,14 @@ export const Username = observer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.roles && member.roles.length > 0) {
|
if (member.timeout) {
|
||||||
const srv = client.servers.get(member._id.server);
|
timed_out = member.timeout;
|
||||||
if (srv?.roles) {
|
}
|
||||||
for (const role of member.roles) {
|
|
||||||
const c = srv.roles[role].colour;
|
if (!color) {
|
||||||
if (c) {
|
for (const [_, { colour }] of member.orderedRoles) {
|
||||||
color = c;
|
if (colour) {
|
||||||
continue;
|
color = colour;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,14 +109,53 @@ export const Username = observer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const el = (
|
||||||
|
<>
|
||||||
|
<Name {...otherProps} ref={innerRef} colour={color}>
|
||||||
|
{prefixAt ? "@" : undefined}
|
||||||
|
{masquerade?.name ?? username ?? (
|
||||||
|
<Text id="app.main.channel.unknown_user" />
|
||||||
|
)}
|
||||||
|
</Name>
|
||||||
|
|
||||||
|
{timed_out && (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Text
|
||||||
|
id="app.main.channel.user_timed_out"
|
||||||
|
fields={{
|
||||||
|
time: dayjs(timed_out).fromNow(true),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<TimeFive
|
||||||
|
size={16}
|
||||||
|
color="var(--secondary-foreground)"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
if (user?.bot) {
|
if (user?.bot) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span {...otherProps} style={{ color }}>
|
{el}
|
||||||
{masquerade?.name ?? username ?? (
|
<BotBadge>
|
||||||
<Text id="app.main.channel.unknown_user" />
|
{masquerade ? (
|
||||||
|
<Text id="app.main.channel.bridge" />
|
||||||
|
) : (
|
||||||
|
<Text id="app.main.channel.bot" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</BotBadge>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (override) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{el}
|
||||||
<BotBadge>
|
<BotBadge>
|
||||||
<Text id="app.main.channel.bot" />
|
<Text id="app.main.channel.bot" />
|
||||||
</BotBadge>
|
</BotBadge>
|
||||||
@@ -96,14 +163,7 @@ export const Username = observer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return el;
|
||||||
<span {...otherProps} style={{ color }}>
|
|
||||||
{prefixAt ? "@" : undefined}
|
|
||||||
{masquerade?.name ?? username ?? (
|
|
||||||
<Text id="app.main.channel.unknown_user" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,12 +177,12 @@ export default function UserShort({
|
|||||||
user?: User;
|
user?: User;
|
||||||
size?: number;
|
size?: number;
|
||||||
prefixAt?: boolean;
|
prefixAt?: boolean;
|
||||||
masquerade?: Masquerade;
|
masquerade?: API.Masquerade;
|
||||||
showServerIdentity?: boolean;
|
showServerIdentity?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const openProfile = () =>
|
const openProfile = () =>
|
||||||
user && openScreen({ id: "profile", user_id: user._id });
|
user &&
|
||||||
|
modalController.push({ type: "user_profile", user_id: user._id });
|
||||||
|
|
||||||
const handleUserClick = (e: MouseEvent) => {
|
const handleUserClick = (e: MouseEvent) => {
|
||||||
if (e.shiftKey && user?._id) {
|
if (e.shiftKey && user?._id) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Presence } from "revolt-api/types/Users";
|
import { User, API } from "revolt.js";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
@@ -25,15 +24,19 @@ export default observer(({ user, tooltip }: Props) => {
|
|||||||
return <>{user.status.text}</>;
|
return <>{user.status.text}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status?.presence === Presence.Busy) {
|
if (user.status?.presence === "Busy") {
|
||||||
return <Text id="app.status.busy" />;
|
return <Text id="app.status.busy" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status?.presence === Presence.Idle) {
|
if (user.status?.presence === "Idle") {
|
||||||
return <Text id="app.status.idle" />;
|
return <Text id="app.status.idle" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status?.presence === Presence.Invisible) {
|
if (user.status?.presence === "Focus") {
|
||||||
|
return <Text id="app.status.focus" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status?.presence === "Invisible") {
|
||||||
return <Text id="app.status.offline" />;
|
return <Text id="app.status.offline" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
.markdown {
|
|
||||||
:global(.emoji) {
|
|
||||||
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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
color: white;
|
|
||||||
font-size: 90%;
|
|
||||||
background: var(--block);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-family: var(--monospace-font), monospace;
|
|
||||||
border-radius: 3px;
|
|
||||||
-webkit-box-decoration-break: clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
margin-right: 4px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: 6px;
|
|
||||||
border: 1px solid var(--tertiary-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.katex-block) {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.spoiler) {
|
|
||||||
padding: 0 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
color: transparent;
|
|
||||||
background: #151515;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:global(.shown) {
|
|
||||||
cursor: auto;
|
|
||||||
user-select: all;
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--secondary-background);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.code) {
|
|
||||||
font-family: var(--monospace-font), monospace;
|
|
||||||
|
|
||||||
:global(.lang) {
|
|
||||||
width: fit-content;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
|
|
||||||
div {
|
|
||||||
color: #111;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--accent);
|
|
||||||
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
box-shadow: 0 2px #787676;
|
|
||||||
border-radius: calc(var(--border-radius) / 3);
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
box-shadow: 0 1px #787676;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
width: 0;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] + label:before {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
content: "a";
|
|
||||||
font-size: 10px;
|
|
||||||
margin-right: 6px;
|
|
||||||
line-height: 12px;
|
|
||||||
background: white;
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"][checked="true"] + label:before {
|
|
||||||
content: "✓";
|
|
||||||
align-items: center;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] + label {
|
|
||||||
line-height: 12px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Suspense, lazy } from "preact/compat";
|
import { Suspense, lazy } from "preact/compat";
|
||||||
|
|
||||||
const Renderer = lazy(() => import("./Renderer"));
|
const Renderer = lazy(() => import("./RemarkRenderer"));
|
||||||
|
|
||||||
export interface MarkdownProps {
|
export interface MarkdownProps {
|
||||||
content?: string;
|
content: string;
|
||||||
disallowBigEmoji?: boolean;
|
disallowBigEmoji?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Markdown(props: MarkdownProps) {
|
export default function Markdown(props: MarkdownProps) {
|
||||||
|
if (!props.content) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error Typings mis-match.
|
// @ts-expect-error Typings mis-match.
|
||||||
<Suspense fallback={props.content}>
|
<Suspense fallback={props.content}>
|
||||||
|
|||||||
261
src/components/markdown/RemarkRenderer.tsx
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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 = /(^(?:[>*+-][^\S\r\n]*){5})(?:[>*+-][^\S\r\n]*)+(.*$)/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching multi-line blockquotes
|
||||||
|
*/
|
||||||
|
const RE_BLOCKQUOTE = /^([^\S\r\n]*>[^\n]+\n?)+/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching HTML tags
|
||||||
|
*/
|
||||||
|
const RE_HTML_TAGS = /^(<\/?[a-zA-Z0-9]+>)(.*$)/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching empty lines
|
||||||
|
*/
|
||||||
|
const RE_EMPTY_LINE = /^\s*?$/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching line starting with plus
|
||||||
|
*/
|
||||||
|
const RE_PLUS = /^\s*\+(?:$|[^+])/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise Markdown input before rendering
|
||||||
|
* @param content Input string
|
||||||
|
* @returns Sanitised string
|
||||||
|
*/
|
||||||
|
function sanitise(content: string) {
|
||||||
|
return (
|
||||||
|
content
|
||||||
|
// Strip excessive blockquote or list indentation
|
||||||
|
.replace(RE_RECURSIVE, (_, m0, m1) => m0 + m1)
|
||||||
|
|
||||||
|
// Append empty character if string starts with html tag
|
||||||
|
// This is to avoid inconsistencies in rendering Markdown inside/after HTML tags
|
||||||
|
// https://github.com/revoltchat/revite/issues/733
|
||||||
|
.replace(RE_HTML_TAGS, (match) => `\u200E${match}`)
|
||||||
|
|
||||||
|
// Append empty character if line starts with a plus
|
||||||
|
// which would usually open a new list but we want
|
||||||
|
// to avoid that behaviour in our case.
|
||||||
|
.replace(RE_PLUS, (match) => `\u200E${match}`)
|
||||||
|
|
||||||
|
// Replace empty lines with non-breaking space
|
||||||
|
// because remark renderer is collapsing empty
|
||||||
|
// or otherwise whitespace-only lines of text
|
||||||
|
.replace(RE_EMPTY_LINE, "")
|
||||||
|
|
||||||
|
// Ensure empty line after blockquotes for correct rendering
|
||||||
|
.replace(RE_BLOCKQUOTE, (match) => `${match}\n`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remark renderer component
|
||||||
|
*/
|
||||||
|
export default memo(({ content, disallowBigEmoji }: MarkdownProps) => {
|
||||||
|
const sanitisedContent = useMemo(() => sanitise(content), [content]);
|
||||||
|
|
||||||
|
const [Content, setContent] = useState<React.ReactElement>(null!);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
render
|
||||||
|
.process(sanitisedContent)
|
||||||
|
.then((file) => setContent(file.result));
|
||||||
|
}, [sanitisedContent]);
|
||||||
|
|
||||||
|
const largeEmoji = useMemo(
|
||||||
|
() => !disallowBigEmoji && isOnlyEmoji(content!),
|
||||||
|
[content, disallowBigEmoji],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Container largeEmoji={largeEmoji}>{Content}</Container>;
|
||||||
|
});
|
||||||
@@ -1,269 +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";
|
|
||||||
// @ts-expect-error No typings.
|
|
||||||
import MarkdownSub from "markdown-it-sub";
|
|
||||||
// @ts-expect-error No typings.
|
|
||||||
import MarkdownSup from "markdown-it-sup";
|
|
||||||
import Prism from "prismjs";
|
|
||||||
import "prismjs/themes/prism-tomorrow.css";
|
|
||||||
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 { 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";
|
|
||||||
|
|
||||||
// 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(MarkdownSup)
|
|
||||||
.use(MarkdownSub)
|
|
||||||
.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;
|
|
||||||
|
|
||||||
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const { openLink } = useIntermediate();
|
|
||||||
|
|
||||||
if (typeof content === "undefined") return null;
|
|
||||||
if (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_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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
7
src/components/markdown/hast.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { passThroughComponents } from "./plugins/remarkRegexComponent";
|
||||||
|
import { timestampHandler } from "./plugins/timestamps";
|
||||||
|
|
||||||
|
export const handlers = {
|
||||||
|
...passThroughComponents("emoji", "spoiler", "mention", "channel"),
|
||||||
|
timestamp: timestampHandler,
|
||||||
|
};
|
||||||
79
src/components/markdown/plugins/Codeblock.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { useCallback, useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
import { Tooltip } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base codeblock styles
|
||||||
|
*/
|
||||||
|
const Base = styled.pre`
|
||||||
|
padding: 1em;
|
||||||
|
overflow-x: scroll;
|
||||||
|
background: var(--block);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy codeblock contents button styles
|
||||||
|
*/
|
||||||
|
const Lang = styled.div`
|
||||||
|
font-family: var(--monospace-font);
|
||||||
|
width: fit-content;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #111;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--accent);
|
||||||
|
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: 0 2px #787676;
|
||||||
|
border-radius: calc(var(--border-radius) / 3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow: 0 1px #787676;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a codeblock with copy text button
|
||||||
|
*/
|
||||||
|
export const RenderCodeblock: React.FC<{ class: string }> = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
|
|
||||||
|
let text = "text";
|
||||||
|
if (props.class) {
|
||||||
|
text = props.class.split("-")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCopy = useCallback(() => {
|
||||||
|
const text = ref.current?.querySelector("code")?.innerText;
|
||||||
|
text && modalController.writeText(text);
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base ref={ref}>
|
||||||
|
<Lang>
|
||||||
|
<Tooltip content="Copy to Clipboard" placement="top">
|
||||||
|
{/**
|
||||||
|
// @ts-expect-error Preact-React */}
|
||||||
|
<a onClick={onCopy}>{text}</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Lang>
|
||||||
|
{children}
|
||||||
|
</Base>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
src/components/markdown/plugins/anchors.tsx
Normal 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()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/markdown/plugins/channels.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { clientController } from "../../../controllers/client/ClientController";
|
||||||
|
import { createComponent, CustomComponentProps } from "./remarkRegexComponent";
|
||||||
|
|
||||||
|
export function RenderChannel({ match }: CustomComponentProps) {
|
||||||
|
const channel = clientController.getAvailableClient().channels.get(match)!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`${
|
||||||
|
channel.server_id ? `/server/${channel.server_id}` : ""
|
||||||
|
}/channel/${match}`}>{`#${channel.name}`}</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remarkChannels = createComponent(
|
||||||
|
"channel",
|
||||||
|
/<#([A-z0-9]{26})>/g,
|
||||||
|
(match) => clientController.getAvailableClient().channels.has(match),
|
||||||
|
);
|
||||||