Compare commits
645 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54fe2aa7a3 | ||
|
|
3ff201562b | ||
|
|
e139ac6584 | ||
|
|
85210df28e | ||
|
|
0af116ce76 | ||
|
|
a09bb109fd | ||
|
|
c97185a50e | ||
|
|
50f7fedfa0 | ||
|
|
178c6496bd | ||
|
|
c5eb9f0b99 | ||
|
|
af4c1280f5 | ||
|
|
97ae11f656 | ||
|
|
e182dd50f2 | ||
|
|
43f98e6be6 | ||
|
|
70ba6c3c6b | ||
|
|
29a7376bc7 | ||
|
|
db02178fce | ||
|
|
1d69bef7f9 | ||
|
|
0a83a8804f | ||
|
|
5795e20865 | ||
|
|
4aba1c8b74 | ||
|
|
dc694d4ffe | ||
|
|
fafc56bb90 | ||
|
|
a83611c287 | ||
|
|
2cca320291 | ||
|
|
834582a870 | ||
|
|
2390b990c5 | ||
|
|
166046a4b1 | ||
|
|
f2dbe8abbe | ||
|
|
1a814713df | ||
|
|
fceb10e2df | ||
|
|
94323b3597 | ||
|
|
a8c5cb4821 | ||
|
|
6e32aad729 | ||
|
|
49f6249144 | ||
|
|
ab08b58ef5 | ||
|
|
ba9efc64c3 | ||
|
|
e986ef914f | ||
|
|
68117cd9e4 | ||
|
|
ccb4f8c0e4 | ||
|
|
1367a50b75 | ||
|
|
aec21e661d | ||
|
|
ae7697b33c | ||
|
|
37f72fe0b6 | ||
|
|
5660938f47 | ||
|
|
1c76385d79 | ||
|
|
208a3d9045 | ||
|
|
16c9483f37 | ||
|
|
70939fa8f0 | ||
|
|
ec1f846c92 | ||
|
|
1570657176 | ||
|
|
7e78f7a670 | ||
|
|
d556fe188a | ||
|
|
c07aeb3ba8 | ||
|
|
a6c6aed61c | ||
|
|
28a20d9b1e | ||
|
|
077e361a26 | ||
|
|
6180f2e1b9 | ||
|
|
5e57a56d21 | ||
|
|
402f62e09a | ||
|
|
6ec2e9c822 | ||
|
|
684defdc19 | ||
|
|
5ed2dc6e0e | ||
|
|
ce86a6f120 | ||
|
|
96b1a5f296 | ||
|
|
e9ebccf0df | ||
|
|
02b2aef958 | ||
|
|
c6d60cff64 | ||
|
|
81771f511c | ||
|
|
004160b664 | ||
|
|
2d25d3c2bc | ||
|
|
4728804a33 | ||
|
|
8524b9ecd6 | ||
|
|
eca598e28f | ||
|
|
f808c56121 | ||
|
|
77da0c912f | ||
|
|
e8a875eb32 | ||
|
|
e7a94426c2 | ||
|
|
17613837b6 | ||
|
|
4b4c98066c | ||
|
|
4a5b69800c | ||
|
|
70d6c3e9bf | ||
|
|
90e32af220 | ||
|
|
fdc0272940 | ||
|
|
d90a837714 | ||
|
|
47f7e0e5a0 | ||
|
|
25388a77aa | ||
|
|
2155d9bb80 | ||
|
|
46ab10f733 | ||
|
|
6e91ec3a0e | ||
|
|
b55aa12100 | ||
|
|
ded6a80b58 | ||
|
|
7435f1101a | ||
|
|
7720770c67 | ||
|
|
d9fc9e82ab | ||
|
|
ae66e4b3f8 | ||
|
|
1e65f10d3f | ||
|
|
a76f27152b | ||
|
|
de0df4b534 | ||
|
|
f78cf6e79a | ||
|
|
b84c36eb2e | ||
|
|
6355aa863c | ||
|
|
80cc10e8b9 | ||
|
|
10c37d205a | ||
|
|
a9e94c341c | ||
|
|
3b181224fd | ||
|
|
89fa9dfd64 | ||
|
|
4a08ae75b3 | ||
|
|
d9b0f45c6a | ||
|
|
c5a3fb72e1 | ||
|
|
f0d7d8fac6 | ||
|
|
1f485bfd55 | ||
|
|
9e367db324 | ||
|
|
a2fdab8eb9 | ||
|
|
2c052c162f | ||
|
|
b1c9e8c07a | ||
|
|
f71817b0a2 | ||
|
|
73d09bc99c | ||
|
|
5ebb54a857 | ||
|
|
8725b2c230 | ||
|
|
fd18f2acdf | ||
|
|
3bffe58549 | ||
|
|
e8bc22370b | ||
|
|
b7be3011da | ||
|
|
f0045c9406 | ||
|
|
3186b5f24b | ||
|
|
ca5ce7d468 | ||
|
|
a05f6a64a8 | ||
|
|
70dffe95ff | ||
|
|
0360889fd6 | ||
|
|
7304411c5d | ||
|
|
22dd095ea9 | ||
|
|
30a270193f | ||
|
|
ee1dd2293e | ||
|
|
34d5e88def | ||
|
|
30c9dfce02 | ||
|
|
48ad4d040d | ||
|
|
1b4f097b1c | ||
|
|
7b6193ab62 | ||
|
|
10a2733fd5 | ||
|
|
e7353e184f | ||
|
|
a479863f88 | ||
|
|
c550545116 | ||
|
|
1d7da9c455 | ||
|
|
5be0fdea0b | ||
|
|
a2a6eaf695 | ||
|
|
d08573b6b8 | ||
|
|
af7daee3e7 | ||
|
|
3406b46db5 | ||
|
|
2b45cf1f67 | ||
|
|
ba4258aa89 | ||
|
|
fc0a3f38ac | ||
|
|
ad96da59c3 | ||
|
|
c7ce689739 | ||
|
|
fa0a8d30e7 | ||
|
|
b57ef84e66 | ||
|
|
e5432ef260 | ||
|
|
719156aadf | ||
|
|
0720005c93 | ||
|
|
897f127fbd | ||
|
|
fd8ade1bf1 | ||
|
|
7f6b0f572b | ||
|
|
a4d982ea62 | ||
|
|
317f27e5f9 | ||
|
|
b2427bd810 | ||
|
|
4ac5c2c677 | ||
|
|
2234962acc | ||
|
|
8f95da4b07 | ||
|
|
102bde65ba | ||
|
|
3d5421819f | ||
|
|
5167cacee8 | ||
|
|
882eed0737 | ||
|
|
e82ed2cbcb | ||
|
|
05466fbd7f | ||
|
|
2bfd26b2b5 | ||
|
|
a17b62b14c | ||
|
|
88cffdb70e | ||
|
|
51ae1c819a | ||
|
|
2608f9558c | ||
|
|
8176d60d96 | ||
|
|
2ce99b969d | ||
|
|
8b97904144 | ||
|
|
0e34f9a464 | ||
|
|
c09380644b | ||
|
|
1dfffce606 | ||
|
|
7e98b19587 | ||
|
|
2a1689009a | ||
|
|
5ef3b055ff | ||
|
|
f554afd6b1 | ||
|
|
5474693711 | ||
|
|
f9a41be530 | ||
|
|
c61bc46673 | ||
|
|
dd304d3569 | ||
|
|
2eff251e0c | ||
|
|
531db48c25 | ||
|
|
9c0ce6526c | ||
|
|
96123ccf63 | ||
|
|
305c2cb806 | ||
|
|
9af122b96e | ||
|
|
7ca08f2f30 | ||
|
|
c7dbfca53d | ||
|
|
8aa66dddfd | ||
|
|
eb43b96a1b | ||
|
|
a2963adbee | ||
|
|
baebfdb0bb | ||
|
|
c3c2f409e7 | ||
|
|
89312ceb58 | ||
|
|
9b915d289b | ||
|
|
3de8f9077d | ||
|
|
90b4e44bbe | ||
|
|
bd25b7f3b7 | ||
|
|
85dfb3c1e5 | ||
|
|
d16e42374f | ||
|
|
d56b802786 | ||
|
|
93db217239 | ||
|
|
33ef680c41 | ||
|
|
a150619d08 | ||
|
|
7d5fb5f041 | ||
|
|
e824b3cfe2 | ||
|
|
cd885e3b3a | ||
|
|
005622800d | ||
|
|
aef4fd39b9 | ||
|
|
2e57eaad1d | ||
|
|
a5d5f75f52 | ||
|
|
130073689d | ||
|
|
2d99acabe2 | ||
|
|
0e5231ba43 | ||
|
|
e62d76a6f2 | ||
|
|
ce55ed8221 | ||
|
|
c5e7fe7bdc | ||
|
|
c723fae0e2 | ||
|
|
68172d12b0 | ||
|
|
44ce76bcb1 | ||
|
|
44b9bd0046 | ||
|
|
2e38558a9d | ||
|
|
a679bfcd95 | ||
|
|
44315f327b | ||
|
|
4f7724dbaf | ||
|
|
dc3cc33893 | ||
|
|
2537088099 | ||
|
|
02aaa06cb3 | ||
|
|
abf5121b74 | ||
|
|
cc7584a223 | ||
|
|
43b6351237 | ||
|
|
3b74920ece | ||
|
|
005762a1a2 | ||
|
|
5841c4f38d | ||
|
|
6acc84fd9e | ||
|
|
afc072da2c | ||
|
|
8634c16a47 | ||
|
|
0aa3359f96 | ||
|
|
077e5b2998 | ||
|
|
4b01000d4c | ||
|
|
949d28a88f | ||
|
|
57cde41983 | ||
|
|
cb5b3e9468 | ||
|
|
69f19d24a3 | ||
|
|
549c54e311 | ||
|
|
ec7f9effd8 | ||
|
|
1f4cc7bb19 | ||
|
|
1d78e2bc20 | ||
|
|
942800a2a6 | ||
|
|
414996c3f5 | ||
|
|
0c3dab8dd2 | ||
|
|
c48f9a69cc | ||
|
|
3277887089 | ||
|
|
304339f589 | ||
|
|
45cfdef45d | ||
|
|
f440c3f2c8 | ||
|
|
db74a486c5 | ||
|
|
4f36d149d7 | ||
|
|
3727bfb67f | ||
|
|
f26ab2f941 | ||
|
|
cf56b24dda | ||
|
|
2a8cb3c4e2 | ||
|
|
5478e648a7 | ||
|
|
b47d633727 | ||
|
|
810cdeeab4 | ||
|
|
075049abc4 | ||
|
|
56afbe6eb1 | ||
|
|
cf309102a2 | ||
|
|
32b37ed8f0 | ||
|
|
ce8ac0a81c | ||
|
|
4d8e0d7b85 | ||
|
|
6d7f52d2d6 | ||
|
|
e63b3d1b3e | ||
|
|
d77d953f84 | ||
|
|
689835cc17 | ||
|
|
6456a6b0c0 | ||
|
|
996c5f86c1 | ||
|
|
3fc8fe505b | ||
|
|
daeecc9b68 | ||
|
|
982398b32f | ||
|
|
fae4c504c9 | ||
|
|
b4a56f6dd7 | ||
|
|
fc26bef80a | ||
|
|
034552a063 | ||
|
|
bb505273f4 | ||
|
|
f876df6acc | ||
|
|
d097223d41 | ||
|
|
d01f7be58a | ||
|
|
d5375ca9ed | ||
|
|
eda8404144 | ||
|
|
e17a7cedb6 | ||
|
|
4ad4cff23f | ||
|
|
cc7a44dc17 | ||
|
|
873e68e1e1 | ||
|
|
4f44a68198 | ||
|
|
1eab957d85 | ||
|
|
4c145af7a3 | ||
|
|
7fab4ca1ba | ||
|
|
c1e45c4a30 | ||
|
|
5784a005dc | ||
|
|
a3e4d6998f | ||
|
|
32907764b3 | ||
|
|
cb34b1634d | ||
|
|
5199fd2566 | ||
|
|
b31c6c6780 | ||
|
|
aeec2c076e | ||
|
|
8bbce188ef | ||
|
|
dbdc010764 | ||
|
|
a81c48cc22 | ||
|
|
6eb77b7c2f | ||
|
|
92a50fe51d | ||
|
|
572caf6826 | ||
|
|
b0c8ceb302 | ||
|
|
c9ae6532a0 | ||
|
|
619e3c4852 | ||
|
|
e5cfcb601b | ||
|
|
2b92bf3694 | ||
|
|
cd42d09ea9 | ||
|
|
c56b1c8a86 | ||
|
|
e8d99e15f7 | ||
|
|
4dcec504ca | ||
|
|
1308e52e42 | ||
|
|
f6d356c5ce | ||
|
|
eb2de869b8 | ||
|
|
c6030d33ca | ||
|
|
655058a7e6 | ||
|
|
16d4ffbe3a | ||
|
|
775125c8a7 | ||
|
|
631e63a0b5 | ||
|
|
4cb2306de0 | ||
|
|
f15ee439a9 | ||
|
|
b9a2473d19 | ||
|
|
5b58223f9d | ||
|
|
f34fd0bd00 | ||
|
|
984b02700e | ||
|
|
e310392800 | ||
|
|
2cc291dccd | ||
|
|
2dcf043787 | ||
|
|
6b03ae0dc3 | ||
|
|
5dd5668389 | ||
|
|
8380894692 | ||
|
|
94f16b986a | ||
|
|
2928df8b8c | ||
|
|
71a819fcf0 | ||
|
|
713136672a | ||
|
|
f1bd47be8c | ||
|
|
2e82960ae6 | ||
|
|
a31fcd7346 | ||
|
|
4a1a53d3ab | ||
|
|
be173a838d | ||
|
|
623bd52e1f | ||
|
|
5ebdf3e878 | ||
|
|
761eee2cdc | ||
|
|
831e49919b | ||
|
|
6d90586aee | ||
|
|
a7f0ade83a | ||
|
|
c49e300247 | ||
|
|
6d8e34762e | ||
|
|
33461f5ac2 | ||
|
|
4e3345482f | ||
|
|
7dc6fb27ea | ||
|
|
5ced94755b | ||
|
|
0ffd860fdb | ||
|
|
05e786e3d6 | ||
|
|
d5e638c8f7 | ||
|
|
122ffeeab5 | ||
|
|
1448eac7c1 | ||
|
|
f2dbd5ff96 | ||
|
|
dcae5ad5f2 | ||
|
|
9bd3ade93d | ||
|
|
22dcb883b3 | ||
|
|
2e945780de | ||
|
|
9033b688ab | ||
|
|
1d4ed6609d | ||
|
|
b0269e310f | ||
|
|
74ccf7d820 | ||
|
|
2eae6243bb | ||
|
|
276532e2e1 | ||
|
|
fc07dd2af9 | ||
|
|
989712c2d5 | ||
|
|
ee43fcc91f | ||
|
|
18ca92cec4 | ||
|
|
dc11814695 | ||
|
|
17a31e0904 | ||
|
|
f990530031 | ||
|
|
46f1f0f8e9 | ||
|
|
885e933948 | ||
|
|
9b2e99c559 | ||
|
|
60ed54d6d3 | ||
|
|
939398b277 | ||
|
|
d2c820f080 | ||
|
|
375578177b | ||
|
|
eb9f2ccbaa | ||
|
|
d4b211e678 | ||
|
|
9fc4fbc3e7 | ||
|
|
1f5ac411f6 | ||
|
|
a7748a8492 | ||
|
|
edbcf95ead | ||
|
|
0aa29f775c | ||
|
|
a4a6105bc9 | ||
|
|
23098131b8 | ||
|
|
fdcedb5592 | ||
|
|
17098cf2ab | ||
|
|
7ef3dcc56c | ||
|
|
8a38276f5d | ||
|
|
21ec08ffbd | ||
|
|
1a7211198b | ||
|
|
4f9efb3563 | ||
|
|
190c57e853 | ||
|
|
785eca7289 | ||
|
|
2667e78b43 | ||
|
|
878b48aa7a | ||
|
|
b314e047c1 | ||
|
|
69cfa1db6d | ||
|
|
977016fbb2 | ||
|
|
fb3d9e2a16 | ||
|
|
8da492d00d | ||
|
|
9676014120 | ||
|
|
7d87b8d1e5 | ||
|
|
ecb139721b | ||
|
|
aa45261b0d | ||
|
|
017ec13981 | ||
|
|
880a2ca127 | ||
|
|
5282ab5f12 | ||
|
|
582e6637dc | ||
|
|
65804cd962 | ||
|
|
0411e1cac8 | ||
|
|
bab5c9aa42 | ||
|
|
d680a36cab | ||
|
|
25bde3560b | ||
|
|
ddac2ba5ef | ||
|
|
cd55098921 | ||
|
|
f1bdad0d7f | ||
|
|
9fac2c95e5 | ||
|
|
486d0abd30 | ||
|
|
d9bd48b9a6 | ||
|
|
64e30c89e3 | ||
|
|
1860eaae7a | ||
|
|
771424cbf0 | ||
|
|
925a909ec1 | ||
|
|
f07ee54e05 | ||
|
|
7ee2f630db | ||
|
|
626fdb9f79 | ||
|
|
2cf40ff0b8 | ||
|
|
9edc1acc90 | ||
|
|
641e6c53b6 | ||
|
|
14fbddf780 | ||
|
|
2a69b72bed | ||
|
|
e21094b525 | ||
|
|
da3d038547 | ||
|
|
c6b90803f8 | ||
|
|
93baa19ba1 | ||
|
|
9444f43c72 | ||
|
|
26251e1e60 | ||
|
|
5b3183cbd3 | ||
|
|
e9b963080c | ||
|
|
1164e6f1e7 | ||
|
|
21c7bb979e | ||
|
|
1ff9073a1a | ||
|
|
7ed2f9bd9a | ||
|
|
2cdbeb6f12 | ||
|
|
7bd95621f1 | ||
|
|
a05501a909 | ||
|
|
e6960a1e15 | ||
|
|
c057713004 | ||
|
|
35e2135e3c | ||
|
|
af74228f8e | ||
|
|
9a44790450 | ||
|
|
5c4bab2a8a | ||
|
|
94380b64bd | ||
|
|
cbfd03f9c6 | ||
|
|
edf58f1d7d | ||
|
|
17fed7cd9c | ||
|
|
266861bdad | ||
|
|
426e1a433b | ||
|
|
3b8dfcec51 | ||
|
|
6f892edd5e | ||
|
|
126bfec339 | ||
|
|
59938cd46b | ||
|
|
a445bcd0b9 | ||
|
|
2acb6825e9 | ||
|
|
7d44a1e979 | ||
|
|
aa1fabf857 | ||
|
|
c714a0608c | ||
|
|
92d15e110a | ||
|
|
1367ff9914 | ||
|
|
7a2d64c0ef | ||
|
|
60b5f7cab2 | ||
|
|
d81c52e9bb | ||
|
|
c54f1bd7a3 | ||
|
|
24f721e414 | ||
|
|
3e19843bf7 | ||
|
|
183eea9f24 | ||
|
|
548ea7220b | ||
|
|
8cd45b64a1 | ||
|
|
c33d97a2ed | ||
|
|
7926a1f9b9 | ||
|
|
c7da1177ab | ||
|
|
1e5539f165 | ||
|
|
d019add257 | ||
|
|
cc8ce7a05c | ||
|
|
6913fddcd3 | ||
|
|
c13040f0b0 | ||
|
|
b3285974f9 | ||
|
|
24a1091954 | ||
|
|
9fd7329554 | ||
|
|
2a19a9964d | ||
|
|
3fc9c1b74a | ||
|
|
f6f0c20b08 | ||
|
|
26a1c165d9 | ||
|
|
2af87fa8b8 | ||
|
|
d34c8d08a4 | ||
|
|
0f687fb8b8 | ||
|
|
603dd3786a | ||
|
|
9fbe4278c2 | ||
|
|
b222b4f708 | ||
|
|
abc2449b07 | ||
|
|
e6459de0d9 | ||
|
|
323505fbb4 | ||
|
|
2b06c6f2e6 | ||
|
|
5a56e46f7b | ||
|
|
abe9ece38f | ||
|
|
cb8d837370 | ||
|
|
500a19d655 | ||
|
|
0d3daf5fa3 | ||
|
|
66aede01dc | ||
|
|
6d7be57dcf | ||
|
|
5b913205af | ||
|
|
fd93d89b26 | ||
|
|
abdfcd879d | ||
|
|
b231424f96 | ||
|
|
b2418d5384 | ||
|
|
f2232a0740 | ||
|
|
04c6d990bd | ||
|
|
455bb09108 | ||
|
|
d8fe617535 | ||
|
|
970568fd17 | ||
|
|
f6677889e0 | ||
|
|
04780ab7aa | ||
|
|
b7df8019f0 | ||
|
|
0a9115248d | ||
|
|
27d492e9e2 | ||
|
|
bc22d36ef8 | ||
|
|
cf9625f33e | ||
|
|
446fd9c7c0 | ||
|
|
adc7892d8c | ||
|
|
f805f4ead6 | ||
|
|
00ffa1b6cd | ||
|
|
055fbe786d | ||
|
|
7a561bd034 | ||
|
|
5fb1f556d5 | ||
|
|
f4ba315cef | ||
|
|
9ba12da544 | ||
|
|
657096fd9a | ||
|
|
9374900ce0 | ||
|
|
7e5610eb36 | ||
|
|
1253638861 | ||
|
|
83feb28909 | ||
|
|
5422cb76f1 | ||
|
|
a6eb52ae76 | ||
|
|
4488947eed | ||
|
|
bf8f164f55 | ||
|
|
5487fbc048 | ||
|
|
a70dbb130f | ||
|
|
7edf544d73 | ||
|
|
ad3bde9920 | ||
|
|
85a98b3706 | ||
|
|
85e3f3761a | ||
|
|
f0b116714b | ||
|
|
dbef06269b | ||
|
|
894815268a | ||
|
|
8ecec0bc7e | ||
|
|
66839e02f6 | ||
|
|
bad8f36bf5 | ||
|
|
f5c50230a9 | ||
|
|
0136fd3cab | ||
|
|
2d18953344 | ||
|
|
d930ab869a | ||
|
|
dbdb82bd74 | ||
|
|
61309bacd9 | ||
|
|
b3e88d33a7 | ||
|
|
73fda641c8 | ||
|
|
be01a4bd81 | ||
|
|
0814e3c905 | ||
|
|
c7dd2e2093 | ||
|
|
cfa525f957 | ||
|
|
43d579744f | ||
|
|
48a008093b | ||
|
|
70c099c4b5 | ||
|
|
363f2340a0 | ||
|
|
3a6346aa63 | ||
|
|
9ef9680e07 | ||
|
|
e3cec93669 | ||
|
|
b6c926d2c8 | ||
|
|
c430ebb3a3 | ||
|
|
ae13814449 | ||
|
|
7a9ff98550 | ||
|
|
3d54047f87 | ||
|
|
dc75c1cfb4 | ||
|
|
e2aee0be81 | ||
|
|
44486aa62d | ||
|
|
a0e4de73cc | ||
|
|
38f9a79bd3 | ||
|
|
fc1aaf02bf | ||
|
|
c05b6c5118 | ||
|
|
72197c1a0a | ||
|
|
46bcb8ac75 | ||
|
|
2ba1bab82d | ||
|
|
3c56f7f481 | ||
|
|
fcd8a41fc9 | ||
|
|
35f8b1ed85 | ||
|
|
7969e13fc1 | ||
|
|
4d433ab22d | ||
|
|
d7f46607ad | ||
|
|
1e59390599 | ||
|
|
2457476bae | ||
|
|
35fb1e710b | ||
|
|
014b740e47 | ||
|
|
2b3c04592b | ||
|
|
ae50d57814 | ||
|
|
9900d661be | ||
|
|
369b59a203 | ||
|
|
6a18ba0110 | ||
|
|
0a49ddb31e | ||
|
|
25385edf12 | ||
|
|
721cccf152 | ||
|
|
3b017eb92b | ||
|
|
641b82dc45 | ||
|
|
42e2041d6f | ||
|
|
2c3ebd4c03 | ||
|
|
81a763f17f | ||
|
|
1ab7d27ba9 |
@@ -14,12 +14,17 @@
|
|||||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||||
|
|
||||||
# VITE_CUSTOM_THEME=true
|
# VITE_CUSTOM_THEME=true
|
||||||
# VITE_PRIMARY_COLOR=#0dbd8b
|
# VITE_THEME_ACCENT=#0dbd8b
|
||||||
# VITE_BG_COLOR_1=#ffffff
|
# VITE_THEME_ACCENT_20=#0dbd8b33
|
||||||
# VITE_BG_COLOR_2=#f0f1f4
|
# VITE_THEME_ALERT=#ff5b55
|
||||||
# VITE_BG_COLOR_3=#dbdfe4
|
# VITE_THEME_ALERT_20=#ff5b5533
|
||||||
# VITE_BG_COLOR_4=#d1d3d7
|
# VITE_THEME_LINKS=#0086e6
|
||||||
# VITE_INPUT_BORDER_COLOR=#e7e7e7
|
# VITE_THEME_PRIMARY_CONTENT=#ffffff
|
||||||
# VITE_INPUT_BORDER_COLOR_FOCUSED=#238cf5
|
# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
|
||||||
# VITE_TEXT_COLOR_1=#17191c
|
# VITE_THEME_TERTIARY_CONTENT=#8e99a4
|
||||||
# VITE_TEXT_COLOR_2=#61708b
|
# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433
|
||||||
|
# VITE_THEME_QUATERNARY_CONTENT=#6f7882
|
||||||
|
# VITE_THEME_QUINARY_CONTENT=#394049
|
||||||
|
# VITE_THEME_SYSTEM=#21262c
|
||||||
|
# VITE_THEME_BACKGROUND=#15191e
|
||||||
|
# VITE_THEME_BACKGROUND_85=#15191ed9
|
||||||
38
.eslintrc.cjs
Normal file
38
.eslintrc.cjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
"matrix-org",
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
"plugin:matrix-org/react",
|
||||||
|
"plugin:matrix-org/a11y",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"jsx-a11y/media-has-caption": ["off"],
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"src/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
"plugin:matrix-org/typescript",
|
||||||
|
"plugin:matrix-org/react",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @vector-im/element-call-reviewers
|
||||||
67
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
67
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: [T-Defect]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
|
Please report security issues by email to security@matrix.org
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Please attach screenshots, videos or logs if you can.
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: |
|
||||||
|
1. Where are you starting? What can you see?
|
||||||
|
2. What do you click?
|
||||||
|
3. More steps…
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: result
|
||||||
|
attributes:
|
||||||
|
label: Outcome
|
||||||
|
placeholder: Tell us what went wrong
|
||||||
|
value: |
|
||||||
|
#### What did you expect?
|
||||||
|
|
||||||
|
#### What happened instead?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
placeholder: Windows, macOS, Ubuntu, Android…
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser information
|
||||||
|
description: Which browser are you using? Which version?
|
||||||
|
placeholder: e.g. Chromium Version 92.0.4515.131
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: webapp-url
|
||||||
|
attributes:
|
||||||
|
label: URL for webapp
|
||||||
|
description: Which URL are you using to access the webapp? If a private server, tell us what version of Element Call you are using.
|
||||||
|
placeholder: e.g. call.element.io
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: rageshake
|
||||||
|
attributes:
|
||||||
|
label: Will you send logs?
|
||||||
|
description: |
|
||||||
|
To send them, press the 'Submit Feedback' button and check 'Include Debug Logs'. Please link to this issue in the description field.
|
||||||
|
options:
|
||||||
|
- 'Yes'
|
||||||
|
- 'No'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Questions & support
|
||||||
|
url: https://matrix.to/#/#webrtc:matrix.org
|
||||||
|
about: Please ask and answer questions here.
|
||||||
36
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Enhancement request
|
||||||
|
description: Do you have a suggestion or feature request?
|
||||||
|
labels: [T-Enhancement]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to propose a new feature or make a suggestion.
|
||||||
|
- type: textarea
|
||||||
|
id: usecase
|
||||||
|
attributes:
|
||||||
|
label: Your use case
|
||||||
|
description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
|
||||||
|
placeholder: Tell us what you would like to do!
|
||||||
|
value: |
|
||||||
|
#### What would you like to do?
|
||||||
|
|
||||||
|
#### Why would you like to do it?
|
||||||
|
|
||||||
|
#### How would you like to achieve it?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternative
|
||||||
|
attributes:
|
||||||
|
label: Have you considered any alternatives?
|
||||||
|
placeholder: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
placeholder: Is there anything else you'd like to add?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
31
.github/workflows/build.yaml
vendored
Normal file
31
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
env:
|
||||||
|
VITE_DEFAULT_HOMESERVER: "https://call.ems.host"
|
||||||
|
VITE_SENTRY_DSN: https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
||||||
|
VITE_SENTRY_ENVIRONMENT: main-branch-cd
|
||||||
|
VITE_RAGESHAKE_SUBMIT_URL: https://element.io/bugreports/submit
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Yarn cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: "yarn install"
|
||||||
|
- name: Build
|
||||||
|
run: "yarn run build"
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: dist
|
||||||
|
# We'll only use this in a triggered job, then we're done with it
|
||||||
|
retention-days: 1
|
||||||
22
.github/workflows/lint.yaml
vendored
Normal file
22
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Lint, format & type check
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
jobs:
|
||||||
|
prettier:
|
||||||
|
name: Lint, format & type check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Yarn cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: "yarn install"
|
||||||
|
- name: Prettier
|
||||||
|
run: "yarn run prettier:check"
|
||||||
|
- name: ESLint
|
||||||
|
run: "yarn run lint:js"
|
||||||
|
- name: Type check
|
||||||
|
run: "yarn run lint:types"
|
||||||
79
.github/workflows/netlify-main.yaml
vendored
Normal file
79
.github/workflows/netlify-main.yaml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Netlify Main
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
deployments: write
|
||||||
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
steps:
|
||||||
|
- name: Create Deployment
|
||||||
|
uses: bobheadxi/deployments@v1
|
||||||
|
id: deployment
|
||||||
|
with:
|
||||||
|
step: start
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
env: main-branch-cd
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
|
||||||
|
- name: 'Download artifact'
|
||||||
|
uses: actions/github-script@v3.1.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{ github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "build"
|
||||||
|
})[0];
|
||||||
|
const download = await github.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/build.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: Extract Artifacts
|
||||||
|
run: unzip -d dist build.zip && rm build.zip
|
||||||
|
|
||||||
|
- name: Add redirects file
|
||||||
|
# We fetch from github directly as we don't bother checking out the repo
|
||||||
|
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/main/config/netlify_redirects > dist/_redirects
|
||||||
|
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
id: netlify
|
||||||
|
uses: nwtgck/actions-netlify@v1.2.3
|
||||||
|
with:
|
||||||
|
publish-dir: dist
|
||||||
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
|
production-branch: main
|
||||||
|
production-deploy: true
|
||||||
|
# These don't work because we're in workflow_run
|
||||||
|
enable-pull-request-comment: false
|
||||||
|
enable-commit-comment: false
|
||||||
|
github-deployment-environment: main
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
|
timeout-minutes: 1
|
||||||
|
|
||||||
|
- name: Update deployment status
|
||||||
|
uses: bobheadxi/deployments@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
step: finish
|
||||||
|
override: false
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
env: ${{ steps.deployment.outputs.env }}
|
||||||
|
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
env_url: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -32,10 +32,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.idea/
|
||||||
|
|||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -18,11 +18,6 @@ module.exports = {
|
|||||||
);
|
);
|
||||||
config.plugins.push(svgrPlugin());
|
config.plugins.push(svgrPlugin());
|
||||||
config.resolve = config.resolve || {};
|
config.resolve = config.resolve || {};
|
||||||
config.resolve.alias = config.resolve.alias || {};
|
|
||||||
config.resolve.alias["$(res)"] = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../node_modules/matrix-react-sdk/res"
|
|
||||||
);
|
|
||||||
config.resolve.dedupe = config.resolve.dedupe || [];
|
config.resolve.dedupe = config.resolve.dedupe || [];
|
||||||
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
@@ -1,5 +1,22 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.insertSpaces": true,
|
"editor.insertSpaces": true,
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2,
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
10
Dockerfile
10
Dockerfile
@@ -1,15 +1,15 @@
|
|||||||
FROM node:16-buster as builder
|
FROM --platform=$BUILDPLATFORM node:16-buster as builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src/matrix-video-chat
|
COPY . /src/element-call
|
||||||
RUN matrix-video-chat/scripts/dockerbuild.sh
|
RUN element-call/scripts/dockerbuild.sh
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginxinc/nginx-unprivileged:alpine
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
COPY --from=builder /src/matrix-video-chat/dist /app
|
COPY --from=builder /src/element-call/dist /app
|
||||||
COPY scripts/default.conf /etc/nginx/conf.d/
|
COPY config/default.conf /etc/nginx/conf.d/
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -1,10 +1,12 @@
|
|||||||
# Matrix Video Chat
|
# Element Call
|
||||||
|
|
||||||
Testbed for full mesh video chat.
|
Showcase for full mesh video chat powered by Matrix, implementing [MSC3401](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md).
|
||||||
|
|
||||||
|
Discussion in [#webrtc:matrix.org: ](https://matrix.to/#/#webrtc:matrix.org)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
`matrix-video-chat` is built against the `robertlong/group-call` branch of both [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902) and [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/pull/6848). Because of how these packages are configured and Vite's requirements, you will need to clone them locally and use `yarn link` to stich things together.
|
`element-call` is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902). Because of how this package is configured and Vite's requirements, you will need to clone it locally and use `yarn link` to stich things together.
|
||||||
|
|
||||||
First clone, install, and link `matrix-js-sdk`
|
First clone, install, and link `matrix-js-sdk`
|
||||||
|
|
||||||
@@ -16,30 +18,37 @@ yarn
|
|||||||
yarn link
|
yarn link
|
||||||
```
|
```
|
||||||
|
|
||||||
Then clone, install, link `matrix-js-sdk` into `matrix-react-sdk`, and link `matrix-react-sdk`
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
|
||||||
cd matrix-react-sdk
|
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn link
|
|
||||||
```
|
|
||||||
|
|
||||||
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
||||||
|
|
||||||
Finally we can set up this project.
|
Finally we can set up this project.
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/vector-im/matrix-video-chat.git
|
git clone https://github.com/vector-im/element-call.git
|
||||||
cd matrix-video-chat
|
cd element-call
|
||||||
yarn
|
yarn
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
cp .env.example .env
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
Configuration options are documented in the `.env` file.
|
Configuration options are documented in the `.env` file.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All files in this project are:
|
||||||
|
|
||||||
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
4
config/netlify_redirects
Normal file
4
config/netlify_redirects
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# This file is copied to the netlify deploy dir in the upload stage
|
||||||
|
|
||||||
|
# Redirect any unknown path to index.html
|
||||||
|
/* /index.html 200
|
||||||
20
i18next-parser.config.js
Normal file
20
i18next-parser.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
keySeparator: false,
|
||||||
|
namespaceSeparator: false,
|
||||||
|
contextSeparator: "|",
|
||||||
|
pluralSeparator: "|",
|
||||||
|
createOldCatalogs: false,
|
||||||
|
defaultNamespace: "app",
|
||||||
|
lexers: {
|
||||||
|
ts: [{
|
||||||
|
lexer: "JavascriptLexer",
|
||||||
|
functions: ["t", "translatedError"],
|
||||||
|
functionsNamespace: ["useTranslation", "withTranslation"],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
locales: ["en-GB"],
|
||||||
|
output: "public/locales/$LOCALE/$NAMESPACE.json",
|
||||||
|
input: ["src/**/*.{ts,tsx}"],
|
||||||
|
sort: true,
|
||||||
|
useKeysAsDefaultValue: true,
|
||||||
|
};
|
||||||
41
package.json
41
package.json
@@ -1,14 +1,22 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "start-storybook -p 6006",
|
||||||
"build-storybook": "build-storybook"
|
"build-storybook": "build-storybook",
|
||||||
|
"prettier:check": "prettier -c src",
|
||||||
|
"prettier:format": "prettier -w src",
|
||||||
|
"lint": "yarn lint:types && yarn lint:js",
|
||||||
|
"lint:js": "eslint --max-warnings 0 src",
|
||||||
|
"lint:types": "tsc",
|
||||||
|
"i18n": "node_modules/i18next-parser/bin/cli.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||||
"@react-aria/button": "^3.3.4",
|
"@react-aria/button": "^3.3.4",
|
||||||
"@react-aria/dialog": "^3.1.4",
|
"@react-aria/dialog": "^3.1.4",
|
||||||
"@react-aria/focus": "^3.5.0",
|
"@react-aria/focus": "^3.5.0",
|
||||||
@@ -18,6 +26,7 @@
|
|||||||
"@react-aria/tabs": "^3.1.0",
|
"@react-aria/tabs": "^3.1.0",
|
||||||
"@react-aria/tooltip": "^3.1.3",
|
"@react-aria/tooltip": "^3.1.3",
|
||||||
"@react-aria/utils": "^3.10.0",
|
"@react-aria/utils": "^3.10.0",
|
||||||
|
"@react-spring/web": "^9.4.4",
|
||||||
"@react-stately/collections": "^3.3.4",
|
"@react-stately/collections": "^3.3.4",
|
||||||
"@react-stately/overlays": "^3.1.3",
|
"@react-stately/overlays": "^3.1.3",
|
||||||
"@react-stately/select": "^3.1.3",
|
"@react-stately/select": "^3.1.3",
|
||||||
@@ -25,31 +34,55 @@
|
|||||||
"@react-stately/tree": "^3.2.0",
|
"@react-stately/tree": "^3.2.0",
|
||||||
"@sentry/react": "^6.13.3",
|
"@sentry/react": "^6.13.3",
|
||||||
"@sentry/tracing": "^6.13.3",
|
"@sentry/tracing": "^6.13.3",
|
||||||
|
"@types/grecaptcha": "^3.0.4",
|
||||||
|
"@types/sdp-transform": "^2.4.5",
|
||||||
|
"@use-gesture/react": "^10.2.11",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
"i18next": "^21.10.0",
|
||||||
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
"i18next-browser-languagedetector": "^6.1.8",
|
||||||
|
"i18next-http-backend": "^1.4.4",
|
||||||
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#5a0787349d4951012eabe72f3363c17bdcda0d56",
|
||||||
|
"matrix-widget-api": "^1.0.0",
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^7",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
|
"react-i18next": "^11.18.6",
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-use-clipboard": "^1.0.7",
|
"react-use-clipboard": "^1.0.7",
|
||||||
"react-use-measure": "^2.1.1",
|
"react-use-measure": "^2.1.1",
|
||||||
|
"sdp-transform": "^2.14.1",
|
||||||
"unique-names-generator": "^4.6.0"
|
"unique-names-generator": "^4.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||||
"@storybook/react": "^6.5.0-alpha.5",
|
"@storybook/react": "^6.5.0-alpha.5",
|
||||||
|
"@types/request": "^2.48.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
||||||
|
"@typescript-eslint/parser": "^5.22.0",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
|
"eslint": "^8.14.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-matrix-org": "^0.4.0",
|
||||||
|
"eslint-plugin-react": "^7.29.4",
|
||||||
|
"eslint-plugin-react-hooks": "^4.5.0",
|
||||||
|
"i18next-parser": "^6.6.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"storybook-builder-vite": "^0.1.12",
|
"storybook-builder-vite": "^0.1.12",
|
||||||
|
"typescript": "^4.6.4",
|
||||||
|
"typescript-strict-plugin": "^2.0.1",
|
||||||
"vite": "^2.4.2",
|
"vite": "^2.4.2",
|
||||||
"vite-plugin-html-template": "^1.1.0",
|
"vite-plugin-html-template": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^0.4.0"
|
"vite-plugin-svgr": "^0.4.0"
|
||||||
|
|||||||
132
public/locales/bg/app.json
Normal file
132
public/locales/bg/app.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Вече имате акаунт?</0><1><0>Влезте с него</0> или <2>Влезте като гост</2></1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Създайте акаунт</0> или <2>Влезте като гост</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Възникна грешка.</0><1>Изпращнето на debug логове ще ни помогне да открием проблема.</1>",
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Защо не настройте парола за да запазите акаунта си?</0><1>Ще можете да запазите името и аватара си за бъдещи разговори</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Приемете разрешенията за камера/микрофон за да се присъедините в разговора.",
|
||||||
|
"Accept microphone permissions to join the call.": "Приемете разрешението за микрофона за да се присъедините в разговора.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.",
|
||||||
|
"Audio": "Звук",
|
||||||
|
"Avatar": "Аватар",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия</2>",
|
||||||
|
"Call link copied": "Връзка към разговора бе копирана",
|
||||||
|
"Call type menu": "Меню \"тип на разговора\"",
|
||||||
|
"Camera": "Камера",
|
||||||
|
"Camera {{n}}": "Камера {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Необходими са разрешения за камера/микрофон за да се присъедините в разговора.",
|
||||||
|
"Change layout": "Промени изгледа",
|
||||||
|
"Close": "Затвори",
|
||||||
|
"Confirm password": "Потвърди паролата",
|
||||||
|
"Connection lost": "Връзката се изгуби",
|
||||||
|
"Copied!": "Копирано!",
|
||||||
|
"Copy and share this call link": "Копирай и сподели връзка към разговора",
|
||||||
|
"Copy call link and join later": "Копирай връзка към разговора и се присъедини по-късно",
|
||||||
|
"Create account": "Създай акаунт",
|
||||||
|
"Debug log": "Debug логове",
|
||||||
|
"Debug log request": "Заявка за debug логове",
|
||||||
|
"Description (optional)": "Описание (незадължително)",
|
||||||
|
"Details": "Детайли",
|
||||||
|
"Developer": "Разработчик",
|
||||||
|
"Display name": "Име/псевдоним",
|
||||||
|
"Download debug logs": "Изтеглете debug логове",
|
||||||
|
"Entering room…": "Влизане в стаята…",
|
||||||
|
"Exit full screen": "Излез от цял екран",
|
||||||
|
"Fetching group call timed out.": "Изтече времето за взимане на груповия разговор.",
|
||||||
|
"Freedom": "Свобода",
|
||||||
|
"Full screen": "Цял екран",
|
||||||
|
"Go": "Напред",
|
||||||
|
"Grid layout menu": "Меню \"решетков изглед\"",
|
||||||
|
"Having trouble? Help us fix it.": "Имате проблем? Помогнете да го поправим.",
|
||||||
|
"Home": "Начало",
|
||||||
|
"Include debug logs": "Включи debug логове",
|
||||||
|
"Incompatible versions": "Несъвместими версии",
|
||||||
|
"Incompatible versions!": "Несъвместими версии!",
|
||||||
|
"Inspector": "Инспектор",
|
||||||
|
"Invite": "Покани",
|
||||||
|
"Invite people": "Покани хора",
|
||||||
|
"Join call": "Влез в разговора",
|
||||||
|
"Join call now": "Влез в разговора сега",
|
||||||
|
"Join existing call?": "Присъединяване към съществуващ разговор?",
|
||||||
|
"Leave": "Напусни",
|
||||||
|
"Loading room…": "Напускане на стаята…",
|
||||||
|
"Loading…": "Зареждане…",
|
||||||
|
"Local volume": "Локална сила на звука",
|
||||||
|
"Logging in…": "Влизане…",
|
||||||
|
"Login": "Влез",
|
||||||
|
"Login to your account": "Влезте в акаунта си",
|
||||||
|
"Microphone": "Микрофон",
|
||||||
|
"Microphone permissions needed to join the call.": "Необходими са разрешения за микрофона за да можете да се присъедините в разговора.",
|
||||||
|
"Microphone {{n}}": "Микрофон {{n}}",
|
||||||
|
"More": "Още",
|
||||||
|
"More menu": "Мено \"още\"",
|
||||||
|
"Mute microphone": "Заглуши микрофона",
|
||||||
|
"No": "Не",
|
||||||
|
"Not now, return to home screen": "Не сега, върни се на началния екран",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Други потребители се опитват да се присъединят в разговора от несъвместими версии. Следните потребители трябва да проверят дали са презаредили браузърите си<1>{userLis}</1>",
|
||||||
|
"Password": "Парола",
|
||||||
|
"Passwords must match": "Паролите не съвпадат",
|
||||||
|
"Press and hold spacebar to talk": "Натиснете и задръжте Space за да говорите",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Натиснете и задръжте Space за да говорите заедно с {{name}}",
|
||||||
|
"Press and hold to talk": "Натиснете и задръжте за да говорите",
|
||||||
|
"Press and hold to talk over {{name}}": "Натиснете и задръжте за да говорите заедно с {{name}}",
|
||||||
|
"Profile": "Профил",
|
||||||
|
"Recaptcha dismissed": "Recaptcha отхвърлена",
|
||||||
|
"Recaptcha not loaded": "Recaptcha не е заредена",
|
||||||
|
"Register": "Регистрация",
|
||||||
|
"Registering…": "Регистриране…",
|
||||||
|
"Release spacebar key to stop": "Отпуснете Space за да спрете",
|
||||||
|
"Release to stop": "Отпуснете за да спрете",
|
||||||
|
"Remove": "Премахни",
|
||||||
|
"Return to home screen": "Връщане на началния екран",
|
||||||
|
"Save": "Запази",
|
||||||
|
"Saving…": "Запазване…",
|
||||||
|
"Select an option": "Изберете опция",
|
||||||
|
"Send debug logs": "Изпратете debug логове",
|
||||||
|
"Sending…": "Изпращане…",
|
||||||
|
"Settings": "Настройки",
|
||||||
|
"Share screen": "Сподели екрана",
|
||||||
|
"Show call inspector": "Покажи инспектора на разговора",
|
||||||
|
"Sign in": "Влез",
|
||||||
|
"Sign out": "Излез",
|
||||||
|
"Spatial audio": "Пространствен звук",
|
||||||
|
"Speaker": "Говорител",
|
||||||
|
"Speaker {{n}}": "Говорител {{n}}",
|
||||||
|
"Spotlight": "Прожектор",
|
||||||
|
"Stop sharing screen": "Спри споделянето на екрана",
|
||||||
|
"Submit feedback": "Изпрати обратна връзка",
|
||||||
|
"Submitting feedback…": "Изпращане на обратна връзка…",
|
||||||
|
"Take me Home": "Отиди в Начало",
|
||||||
|
"Talk over speaker": "Говорете заедно с говорителя",
|
||||||
|
"Talking…": "Говорене…",
|
||||||
|
"Thanks! We'll get right on it.": "Благодарим! Веднага ще се заемем.",
|
||||||
|
"This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?",
|
||||||
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Този сайт се предпазва от ReCAPTCHA и важат <2>Политиката за поверителност</2> и <6>Условията за ползване на услугата</6> на Google.<9></9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия</12>",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Това прави звука на говорителя да изглежда, че излиза от мястото където са позиционирани на екрана. (Експериментална функция: може да повлияе на стабилността на звука.)",
|
||||||
|
"Turn off camera": "Изключи камерата",
|
||||||
|
"Turn on camera": "Включи камерата",
|
||||||
|
"Unmute microphone": "Включи микрофона",
|
||||||
|
"User ID": "Потребителски идентификатор",
|
||||||
|
"User menu": "Потребителско меню",
|
||||||
|
"Username": "Потребителско име",
|
||||||
|
"Version: {{version}}": "Версия: {{version}}",
|
||||||
|
"Video": "Видео",
|
||||||
|
"Video call": "Видео разговор",
|
||||||
|
"Video call name": "Име на видео разговора",
|
||||||
|
"Waiting for network": "Изчакване на мрежата",
|
||||||
|
"Waiting for other participants…": "Изчакване на други участници…",
|
||||||
|
"Walkie-talkie call": "Уоки-токи разговор",
|
||||||
|
"Walkie-talkie call name": "Име на уоки-токи разговора",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не се поддържа или се блокира от браузъра.",
|
||||||
|
"Yes, join call": "Да, присъедини се",
|
||||||
|
"You can't talk at the same time": "Не можете да говорите едновременно",
|
||||||
|
"Your recent calls": "Скорошните ви разговори",
|
||||||
|
"{{count}} people connected|one": "{{count}} човек се свърза",
|
||||||
|
"{{count}} people connected|other": "{{count}} човека се звързаха",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, разговорът ви приключи",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} презентира",
|
||||||
|
"{{name}} is talking…": "{{name}} говори…",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - уоки-токи разговор"
|
||||||
|
}
|
||||||
133
public/locales/de/app.json
Normal file
133
public/locales/de/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Du hast bereits ein Konto?</0><1><0>Anmelden</0> Oder <2>Als Gast betreten</2></1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Konto erstellen</0> Oder <2>Als Gast betreten</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoppla, da ist etwas schief gelaufen.</0><1>Die Übermittlung von Debug-Protokollen wird uns helfen, das Problem zu finden.</1>",
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Warum vergibst Du nicht abschließend ein Passwort, um Dein Konto zu erhalten?</0><1>Du kannst Deinen Namen behalten und einen Avatar für zukünftige Anrufe festlegen.</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Erlaube Zugriff auf Kamera/Mikrofon um dem Anruf beizutreten.",
|
||||||
|
"Accept microphone permissions to join the call.": "Erlaube Zugriff auf das Mikrofon um dem Anruf beizutreten.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um dieses besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Mit dem Klick auf \"Los geht's\", akzeptierst Du unsere <2>Geschäftsbedingungen</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn Du auf \"Jetzt anrufen\" klickst, erklärst Du dich mit unserer <2>Geschäftsbedingungen</2> einverstanden",
|
||||||
|
"Call link copied": "Anruflink kopiert",
|
||||||
|
"Call type menu": "Anruftyp Menü",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Camera {{n}}": "Kamera {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Kamera-/Mikrofonberechtigung für die Teilnahme am Anruf erforderlich.",
|
||||||
|
"Change layout": "Layout ändern",
|
||||||
|
"Close": "Schließen",
|
||||||
|
"Confirm password": "Passwort bestätigen",
|
||||||
|
"Connection lost": "Verbindung verloren",
|
||||||
|
"Copied!": "Kopiert!",
|
||||||
|
"Copy and share this call link": "Kopiere und teile diesen Anruflink",
|
||||||
|
"Copy call link and join later": "Kopiere den Anruflink und nehme später teil",
|
||||||
|
"Create account": "Konto erstellen",
|
||||||
|
"Debug log": "Debug-Protokoll",
|
||||||
|
"Debug log request": "Debug-Log Anfrage",
|
||||||
|
"Description (optional)": "Beschreibung (wahlweise)",
|
||||||
|
"Details": "Details",
|
||||||
|
"Developer": "Entwickler",
|
||||||
|
"Display name": "Anzeigename",
|
||||||
|
"Download debug logs": "Debug-Logs herunterladen",
|
||||||
|
"Entering room…": "Betrete Raum …",
|
||||||
|
"Exit full screen": "Vollbildmodus verlassen",
|
||||||
|
"Freedom": "Freiraum",
|
||||||
|
"Full screen": "Vollbild",
|
||||||
|
"Go": "Los geht's",
|
||||||
|
"Grid layout menu": "Grid-Layout-Menü",
|
||||||
|
"Having trouble? Help us fix it.": "Hast Du Probleme? Hilf uns, es zu beheben.",
|
||||||
|
"Home": "Startseite",
|
||||||
|
"Include debug logs": "Debug-Logs hinzufügen",
|
||||||
|
"Incompatible versions": "Inkompatible Versionen",
|
||||||
|
"Incompatible versions!": "Inkompatible Versionen!",
|
||||||
|
"Inspector": "Inspektor",
|
||||||
|
"Invite": "Einladen",
|
||||||
|
"Invite people": "Personen einladen",
|
||||||
|
"Join call": "Anruf beitreten",
|
||||||
|
"Join call now": "Trete dem Anruf bei",
|
||||||
|
"Join existing call?": "An bestehendem Anruf teilnehmen?",
|
||||||
|
"Leave": "Verlassen",
|
||||||
|
"Loading room…": "Lade Raum …",
|
||||||
|
"Loading…": "Lade …",
|
||||||
|
"Local volume": "Lokale Lautstärke",
|
||||||
|
"Logging in…": "Anmelden …",
|
||||||
|
"Login": "Anmelden",
|
||||||
|
"Login to your account": "Anmeldung bei Deinem Konto",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Microphone permissions needed to join the call.": "Mikrofon Berechtigung ist erforderlich, um dem Anruf beizutreten.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"More": "Mehr",
|
||||||
|
"More menu": "Weiteres Menü",
|
||||||
|
"Mute microphone": "Mikrofon stummschalten",
|
||||||
|
"No": "Nein",
|
||||||
|
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Noch nicht registriert? <2>Konto erstellen</2>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Andere Benutzer versuchen, diesem Aufruf von einer inkompatiblen Softwareversion aus beizutreten. Diese Benutzer sollten ihre Web-Browser Seite neu laden:<1>{userLis}</1>",
|
||||||
|
"Password": "Passwort",
|
||||||
|
"Passwords must match": "Passwörter müssen übereinstimmen",
|
||||||
|
"Press and hold spacebar to talk": "Zum Sprechen die Leertaste gedrückt halten",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen die Leertaste gedrückt halten",
|
||||||
|
"Press and hold to talk": "Zum Sprechen gedrückt halten",
|
||||||
|
"Press and hold to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen gedrückt halten",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recaptcha dismissed": "Recaptcha abgelehnt",
|
||||||
|
"Recaptcha not loaded": "Recaptcha nicht geladen",
|
||||||
|
"Register": "Registrieren",
|
||||||
|
"Registering…": "Registrierung …",
|
||||||
|
"Release spacebar key to stop": "Leertaste loslassen, um zu stoppen",
|
||||||
|
"Release to stop": "Loslassen zum Stoppen",
|
||||||
|
"Remove": "Entfernen",
|
||||||
|
"Return to home screen": "Zurück zum Startbildschirm",
|
||||||
|
"Save": "Speichern",
|
||||||
|
"Saving…": "Speichere …",
|
||||||
|
"Select an option": "Wähle eine Option",
|
||||||
|
"Send debug logs": "Debug-Logs senden",
|
||||||
|
"Sending…": "Senden …",
|
||||||
|
"Settings": "Einstellungen",
|
||||||
|
"Share screen": "Bildschirm teilen",
|
||||||
|
"Show call inspector": "Anrufinspektor anzeigen",
|
||||||
|
"Sign in": "Anmelden",
|
||||||
|
"Sign out": "Abmelden",
|
||||||
|
"Spatial audio": "Räumliche Audiowiedergabe",
|
||||||
|
"Speaker": "Wiedergabegerät",
|
||||||
|
"Speaker {{n}}": "Wiedergabegerät {{n}}",
|
||||||
|
"Spotlight": "Rampenlicht",
|
||||||
|
"Stop sharing screen": "Beenden der Bildschirmfreigabe",
|
||||||
|
"Submit feedback": "Feedback senden",
|
||||||
|
"Submitting feedback…": "Feedback senden …",
|
||||||
|
"Take me Home": "Zurück zur Startseite",
|
||||||
|
"Talk over speaker": "Aktiven Sprecher verdrängen und sprechen",
|
||||||
|
"Talking…": "Sprechen …",
|
||||||
|
"Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.",
|
||||||
|
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
|
||||||
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Diese Website ist durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> sowie die <6> Nutzungsbedingungen </6> von Google.<9></9>Indem Du auf \"Registrieren\" klickst, stimmst Du unseren <12>Geschäftsbedingungen</12> zu",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Dadurch wird die Audiowiedergabe eines Sprechers so wiedergegeben, als käme er von der Stelle, an der das zugehörige Videobild auf dem Bildschirm positioniert ist (Experimentelle Funktion: Dies kann die Stabilität der Audiowiedergabe beeinträchtigen).",
|
||||||
|
"Turn off camera": "Kamera ausschalten",
|
||||||
|
"Turn on camera": "Kamera einschalten",
|
||||||
|
"Unmute microphone": "Mikrofon aktivieren",
|
||||||
|
"User ID": "Benutzer ID",
|
||||||
|
"User menu": "Benutzermenü",
|
||||||
|
"Username": "Benutzername",
|
||||||
|
"Version: {{version}}": "Version: {{version}}",
|
||||||
|
"Video": "Video",
|
||||||
|
"Video call": "Videoanruf",
|
||||||
|
"Video call name": "Name des Videoanrufs",
|
||||||
|
"Waiting for network": "Warte auf Netzwerk",
|
||||||
|
"Waiting for other participants…": "Warte auf weitere Teilnehmer …",
|
||||||
|
"Walkie-talkie call": "Walkie-talkie Anruf",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Web-Browser nicht unterstützt oder ist blockiert.",
|
||||||
|
"Yes, join call": "Ja, Anruf beitreten",
|
||||||
|
"You can't talk at the same time": "Du kannst nicht gleichzeitig sprechen",
|
||||||
|
"Your recent calls": "Deine lezten Anrufe",
|
||||||
|
"{{count}} people connected|one": "{{count}} Teilnehmer verbunden",
|
||||||
|
"{{count}} people connected|other": "{{count}} Teilnehmer verbunden",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, Dein Anruf wurde beendet",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} präsentiert",
|
||||||
|
"{{name}} is talking…": "{{name}} spricht …",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} – Walkie-Talkie-Anruf",
|
||||||
|
"Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.",
|
||||||
|
"Walkie-talkie call name": "Walkie-talkie Anruf Name",
|
||||||
|
"Sending debug logs…": "Sende Debug-Logs …"
|
||||||
|
}
|
||||||
133
public/locales/en-GB/app.json
Normal file
133
public/locales/en-GB/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"{{count}} people connected|one": "{{count}} person connected",
|
||||||
|
"{{count}} people connected|other": "{{count}} people connected",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
|
||||||
|
"{{name}} is presenting": "{{name}} is presenting",
|
||||||
|
"{{name}} is talking…": "{{name}} is talking…",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>",
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.",
|
||||||
|
"Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "By clicking \"Go\", you agree to our <2>Terms and conditions</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>",
|
||||||
|
"Call link copied": "Call link copied",
|
||||||
|
"Call type menu": "Call type menu",
|
||||||
|
"Camera": "Camera",
|
||||||
|
"Camera {{n}}": "Camera {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Camera/microphone permissions needed to join the call.",
|
||||||
|
"Change layout": "Change layout",
|
||||||
|
"Close": "Close",
|
||||||
|
"Confirm password": "Confirm password",
|
||||||
|
"Connection lost": "Connection lost",
|
||||||
|
"Copied!": "Copied!",
|
||||||
|
"Copy and share this call link": "Copy and share this call link",
|
||||||
|
"Copy call link and join later": "Copy call link and join later",
|
||||||
|
"Create account": "Create account",
|
||||||
|
"Debug log": "Debug log",
|
||||||
|
"Debug log request": "Debug log request",
|
||||||
|
"Description (optional)": "Description (optional)",
|
||||||
|
"Details": "Details",
|
||||||
|
"Developer": "Developer",
|
||||||
|
"Display name": "Display name",
|
||||||
|
"Download debug logs": "Download debug logs",
|
||||||
|
"Entering room…": "Entering room…",
|
||||||
|
"Exit full screen": "Exit full screen",
|
||||||
|
"Fetching group call timed out.": "Fetching group call timed out.",
|
||||||
|
"Freedom": "Freedom",
|
||||||
|
"Full screen": "Full screen",
|
||||||
|
"Go": "Go",
|
||||||
|
"Grid layout menu": "Grid layout menu",
|
||||||
|
"Having trouble? Help us fix it.": "Having trouble? Help us fix it.",
|
||||||
|
"Home": "Home",
|
||||||
|
"Include debug logs": "Include debug logs",
|
||||||
|
"Incompatible versions": "Incompatible versions",
|
||||||
|
"Incompatible versions!": "Incompatible versions!",
|
||||||
|
"Inspector": "Inspector",
|
||||||
|
"Invite": "Invite",
|
||||||
|
"Invite people": "Invite people",
|
||||||
|
"Join call": "Join call",
|
||||||
|
"Join call now": "Join call now",
|
||||||
|
"Join existing call?": "Join existing call?",
|
||||||
|
"Leave": "Leave",
|
||||||
|
"Loading room…": "Loading room…",
|
||||||
|
"Loading…": "Loading…",
|
||||||
|
"Local volume": "Local volume",
|
||||||
|
"Logging in…": "Logging in…",
|
||||||
|
"Login": "Login",
|
||||||
|
"Login to your account": "Login to your account",
|
||||||
|
"Microphone": "Microphone",
|
||||||
|
"Microphone {{n}}": "Microphone {{n}}",
|
||||||
|
"Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.",
|
||||||
|
"More": "More",
|
||||||
|
"More menu": "More menu",
|
||||||
|
"Mute microphone": "Mute microphone",
|
||||||
|
"No": "No",
|
||||||
|
"Not now, return to home screen": "Not now, return to home screen",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
||||||
|
"Password": "Password",
|
||||||
|
"Passwords must match": "Passwords must match",
|
||||||
|
"Press and hold spacebar to talk": "Press and hold spacebar to talk",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}",
|
||||||
|
"Press and hold to talk": "Press and hold to talk",
|
||||||
|
"Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}",
|
||||||
|
"Profile": "Profile",
|
||||||
|
"Recaptcha dismissed": "Recaptcha dismissed",
|
||||||
|
"Recaptcha not loaded": "Recaptcha not loaded",
|
||||||
|
"Register": "Register",
|
||||||
|
"Registering…": "Registering…",
|
||||||
|
"Release spacebar key to stop": "Release spacebar key to stop",
|
||||||
|
"Release to stop": "Release to stop",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"Return to home screen": "Return to home screen",
|
||||||
|
"Save": "Save",
|
||||||
|
"Saving…": "Saving…",
|
||||||
|
"Select an option": "Select an option",
|
||||||
|
"Send debug logs": "Send debug logs",
|
||||||
|
"Sending debug logs…": "Sending debug logs…",
|
||||||
|
"Sending…": "Sending…",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"Share screen": "Share screen",
|
||||||
|
"Show call inspector": "Show call inspector",
|
||||||
|
"Sign in": "Sign in",
|
||||||
|
"Sign out": "Sign out",
|
||||||
|
"Spatial audio": "Spatial audio",
|
||||||
|
"Speaker": "Speaker",
|
||||||
|
"Speaker {{n}}": "Speaker {{n}}",
|
||||||
|
"Spotlight": "Spotlight",
|
||||||
|
"Stop sharing screen": "Stop sharing screen",
|
||||||
|
"Submit feedback": "Submit feedback",
|
||||||
|
"Submitting feedback…": "Submitting feedback…",
|
||||||
|
"Take me Home": "Take me Home",
|
||||||
|
"Talk over speaker": "Talk over speaker",
|
||||||
|
"Talking…": "Talking…",
|
||||||
|
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.",
|
||||||
|
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
||||||
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)",
|
||||||
|
"Turn off camera": "Turn off camera",
|
||||||
|
"Turn on camera": "Turn on camera",
|
||||||
|
"Unmute microphone": "Unmute microphone",
|
||||||
|
"User ID": "User ID",
|
||||||
|
"User menu": "User menu",
|
||||||
|
"Username": "Username",
|
||||||
|
"Version: {{version}}": "Version: {{version}}",
|
||||||
|
"Video": "Video",
|
||||||
|
"Video call": "Video call",
|
||||||
|
"Video call name": "Video call name",
|
||||||
|
"Waiting for network": "Waiting for network",
|
||||||
|
"Waiting for other participants…": "Waiting for other participants…",
|
||||||
|
"Walkie-talkie call": "Walkie-talkie call",
|
||||||
|
"Walkie-talkie call name": "Walkie-talkie call name",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
|
||||||
|
"Yes, join call": "Yes, join call",
|
||||||
|
"You can't talk at the same time": "You can't talk at the same time",
|
||||||
|
"Your recent calls": "Your recent calls"
|
||||||
|
}
|
||||||
133
public/locales/fr/app.json
Normal file
133
public/locales/fr/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Créer un compte</0> Or <2>Accès invité</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Mince, une erreur est survenue.</0><1>Envoyer les journaux de débogage nous aidera à résoudre le problème.</1>",
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Pourquoi ne pas créer un mot de passe pour conserver votre compte ?</0><1>Vous pourrez garder votre nom et définir un avatar pour vos futurs appels</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Autorisez l’accès à votre caméra et microphone pour rejoindre l’appel.",
|
||||||
|
"Accept microphone permissions to join the call.": "Autorisez l’accès au microphone pour rejoindre l’appel.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Commencer » vous acceptez nos <2>conditions d’utilisation</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Rejoindre l’appel » vous acceptez nos <2>conditions d’utilisation</2>",
|
||||||
|
"Call link copied": "Lien de l’appel copié",
|
||||||
|
"Call type menu": "Menu de type d’appel",
|
||||||
|
"Camera": "Caméra",
|
||||||
|
"Camera {{n}}": "Caméra {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Accès à la caméra et au microphone requis pour rejoindre l’appel.",
|
||||||
|
"Change layout": "Changer la disposition",
|
||||||
|
"Close": "Fermer",
|
||||||
|
"Confirm password": "Confirmer le mot de passe",
|
||||||
|
"Connection lost": "Connexion interrompue",
|
||||||
|
"Copied!": "Copié !",
|
||||||
|
"Copy and share this call link": "Copier et partager le lien de cet appel",
|
||||||
|
"Copy call link and join later": "Copier le lien de cet appel et rejoindre plus tard",
|
||||||
|
"Create account": "Créer un compte",
|
||||||
|
"Debug log": "Journal de débogage",
|
||||||
|
"Debug log request": "Demande d’un journal de débogage",
|
||||||
|
"Description (optional)": "Description (facultatif)",
|
||||||
|
"Details": "Informations",
|
||||||
|
"Developer": "Développeur",
|
||||||
|
"Display name": "Nom d’affichage",
|
||||||
|
"Download debug logs": "Télécharger les journaux de débogage",
|
||||||
|
"Entering room…": "Entrée dans le salon…",
|
||||||
|
"Exit full screen": "Quitter le plein écran",
|
||||||
|
"Freedom": "Libre",
|
||||||
|
"Full screen": "Plein écran",
|
||||||
|
"Go": "Commencer",
|
||||||
|
"Grid layout menu": "Menu en grille",
|
||||||
|
"Having trouble? Help us fix it.": "Un problème ? Aidez nous à le résoudre.",
|
||||||
|
"Home": "Accueil",
|
||||||
|
"Include debug logs": "Inclure les journaux de débogage",
|
||||||
|
"Incompatible versions": "Versions incompatibles",
|
||||||
|
"Incompatible versions!": "Versions incompatibles !",
|
||||||
|
"Inspector": "Inspecteur",
|
||||||
|
"Invite people": "Inviter des gens",
|
||||||
|
"Join call": "Rejoindre l’appel",
|
||||||
|
"Join call now": "Rejoindre l’appel maintenant",
|
||||||
|
"Join existing call?": "Rejoindre un appel existant ?",
|
||||||
|
"Leave": "Partir",
|
||||||
|
"Loading room…": "Chargement du salon…",
|
||||||
|
"Loading…": "Chargement…",
|
||||||
|
"Local volume": "Volume local",
|
||||||
|
"Logging in…": "Connexion…",
|
||||||
|
"Login": "Connexion",
|
||||||
|
"Login to your account": "Connectez vous à votre compte",
|
||||||
|
"Microphone": "Microphone",
|
||||||
|
"Microphone permissions needed to join the call.": "Accès au microphone requis pour rejoindre l’appel.",
|
||||||
|
"Microphone {{n}}": "Microphone {{n}}",
|
||||||
|
"More": "Plus",
|
||||||
|
"More menu": "Menu plus",
|
||||||
|
"Mute microphone": "Couper le micro",
|
||||||
|
"No": "Non",
|
||||||
|
"Not now, return to home screen": "Pas maintenant, retourner à l’accueil",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Pas encore de compte ? <2>En créer un</2>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Des utilisateurs essayent de rejoindre cet appel à partir de versions incompatibles. Ces utilisateurs doivent rafraîchir la page dans leur navigateur : <1>{userLis}</1>",
|
||||||
|
"Password": "Mot de passe",
|
||||||
|
"Passwords must match": "Les mots de passe doivent correspondre",
|
||||||
|
"Press and hold spacebar to talk": "Appuyez et maintenez la barre d’espace enfoncée pour parler",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Appuyez et maintenez la barre d’espace enfoncée pour parler par dessus {{name}}",
|
||||||
|
"Press and hold to talk": "Appuyez et maintenez enfoncé pour parler",
|
||||||
|
"Press and hold to talk over {{name}}": "Appuyez et maintenez enfoncé pour parler par dessus {{name}}",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recaptcha dismissed": "Recaptcha refusé",
|
||||||
|
"Recaptcha not loaded": "Recaptcha non chargé",
|
||||||
|
"Register": "S’enregistrer",
|
||||||
|
"Registering…": "Enregistrement…",
|
||||||
|
"Release spacebar key to stop": "Relâcher la barre d’espace pour arrêter",
|
||||||
|
"Release to stop": "Relâcher pour arrêter",
|
||||||
|
"Remove": "Supprimer",
|
||||||
|
"Return to home screen": "Retour à l’accueil",
|
||||||
|
"Save": "Enregistrer",
|
||||||
|
"Saving…": "Enregistrement…",
|
||||||
|
"Select an option": "Sélectionnez une option",
|
||||||
|
"Send debug logs": "Envoyer les journaux de débogage",
|
||||||
|
"Sending…": "Envoi…",
|
||||||
|
"Settings": "Paramètres",
|
||||||
|
"Share screen": "Partage d’écran",
|
||||||
|
"Show call inspector": "Afficher l’inspecteur d’appel",
|
||||||
|
"Sign in": "Connexion",
|
||||||
|
"Sign out": "Déconnexion",
|
||||||
|
"Spatial audio": "Audio spatialisé",
|
||||||
|
"Spotlight": "Premier plan",
|
||||||
|
"Stop sharing screen": "Arrêter le partage d’écran",
|
||||||
|
"Submit feedback": "Envoyer des retours",
|
||||||
|
"Submitting feedback…": "Envoi des retours…",
|
||||||
|
"Take me Home": "Retouner à l’accueil",
|
||||||
|
"Talk over speaker": "Parler par dessus l’intervenant",
|
||||||
|
"Thanks! We'll get right on it.": "Merci ! Nous allons nous y attaquer.",
|
||||||
|
"This call already exists, would you like to join?": "Cet appel existe déjà, voulez-vous le rejoindre ?",
|
||||||
|
"{{name}} is presenting": "{{name}} est le présentateur",
|
||||||
|
"Fetching group call timed out.": "Échec de connexion à l’appel de groupe dans le temps imparti.",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} — Appel talkie-walkie",
|
||||||
|
"{{name}} is talking…": "{{name}} est en train de parler…",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, votre appel est désormais terminé",
|
||||||
|
"{{count}} people connected|other": "{{count}} personnes connectées",
|
||||||
|
"{{count}} people connected|one": "{{count}} personne connectée",
|
||||||
|
"Your recent calls": "Appels récents",
|
||||||
|
"You can't talk at the same time": "Vous ne pouvez pas parler en même temps",
|
||||||
|
"Yes, join call": "Oui, rejoindre l’appel",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC n’est pas pris en charge ou est bloqué par ce navigateur.",
|
||||||
|
"Walkie-talkie call name": "Nom de l’appel talkie-walkie",
|
||||||
|
"Walkie-talkie call": "Appel talkie-walkie",
|
||||||
|
"Waiting for other participants…": "En attente d’autres participants…",
|
||||||
|
"Waiting for network": "En attente du réseau",
|
||||||
|
"Video call name": "Nom de l’appel vidéo",
|
||||||
|
"Video call": "Appel vidéo",
|
||||||
|
"Video": "Vidéo",
|
||||||
|
"Version: {{version}}": "Version : {{version}}",
|
||||||
|
"Username": "Nom d’utilisateur",
|
||||||
|
"User menu": "Menu utilisateur",
|
||||||
|
"User ID": "Identifiant utilisateur",
|
||||||
|
"Unmute microphone": "Allumer le micro",
|
||||||
|
"Turn on camera": "Allumer la caméra",
|
||||||
|
"Turn off camera": "Couper la caméra",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Cela donnera l’impression que le son de l’intervenant provient de là où leur tuile est positionnée sur l’écran. (Fonctionnalité expérimentale : ceci pourrait avoir un impact sur la stabilité du son.)",
|
||||||
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions d’utilisation</6> de Google s’appliquent.<9></9>En cliquant sur « S’enregistrer » vous acceptez également nos <12>conditions d’utilisation</12>",
|
||||||
|
"Talking…": "Vous parlez…",
|
||||||
|
"Speaker {{n}}": "Intervenant {{n}}",
|
||||||
|
"Speaker": "Intervenant",
|
||||||
|
"Invite": "Inviter",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
|
||||||
|
"Sending debug logs…": "Envoi des journaux de débogage…"
|
||||||
|
}
|
||||||
132
public/locales/id/app.json
Normal file
132
public/locales/id/app.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Sudah punya akun?</0><1><0>Masuk</0> Atau <2>Akses sebagai tamu</2></1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Buat akun</0> Atau <2>Akses sebagai tamu</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Aduh, ada yang salah.</0><1>Mengirimkan catatan pengawakutuan akan membantu kami melacak masalahnya.</1>",
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Kenapa tidak selesaikan dengan mengatur sebuah kata sandi untuk menjaga akun Anda?</0><1>Anda akan dapat tetap menggunakan nama Anda dan atur sebuah avatar untuk digunakan dalam panggilan di masa mendatang</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Terima izin kamera/mikrofon untuk bergabung ke panggilan.",
|
||||||
|
"Accept microphone permissions to join the call.": "Terima izin mikrofon untuk bergabung ke panggilan.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan</2> kami",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan</2> kami",
|
||||||
|
"Call link copied": "Tautan panggilan disalin",
|
||||||
|
"Call type menu": "Menu jenis panggilan",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Camera {{n}}": "Kamera {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Izin kamera/mikrofon dibutuhkan untuk bergabung ke panggilan.",
|
||||||
|
"Change layout": "Ubah tata letak",
|
||||||
|
"Close": "Tutup",
|
||||||
|
"Confirm password": "Konfirmasi kata sandi",
|
||||||
|
"Connection lost": "Koneksi hilang",
|
||||||
|
"Copied!": "Disalin!",
|
||||||
|
"Copy and share this call link": "Salin dan bagikan tautan panggilan ini",
|
||||||
|
"Copy call link and join later": "Salin tautan panggilan dan bergabung nanti",
|
||||||
|
"Create account": "Buat akun",
|
||||||
|
"Debug log": "Catatan pengawakutuan",
|
||||||
|
"Debug log request": "Permintaan catatan pengawakutuan",
|
||||||
|
"Description (optional)": "Deskripsi (opsional)",
|
||||||
|
"Details": "Detail",
|
||||||
|
"Developer": "Pengembang",
|
||||||
|
"Display name": "Nama tampilan",
|
||||||
|
"Download debug logs": "Unduh catatan pengawakutuan",
|
||||||
|
"Entering room…": "Memasuki ruangan…",
|
||||||
|
"Exit full screen": "Keluar dari layar penuh",
|
||||||
|
"Fetching group call timed out.": "Waktu pendapatan panggilan grup habis.",
|
||||||
|
"Freedom": "Bebas",
|
||||||
|
"Full screen": "Layar penuh",
|
||||||
|
"Go": "Bergabung",
|
||||||
|
"Grid layout menu": "Menu tata letak kisi",
|
||||||
|
"Having trouble? Help us fix it.": "Mengalami masalah? Bantu kami memperbaikinya.",
|
||||||
|
"Home": "Beranda",
|
||||||
|
"Include debug logs": "Termasuk catatan pengawakutuan",
|
||||||
|
"Incompatible versions": "Versi tidak kompatibel",
|
||||||
|
"Incompatible versions!": "Versi tidak kompatibel!",
|
||||||
|
"Inspector": "Inspektur",
|
||||||
|
"Invite": "Undang",
|
||||||
|
"Invite people": "Undang orang",
|
||||||
|
"Join call": "Bergabung ke panggilan",
|
||||||
|
"Join call now": "Bergabung ke panggilan sekarang",
|
||||||
|
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
|
||||||
|
"Leave": "Keluar",
|
||||||
|
"Loading room…": "Memuat ruangan…",
|
||||||
|
"Loading…": "Memuat…",
|
||||||
|
"Local volume": "Volume lokal",
|
||||||
|
"Logging in…": "Memasuki…",
|
||||||
|
"Login": "Masuk",
|
||||||
|
"Login to your account": "Masuk ke akun Anda",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Microphone permissions needed to join the call.": "Izin mikrofon dibutuhkan untuk bergabung ke panggilan ini.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"More": "Lainnya",
|
||||||
|
"More menu": "Menu lainnya",
|
||||||
|
"Mute microphone": "Bisukan mikrofon",
|
||||||
|
"No": "Tidak",
|
||||||
|
"Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Belum terdaftar? <2>Buat sebuah akun</2>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Pengguna lain sedang mencoba bergabung ke panggilan ini dari versi yang tidak kompatibel. Pengguna berikut seharusnya memastikan bahwa mereka telah memuat ulang peramban mereka: <1>{userLis}</1>",
|
||||||
|
"Password": "Kata sandi",
|
||||||
|
"Passwords must match": "Kata sandi harus cocok",
|
||||||
|
"Press and hold spacebar to talk": "Tekan dan tahan bilah spasi untuk berbicara",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Tekan dan tahan bilah spasi untuk berbicara pada {{name}}",
|
||||||
|
"Press and hold to talk": "Tekan dan tahan untuk berbicara",
|
||||||
|
"Press and hold to talk over {{name}}": "Tekan dan tahan untuk berbicara pada {{name}}",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recaptcha dismissed": "Recaptcha ditutup",
|
||||||
|
"Recaptcha not loaded": "Recaptcha tidak dimuat",
|
||||||
|
"Register": "Daftar",
|
||||||
|
"Registering…": "Mendaftarkan…",
|
||||||
|
"Release spacebar key to stop": "Lepaskan bilah spasi untuk berhenti",
|
||||||
|
"Release to stop": "Lepaskan untuk berhenti",
|
||||||
|
"Remove": "Hapus",
|
||||||
|
"Return to home screen": "Kembali ke layar beranda",
|
||||||
|
"Save": "Simpan",
|
||||||
|
"Saving…": "Menyimpan…",
|
||||||
|
"Select an option": "Pilih sebuah opsi",
|
||||||
|
"Send debug logs": "Kirim catatan pengawakutuan",
|
||||||
|
"Sending…": "Mengirimkan…",
|
||||||
|
"Settings": "Pengaturan",
|
||||||
|
"Share screen": "Bagikan layar",
|
||||||
|
"Show call inspector": "Tampilkan inspektur panggilan",
|
||||||
|
"Sign in": "Masuk",
|
||||||
|
"Sign out": "Keluar",
|
||||||
|
"Spatial audio": "Audio spasial",
|
||||||
|
"Speaker": "Pembicara",
|
||||||
|
"Speaker {{n}}": "Pembicara {{n}}",
|
||||||
|
"Spotlight": "Sorotan",
|
||||||
|
"Stop sharing screen": "Berhenti membagikan layar",
|
||||||
|
"Submit feedback": "Kirim masukan",
|
||||||
|
"Submitting feedback…": "Mengirimkan masukan…",
|
||||||
|
"Take me Home": "Bawa saya ke Beranda",
|
||||||
|
"Talk over speaker": "Bicara pada pembicara",
|
||||||
|
"Talking…": "Berbicara…",
|
||||||
|
"Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.",
|
||||||
|
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
|
||||||
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan</12> kami",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Ini akan membuat suara pembicara seolah-olah berasal dari tempat ubin mereka diposisikan di layar. (Fitur uji coba: ini dapat memengaruhi stabilitas audio.)",
|
||||||
|
"Turn off camera": "Matikan kamera",
|
||||||
|
"Turn on camera": "Nyalakan kamera",
|
||||||
|
"Unmute microphone": "Suarakan mikrofon",
|
||||||
|
"User ID": "ID pengguna",
|
||||||
|
"User menu": "Menu pengguna",
|
||||||
|
"Username": "Nama pengguna",
|
||||||
|
"Version: {{version}}": "Versi: {{version}}",
|
||||||
|
"Video": "Video",
|
||||||
|
"Video call": "Panggilan video",
|
||||||
|
"Video call name": "Nama panggilan video",
|
||||||
|
"Waiting for network": "Menunggu jaringan",
|
||||||
|
"Waiting for other participants…": "Menunggu peserta lain…",
|
||||||
|
"Walkie-talkie call": "Panggilan protofon",
|
||||||
|
"Walkie-talkie call name": "Nama panggilan protofon",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC tidak didukung atau diblokir di peramban ini.",
|
||||||
|
"Yes, join call": "Ya, bergabung ke panggilan",
|
||||||
|
"You can't talk at the same time": "Anda tidak dapat berbicara pada waktu yang sama",
|
||||||
|
"Your recent calls": "Panggilan Anda terkini",
|
||||||
|
"{{count}} people connected|one": "{{count}} orang terhubung",
|
||||||
|
"{{count}} people connected|other": "{{count}} orang terhubung",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, panggilan Anda sekarang telah berakhir",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} sedang mempresentasi",
|
||||||
|
"{{name}} is talking…": "{{name}} sedang berbicara…",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Panggilan protofon"
|
||||||
|
}
|
||||||
12
public/locales/ko/app.json
Normal file
12
public/locales/ko/app.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "",
|
||||||
|
"{{count}} people connected|one": "{{count}}명 연결됨",
|
||||||
|
"{{count}} people connected|other": "{{count}}명 연결됨",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}님, 전화가 종료되었습니다",
|
||||||
|
"{{names}}, {{name}}": "{{names}}님, {{name}}님",
|
||||||
|
"{{name}} is presenting": "{{name}}님이 발표 중",
|
||||||
|
"{{name}} is talking…": "{{name}}님이 말하는 중…",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - 워키토키 전화"
|
||||||
|
}
|
||||||
133
public/locales/ru/app.json
Normal file
133
public/locales/ru/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"Register": "Зарегистрироваться",
|
||||||
|
"Saving…": "Сохранение…",
|
||||||
|
"Registering…": "Регистрация…",
|
||||||
|
"Logging in…": "Вход…",
|
||||||
|
"Entering room…": "Вход в комнату…",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"Waiting for other participants…": "Ожидание других участников…",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Эта функция балансирует звук к расположению плитки на экране. (Экспериментальная функция: может повлиять на стабильность аудио.)",
|
||||||
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Этот сайт защищён ReCAPTCHA от Google, ознакомьтесь с их <2>Политикой конфиденциальности</2> и <6>Пользовательским соглашением</6>.<9></9>Нажимая \"Зарегистрироваться\", вы также принимаете наши <12>Положения и условия</12>.",
|
||||||
|
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
|
||||||
|
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
|
||||||
|
"Talking…": "Говорите…",
|
||||||
|
"Submitting feedback…": "Отправка отзыва…",
|
||||||
|
"Submit feedback": "Отправить отзыв",
|
||||||
|
"Sending debug logs…": "Отправка журнала отладки…",
|
||||||
|
"Select an option": "Выберите вариант",
|
||||||
|
"Release to stop": "Отпустите, чтобы прекратить вещание",
|
||||||
|
"Release spacebar key to stop": "Чтобы прекратить вещание, отпустите [Пробел]",
|
||||||
|
"Press and hold to talk over {{name}}": "Зажмите, чтобы говорить поверх участника {{name}}",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Чтобы говорить поверх участника {{name}}, нажмите и удерживайте [Пробел]",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Другие пользователи пытаются присоединиться с неподдерживаемых версий программы. Этим участникам надо перезагрузить браузер: <1>{userLis}</1>",
|
||||||
|
"Grid layout menu": "Меню \"Расположение сеткой\"",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Присоединиться сейчас\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>",
|
||||||
|
"Your recent calls": "Ваши недавние звонки",
|
||||||
|
"You can't talk at the same time": "Вы не можете говорить одновременно",
|
||||||
|
"Yes, join call": "Да, присоединиться",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не поддерживается или заблокирован в этом браузере.",
|
||||||
|
"Walkie-talkie call name": "Название звонка-рации",
|
||||||
|
"Walkie-talkie call": "Звонок-рация",
|
||||||
|
"Waiting for network": "Ожидание сети",
|
||||||
|
"Video call name": "Название видео-звонка",
|
||||||
|
"Video call": "Видео-звонок",
|
||||||
|
"Video": "Видео",
|
||||||
|
"Version: {{version}}": "Версия: {{version}}",
|
||||||
|
"Username": "Имя пользователя",
|
||||||
|
"User menu": "Меню пользователя",
|
||||||
|
"User ID": "ID пользователя",
|
||||||
|
"Unmute microphone": "Включить микрофон",
|
||||||
|
"Turn on camera": "Включить камеру",
|
||||||
|
"Turn off camera": "Отключить камеру",
|
||||||
|
"Talk over speaker": "Говорить через динамик",
|
||||||
|
"Take me Home": "Перейти в Начало",
|
||||||
|
"Stop sharing screen": "Остановить показ экрана",
|
||||||
|
"Spotlight": "Внимание",
|
||||||
|
"Speaker {{n}}": "Динамик {{n}}",
|
||||||
|
"Speaker": "Динамик",
|
||||||
|
"Spatial audio": "Пространственное аудио",
|
||||||
|
"Sign out": "Выйти",
|
||||||
|
"Sign in": "Войти",
|
||||||
|
"Show call inspector": "Показать инспектор",
|
||||||
|
"Share screen": "Поделиться экраном",
|
||||||
|
"Settings": "Настройки",
|
||||||
|
"Sending…": "Отправка…",
|
||||||
|
"Local volume": "Местная громкость",
|
||||||
|
"Call type menu": "Меню \"Тип звонка\"",
|
||||||
|
"More menu": "Полное меню",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Звонок-рация",
|
||||||
|
"Include debug logs": "Приложить журнал отладки",
|
||||||
|
"Download debug logs": "Скачать журнал отладки",
|
||||||
|
"Debug log request": "Запрос журнала отладки",
|
||||||
|
"Debug log": "Журнал отладки",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ой, что-то пошло не так.</0><1>Отправив журнал отладки, вы поможете нам найти проблемный участок.</1>",
|
||||||
|
"Send debug logs": "Отправить журнал отладки",
|
||||||
|
"Save": "Сохранить",
|
||||||
|
"Return to home screen": "Вернуться в начало",
|
||||||
|
"Remove": "Удалить",
|
||||||
|
"Recaptcha not loaded": "Невозможно начать проверку",
|
||||||
|
"Recaptcha dismissed": "Проверка не пройдена",
|
||||||
|
"Profile": "Профиль",
|
||||||
|
"Press and hold to talk": "Зажмите, чтобы говорить",
|
||||||
|
"Press and hold spacebar to talk": "Чтобы говорить, нажмите и удерживайте [Пробел]",
|
||||||
|
"Passwords must match": "Пароли должны совпадать",
|
||||||
|
"Password": "Пароль",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>",
|
||||||
|
"Not now, return to home screen": "Не сейчас, вернитесь в начало",
|
||||||
|
"No": "Нет",
|
||||||
|
"Mute microphone": "Отключить микрофон",
|
||||||
|
"More": "Больше",
|
||||||
|
"Microphone permissions needed to join the call.": "Нужно разрешение на доступ к микрофону для присоединения к звонку.",
|
||||||
|
"Microphone {{n}}": "Микрофон {{n}}",
|
||||||
|
"Microphone": "Микрофон",
|
||||||
|
"Login to your account": "Войдите в свой аккаунт",
|
||||||
|
"Login": "Вход",
|
||||||
|
"Loading…": "Загрузка…",
|
||||||
|
"Loading room…": "Загрузка комнаты…",
|
||||||
|
"Leave": "Покинуть",
|
||||||
|
"Join existing call?": "Присоединиться к существующему звонку?",
|
||||||
|
"Join call now": "Присоединиться сейчас",
|
||||||
|
"Join call": "Присоединиться",
|
||||||
|
"Invite people": "Пригласить участников",
|
||||||
|
"Invite": "Пригласить",
|
||||||
|
"Inspector": "Инспектор",
|
||||||
|
"Incompatible versions!": "Несовместимые версии!",
|
||||||
|
"Incompatible versions": "Несовместимые версии",
|
||||||
|
"Home": "Начало",
|
||||||
|
"Having trouble? Help us fix it.": "Есть проблема? Помогите нам её устранить.",
|
||||||
|
"Go": "Далее",
|
||||||
|
"Full screen": "Полноэкранный режим",
|
||||||
|
"Freedom": "Свобода",
|
||||||
|
"Fetching group call timed out.": "Истекло время ожидания для группового звонка.",
|
||||||
|
"Exit full screen": "Выйти из полноэкранного режима",
|
||||||
|
"Display name": "Видимое имя",
|
||||||
|
"Developer": "Разработчик",
|
||||||
|
"Details": "Подробности",
|
||||||
|
"Description (optional)": "Описание (необязательно)",
|
||||||
|
"Create account": "Создать аккаунт",
|
||||||
|
"Copy call link and join later": "Скопировать ссылку и присоединиться позже",
|
||||||
|
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
|
||||||
|
"Copied!": "Скопировано!",
|
||||||
|
"Connection lost": "Соединение потеряно",
|
||||||
|
"Confirm password": "Подтвердите пароль",
|
||||||
|
"Close": "Закрыть",
|
||||||
|
"Change layout": "Изменить расположение",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Нужны разрешения на доступ к камере/микрофону для присоединения к звонку.",
|
||||||
|
"Camera {{n}}": "Камера {{n}}",
|
||||||
|
"Camera": "Камера",
|
||||||
|
"Call link copied": "Ссылка на звонок скопирована",
|
||||||
|
"Avatar": "Аватар",
|
||||||
|
"Audio": "Аудио",
|
||||||
|
"Accept microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к микрофону.",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к камере/микрофону.",
|
||||||
|
"{{name}} is talking…": "{{name}} говорит…",
|
||||||
|
"{{name}} is presenting": "{{name}} показывает",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, ваш звонок завершён",
|
||||||
|
"{{count}} people connected|other": "{{count}} подключилось",
|
||||||
|
"{{count}} people connected|one": "{{count}} подключился"
|
||||||
|
}
|
||||||
103
public/locales/tr/app.json
Normal file
103
public/locales/tr/app.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Hesabınızı tutmak için niye bir parola açmıyorsunuz?</0><1>Böylece ileriki aramalarda adınızı ve avatarınızı kullanabileceksiniz</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Aramaya katılmanız için kamera/mikrofon erişimine izin verin.",
|
||||||
|
"Accept microphone permissions to join the call.": "Aramaya katılmak için mikrofon erişim izni verin.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.",
|
||||||
|
"Audio": "Ses",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "\"Git\"e tıklayarak,<2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
|
||||||
|
"Call link copied": "Arama bağlantısı kopyalandı",
|
||||||
|
"Call type menu": "Arama tipi menüsü",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Camera {{n}}": "{{n}}. kamera",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Aramaya katılmak için kamera/mikrofon izinleri gerek.",
|
||||||
|
"Change layout": "Yerleşimi değiştir",
|
||||||
|
"Close": "Kapat",
|
||||||
|
"Confirm password": "Parolayı tekrar edin",
|
||||||
|
"Connection lost": "Bağlantı koptu",
|
||||||
|
"Copied!": "Kopyalandı",
|
||||||
|
"Copy and share this call link": "Arama bağlantısını kopyala ve paylaş",
|
||||||
|
"Copy call link and join later": "Sonra katılmak üzere bağlantıyı kopyala",
|
||||||
|
"Create account": "Hesap aç",
|
||||||
|
"Debug log": "Hata ayıklama kütüğü",
|
||||||
|
"Debug log request": "Hata ayıklama kütük istemi",
|
||||||
|
"Description (optional)": "Tanım (isteğe bağlı)",
|
||||||
|
"Details": "Ayrıntı",
|
||||||
|
"Developer": "Geliştirici",
|
||||||
|
"Display name": "Ekran adı",
|
||||||
|
"Download debug logs": "Hata ayıklama kütüğünü indir",
|
||||||
|
"Entering room…": "Odaya giriliyor…",
|
||||||
|
"Exit full screen": "Tam ekranı terk et",
|
||||||
|
"Fetching group call timed out.": "Grup çağrısı zaman aşımına uğradı.",
|
||||||
|
"Freedom": "Özgürlük",
|
||||||
|
"Full screen": "Tam ekran",
|
||||||
|
"Go": "Git",
|
||||||
|
"Grid layout menu": "Izgara plan menü",
|
||||||
|
"Having trouble? Help us fix it.": "Sorun mu var? Çözmemize yardım edin.",
|
||||||
|
"Home": "Ev",
|
||||||
|
"Include debug logs": "Hata ayıklama kütüğünü dahil et",
|
||||||
|
"Incompatible versions": "Uyumsuz sürümler",
|
||||||
|
"Incompatible versions!": "Sürüm uyumsuz!",
|
||||||
|
"Inspector": "Denetçi",
|
||||||
|
"Invite people": "Kişileri davet et",
|
||||||
|
"Join call": "Aramaya katıl",
|
||||||
|
"Join call now": "Aramaya katıl",
|
||||||
|
"Join existing call?": "Mevcut aramaya katıl?",
|
||||||
|
"Leave": "Çık",
|
||||||
|
"Loading room…": "Oda yükleniyor…",
|
||||||
|
"Loading…": "Yükleniyor…",
|
||||||
|
"Local volume": "Yerel ses seviyesi",
|
||||||
|
"Logging in…": "Giriliyor…",
|
||||||
|
"Login": "Gir",
|
||||||
|
"Login to your account": "Hesabınıza girin",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Microphone permissions needed to join the call.": "Aramaya katılmak için mikrofon erişim izni gerek.",
|
||||||
|
"Microphone {{n}}": "{{n}}. mikrofon",
|
||||||
|
"More": "Daha",
|
||||||
|
"More menu": "Daha fazla",
|
||||||
|
"Mute microphone": "Mikrofonu kapat",
|
||||||
|
"No": "Hayır",
|
||||||
|
"Not now, return to home screen": "Şimdi değil, ev ekranına dön",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Başka kullanıcılar uyumsuz sürümden katılmaya çalışıyorlar. <1>{userLis}</1> tarayıcılarını mutlaka tazelemeliler.",
|
||||||
|
"Password": "Parola",
|
||||||
|
"Passwords must match": "Parolalar aynı olmalı",
|
||||||
|
"Press and hold spacebar to talk": "Konuşmak için boşluk çubuğunu basılı tutun",
|
||||||
|
"Press and hold to talk": "Konuşmak için basılı tutun",
|
||||||
|
"Recaptcha dismissed": "reCAPTCHA atlandı",
|
||||||
|
"Recaptcha not loaded": "reCAPTCHA yüklenmedi",
|
||||||
|
"Register": "Kaydol",
|
||||||
|
"Registering…": "Kaydediyor…",
|
||||||
|
"Release spacebar key to stop": "Kesmek için boşluk tuşunu bırakın",
|
||||||
|
"Release to stop": "Kesmek için bırakın",
|
||||||
|
"Remove": "Çıkar",
|
||||||
|
"Return to home screen": "Ev ekranına geri dön",
|
||||||
|
"Save": "Kaydet",
|
||||||
|
"Saving…": "Kaydediliyor…",
|
||||||
|
"Select an option": "Bir seçenek seç",
|
||||||
|
"Send debug logs": "Hata ayıklama kütüğünü gönder",
|
||||||
|
"Sending…": "Gönderiliyor…",
|
||||||
|
"Settings": "Ayarlar",
|
||||||
|
"Share screen": "Ekran paylaş",
|
||||||
|
"Show call inspector": "Arama denetçisini göster",
|
||||||
|
"Sign in": "Gir",
|
||||||
|
"Sign out": "Çık",
|
||||||
|
"Spatial audio": "Uzamsal ses",
|
||||||
|
"Stop sharing screen": "Ekran paylaşmayı terk et",
|
||||||
|
"Submit feedback": "Geri bildirim ver",
|
||||||
|
"Submitting feedback…": "Geri bildirimler gönderiliyor…",
|
||||||
|
"Take me Home": "Ev ekranına gir",
|
||||||
|
"Talking…": "Konuşuyor…",
|
||||||
|
"Thanks! We'll get right on it.": "Sağol! Bununla ilgileneceğiz.",
|
||||||
|
"This call already exists, would you like to join?": "Bu arama zaten var, katılmak ister misiniz?",
|
||||||
|
"{{count}} people connected|one": "{{count}} kişi bağlı",
|
||||||
|
"{{count}} people connected|other": "{{count}} kişi bağlı",
|
||||||
|
"{{displayName}}, your call is now ended": "Aramanız bitti, {{displayName]}!",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} sunuyor",
|
||||||
|
"{{name}} is talking…": "{{name}} konuşuyor…",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoop, bir şeyler yanlış.</0><1>Hata ayıklama kütüğünü göndermek sorunu incelememize yardımcı olur.</1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Hesap oluştur</0> yahut <2>Konuk olarak gir</2>",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Mevcut hesabınız mı var?</0><1><0>Gir</0> yahut <2>Konuk girişi</2></1>"
|
||||||
|
}
|
||||||
1
public/locales/uk/app.json
Normal file
1
public/locales/uk/app.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -5,6 +5,7 @@ set -ex
|
|||||||
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
||||||
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
||||||
export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
|
export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
|
||||||
|
export VITE_PRODUCT_NAME="Element Call"
|
||||||
|
|
||||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||||
cd matrix-js-sdk
|
cd matrix-js-sdk
|
||||||
@@ -12,22 +13,11 @@ git checkout robertlong/group-call
|
|||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
yarn link
|
yarn link
|
||||||
cd ..
|
|
||||||
|
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
cd ../element-call
|
||||||
cd matrix-react-sdk
|
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn install
|
|
||||||
yarn run build
|
|
||||||
yarn link
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
cd matrix-video-chat
|
|
||||||
|
|
||||||
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
|
|||||||
30
src/@types/global.d.ts
vendored
Normal file
30
src/@types/global.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "matrix-js-sdk/src/@types/global";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||||
|
OLM_OPTIONS: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript doesn't know about the experimental setSinkId method, so we
|
||||||
|
// declare it ourselves
|
||||||
|
interface MediaElement extends HTMLVideoElement {
|
||||||
|
setSinkId: (id: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/@types/modules.d.ts
vendored
Normal file
2
src/@types/modules.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
63
src/App.jsx
63
src/App.jsx
@@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2021 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { OverlayProvider } from "@react-aria/overlays";
|
|
||||||
import { HomePage } from "./home/HomePage";
|
|
||||||
import { LoginPage } from "./auth/LoginPage";
|
|
||||||
import { RegisterPage } from "./auth/RegisterPage";
|
|
||||||
import { RoomPage } from "./room/RoomPage";
|
|
||||||
import { RoomRedirect } from "./room/RoomRedirect";
|
|
||||||
import { ClientProvider } from "./ClientContext";
|
|
||||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
|
||||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
|
||||||
|
|
||||||
export default function App({ history }) {
|
|
||||||
usePageFocusStyle();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router history={history}>
|
|
||||||
<ClientProvider>
|
|
||||||
<OverlayProvider>
|
|
||||||
<Switch>
|
|
||||||
<SentryRoute exact path="/">
|
|
||||||
<HomePage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute exact path="/login">
|
|
||||||
<LoginPage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute exact path="/register">
|
|
||||||
<RegisterPage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute path="/room/:roomId?">
|
|
||||||
<RoomPage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute path="/inspector">
|
|
||||||
<SequenceDiagramViewerPage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute path="*">
|
|
||||||
<RoomRedirect />
|
|
||||||
</SentryRoute>
|
|
||||||
</Switch>
|
|
||||||
</OverlayProvider>
|
|
||||||
</ClientProvider>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
78
src/App.tsx
Normal file
78
src/App.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Suspense } from "react";
|
||||||
|
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { OverlayProvider } from "@react-aria/overlays";
|
||||||
|
|
||||||
|
import { HomePage } from "./home/HomePage";
|
||||||
|
import { LoginPage } from "./auth/LoginPage";
|
||||||
|
import { RegisterPage } from "./auth/RegisterPage";
|
||||||
|
import { RoomPage } from "./room/RoomPage";
|
||||||
|
import { RoomRedirect } from "./room/RoomRedirect";
|
||||||
|
import { ClientProvider } from "./ClientContext";
|
||||||
|
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||||
|
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||||
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
|
import { CrashView } from "./FullScreenView";
|
||||||
|
|
||||||
|
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
history: History;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ history }: AppProps) {
|
||||||
|
usePageFocusStyle();
|
||||||
|
|
||||||
|
const errorPage = <CrashView />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router history={history}>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ClientProvider>
|
||||||
|
<InspectorContextProvider>
|
||||||
|
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||||
|
<OverlayProvider>
|
||||||
|
<Switch>
|
||||||
|
<SentryRoute exact path="/">
|
||||||
|
<HomePage />
|
||||||
|
</SentryRoute>
|
||||||
|
<SentryRoute exact path="/login">
|
||||||
|
<LoginPage />
|
||||||
|
</SentryRoute>
|
||||||
|
<SentryRoute exact path="/register">
|
||||||
|
<RegisterPage />
|
||||||
|
</SentryRoute>
|
||||||
|
<SentryRoute path="/room/:roomId?">
|
||||||
|
<RoomPage />
|
||||||
|
</SentryRoute>
|
||||||
|
<SentryRoute path="/inspector">
|
||||||
|
<SequenceDiagramViewerPage />
|
||||||
|
</SentryRoute>
|
||||||
|
<SentryRoute path="*">
|
||||||
|
<RoomRedirect />
|
||||||
|
</SentryRoute>
|
||||||
|
</Switch>
|
||||||
|
</OverlayProvider>
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
</InspectorContextProvider>
|
||||||
|
</ClientProvider>
|
||||||
|
</Suspense>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./Avatar.module.css";
|
|
||||||
|
|
||||||
const backgroundColors = [
|
|
||||||
"#5C56F5",
|
|
||||||
"#03B381",
|
|
||||||
"#368BD6",
|
|
||||||
"#AC3BA8",
|
|
||||||
"#E64F7A",
|
|
||||||
"#FF812D",
|
|
||||||
"#2DC2C5",
|
|
||||||
"#74D12C",
|
|
||||||
];
|
|
||||||
|
|
||||||
function hashStringToArrIndex(str, arrLength) {
|
|
||||||
let sum = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
sum += str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum % arrLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Avatar({
|
|
||||||
bgKey,
|
|
||||||
src,
|
|
||||||
fallback,
|
|
||||||
size,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const backgroundColor = useMemo(() => {
|
|
||||||
const index = hashStringToArrIndex(
|
|
||||||
bgKey || fallback || src,
|
|
||||||
backgroundColors.length
|
|
||||||
);
|
|
||||||
return backgroundColors[index];
|
|
||||||
}, [bgKey, src, fallback]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.avatar, styles[size || "md"], className)}
|
|
||||||
style={{ backgroundColor, ...style }}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{src ? (
|
|
||||||
<img src={src} />
|
|
||||||
) : typeof fallback === "string" ? (
|
|
||||||
<span>{fallback}</span>
|
|
||||||
) : (
|
|
||||||
fallback
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
.avatar {
|
.avatar {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #ffffff;
|
color: var(--primary-content);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar svg * {
|
.avatar svg * {
|
||||||
fill: #ffffff;
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar span {
|
.avatar span {
|
||||||
@@ -56,4 +56,5 @@
|
|||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
border-radius: 90px;
|
border-radius: 90px;
|
||||||
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/Avatar.tsx
Normal file
115
src/Avatar.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useMemo, CSSProperties } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import { getAvatarUrl } from "./matrix-utils";
|
||||||
|
import { useClient } from "./ClientContext";
|
||||||
|
import styles from "./Avatar.module.css";
|
||||||
|
|
||||||
|
const backgroundColors = [
|
||||||
|
"#5C56F5",
|
||||||
|
"#03B381",
|
||||||
|
"#368BD6",
|
||||||
|
"#AC3BA8",
|
||||||
|
"#E64F7A",
|
||||||
|
"#FF812D",
|
||||||
|
"#2DC2C5",
|
||||||
|
"#74D12C",
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum Size {
|
||||||
|
XS = "xs",
|
||||||
|
SM = "sm",
|
||||||
|
MD = "md",
|
||||||
|
LG = "lg",
|
||||||
|
XL = "xl",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sizes = new Map([
|
||||||
|
[Size.XS, 22],
|
||||||
|
[Size.SM, 32],
|
||||||
|
[Size.MD, 36],
|
||||||
|
[Size.LG, 42],
|
||||||
|
[Size.XL, 90],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function hashStringToArrIndex(str: string, arrLength: number) {
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
sum += str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum % arrLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
|
||||||
|
src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
|
||||||
|
|
||||||
|
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
bgKey?: string;
|
||||||
|
src?: string;
|
||||||
|
size?: Size | number;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
fallback: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar: React.FC<Props> = ({
|
||||||
|
bgKey,
|
||||||
|
src,
|
||||||
|
fallback,
|
||||||
|
size = Size.MD,
|
||||||
|
className,
|
||||||
|
style = {},
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { client } = useClient();
|
||||||
|
|
||||||
|
const [sizeClass, sizePx, sizeStyle] = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(Size).includes(size as Size)
|
||||||
|
? [styles[size as string], sizes.get(size as Size), {}]
|
||||||
|
: [
|
||||||
|
null,
|
||||||
|
size as number,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size,
|
||||||
|
fontSize: Math.round((size as number) / 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[size]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedSrc = useMemo(
|
||||||
|
() => resolveAvatarSrc(client, src, sizePx),
|
||||||
|
[client, src, sizePx]
|
||||||
|
);
|
||||||
|
|
||||||
|
const backgroundColor = useMemo(() => {
|
||||||
|
const index = hashStringToArrIndex(
|
||||||
|
bgKey || fallback || src || "",
|
||||||
|
backgroundColors.length
|
||||||
|
);
|
||||||
|
return backgroundColors[index];
|
||||||
|
}, [bgKey, src, fallback]);
|
||||||
|
|
||||||
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.avatar, sizeClass, className)}
|
||||||
|
style={{ backgroundColor, ...sizeStyle, ...style }}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{resolvedSrc ? (
|
||||||
|
<img src={resolvedSrc} />
|
||||||
|
) : typeof fallback === "string" ? (
|
||||||
|
<span>{fallback}</span>
|
||||||
|
) : (
|
||||||
|
fallback
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2021 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, {
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
createContext,
|
|
||||||
useMemo,
|
|
||||||
useContext,
|
|
||||||
} from "react";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { ErrorView } from "./FullScreenView";
|
|
||||||
import { initClient, defaultHomeserver } from "./matrix-utils";
|
|
||||||
|
|
||||||
const ClientContext = createContext();
|
|
||||||
|
|
||||||
export function ClientProvider({ children }) {
|
|
||||||
const history = useHistory();
|
|
||||||
const [
|
|
||||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
|
||||||
setState,
|
|
||||||
] = useState({
|
|
||||||
loading: true,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
client: undefined,
|
|
||||||
userName: null,
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function restore() {
|
|
||||||
try {
|
|
||||||
const authStore = localStorage.getItem("matrix-auth-store");
|
|
||||||
|
|
||||||
if (authStore) {
|
|
||||||
const {
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
access_token,
|
|
||||||
passwordlessUser,
|
|
||||||
tempPassword,
|
|
||||||
} = JSON.parse(authStore);
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
access_token,
|
|
||||||
|
|
||||||
passwordlessUser,
|
|
||||||
tempPassword,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return { client, passwordlessUser };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { client: undefined };
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restore()
|
|
||||||
.then(({ client, passwordlessUser }) => {
|
|
||||||
setState({
|
|
||||||
client,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: !!client,
|
|
||||||
isPasswordlessUser: !!passwordlessUser,
|
|
||||||
userName: client?.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setState({
|
|
||||||
client: undefined,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const changePassword = useCallback(
|
|
||||||
async (password) => {
|
|
||||||
const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
|
|
||||||
localStorage.getItem("matrix-auth-store")
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.setPassword(
|
|
||||||
{
|
|
||||||
type: "m.login.password",
|
|
||||||
identifier: {
|
|
||||||
type: "m.id.user",
|
|
||||||
user: existingSession.user_id,
|
|
||||||
},
|
|
||||||
user: existingSession.user_id,
|
|
||||||
password: tempPassword,
|
|
||||||
},
|
|
||||||
password
|
|
||||||
);
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({
|
|
||||||
...existingSession,
|
|
||||||
passwordlessUser: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setState({
|
|
||||||
client,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
userName: client.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setClient = useCallback(
|
|
||||||
(newClient, session) => {
|
|
||||||
if (client && client !== newClient) {
|
|
||||||
client.stopClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newClient) {
|
|
||||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
|
||||||
|
|
||||||
setState({
|
|
||||||
client: newClient,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isPasswordlessUser: !!session.passwordlessUser,
|
|
||||||
userName: newClient.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
|
|
||||||
setState({
|
|
||||||
client: undefined,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
window.location = "/";
|
|
||||||
}, [history]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ("BroadcastChannel" in window) {
|
|
||||||
const loadTime = Date.now();
|
|
||||||
const broadcastChannel = new BroadcastChannel("matrix-video-chat");
|
|
||||||
|
|
||||||
function onMessage({ data }) {
|
|
||||||
if (data.load !== undefined && data.load > loadTime) {
|
|
||||||
if (client) {
|
|
||||||
client.stopClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
error: new Error(
|
|
||||||
"This application has been opened in another tab."
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastChannel.addEventListener("message", onMessage);
|
|
||||||
broadcastChannel.postMessage({ load: loadTime });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
broadcastChannel.removeEventListener("message", onMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
const context = useMemo(
|
|
||||||
() => ({
|
|
||||||
loading,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
client,
|
|
||||||
changePassword,
|
|
||||||
logout,
|
|
||||||
userName,
|
|
||||||
setClient,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
loading,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
client,
|
|
||||||
changePassword,
|
|
||||||
logout,
|
|
||||||
userName,
|
|
||||||
setClient,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.matrixclient = client;
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorView error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useClient() {
|
|
||||||
return useContext(ClientContext);
|
|
||||||
}
|
|
||||||
353
src/ClientContext.tsx
Normal file
353
src/ClientContext.tsx
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
FC,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
createContext,
|
||||||
|
useMemo,
|
||||||
|
useContext,
|
||||||
|
} from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ErrorView } from "./FullScreenView";
|
||||||
|
import {
|
||||||
|
initClient,
|
||||||
|
defaultHomeserver,
|
||||||
|
CryptoStoreIntegrityError,
|
||||||
|
} from "./matrix-utils";
|
||||||
|
import { widget } from "./widget";
|
||||||
|
import { translatedError } from "./TranslatedError";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
matrixclient: MatrixClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
user_id: string;
|
||||||
|
device_id: string;
|
||||||
|
access_token: string;
|
||||||
|
passwordlessUser: boolean;
|
||||||
|
tempPassword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSession = (): Session => {
|
||||||
|
const data = localStorage.getItem("matrix-auth-store");
|
||||||
|
if (data) return JSON.parse(data);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const saveSession = (session: Session) =>
|
||||||
|
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
||||||
|
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
||||||
|
|
||||||
|
interface ClientState {
|
||||||
|
loading: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isPasswordlessUser: boolean;
|
||||||
|
client: MatrixClient;
|
||||||
|
userName: string;
|
||||||
|
changePassword: (password: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
setClient: (client: MatrixClient, session: Session) => void;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientContext = createContext<ClientState>(null);
|
||||||
|
|
||||||
|
type ClientProviderState = Omit<
|
||||||
|
ClientState,
|
||||||
|
"changePassword" | "logout" | "setClient"
|
||||||
|
> & { error?: Error };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const [
|
||||||
|
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||||
|
setState,
|
||||||
|
] = useState<ClientProviderState>({
|
||||||
|
loading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
client: undefined,
|
||||||
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async (): Promise<
|
||||||
|
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||||
|
> => {
|
||||||
|
if (widget) {
|
||||||
|
// We're inside a widget, so let's engage *matryoshka mode*
|
||||||
|
logger.log("Using a matryoshka client");
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: await widget.client,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// We're running as a standalone application
|
||||||
|
try {
|
||||||
|
const session = loadSession();
|
||||||
|
if (!session) return { client: undefined, isPasswordlessUser: false };
|
||||||
|
|
||||||
|
logger.log("Using a standalone client");
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
const { user_id, device_id, access_token, passwordlessUser } =
|
||||||
|
session;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
client: await initClient(
|
||||||
|
{
|
||||||
|
baseUrl: defaultHomeserver,
|
||||||
|
accessToken: access_token,
|
||||||
|
userId: user_id,
|
||||||
|
deviceId: device_id,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
),
|
||||||
|
isPasswordlessUser: passwordlessUser,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof CryptoStoreIntegrityError) {
|
||||||
|
// We can't use this session anymore, so let's log it out
|
||||||
|
try {
|
||||||
|
const client = await initClient(
|
||||||
|
{
|
||||||
|
baseUrl: defaultHomeserver,
|
||||||
|
accessToken: access_token,
|
||||||
|
userId: user_id,
|
||||||
|
deviceId: device_id,
|
||||||
|
},
|
||||||
|
false // Don't need the crypto store just to log out
|
||||||
|
);
|
||||||
|
await client.logout(true);
|
||||||
|
} catch (err_) {
|
||||||
|
logger.warn(
|
||||||
|
"The previous session was lost, and we couldn't log it out, " +
|
||||||
|
"either"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
} catch (err) {
|
||||||
|
clearSession();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init()
|
||||||
|
.then(({ client, isPasswordlessUser }) => {
|
||||||
|
setState({
|
||||||
|
client,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: Boolean(client),
|
||||||
|
isPasswordlessUser,
|
||||||
|
userName: client?.getUserIdLocalpart(),
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
setState({
|
||||||
|
client: undefined,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changePassword = useCallback(
|
||||||
|
async (password: string) => {
|
||||||
|
const { tempPassword, ...session } = loadSession();
|
||||||
|
|
||||||
|
await client.setPassword(
|
||||||
|
{
|
||||||
|
type: "m.login.password",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: session.user_id,
|
||||||
|
},
|
||||||
|
user: session.user_id,
|
||||||
|
password: tempPassword,
|
||||||
|
},
|
||||||
|
password
|
||||||
|
);
|
||||||
|
|
||||||
|
saveSession({ ...session, passwordlessUser: false });
|
||||||
|
|
||||||
|
setState({
|
||||||
|
client,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
userName: client.getUserIdLocalpart(),
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setClient = useCallback(
|
||||||
|
(newClient: MatrixClient, session: Session) => {
|
||||||
|
if (client && client !== newClient) {
|
||||||
|
client.stopClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newClient) {
|
||||||
|
saveSession(session);
|
||||||
|
|
||||||
|
setState({
|
||||||
|
client: newClient,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isPasswordlessUser: session.passwordlessUser,
|
||||||
|
userName: newClient.getUserIdLocalpart(),
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clearSession();
|
||||||
|
|
||||||
|
setState({
|
||||||
|
client: undefined,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
const logout = useCallback(async () => {
|
||||||
|
await client.logout(true);
|
||||||
|
await client.clearStores();
|
||||||
|
clearSession();
|
||||||
|
setState({
|
||||||
|
client: undefined,
|
||||||
|
loading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: true,
|
||||||
|
userName: "",
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
history.push("/");
|
||||||
|
}, [history, client]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// To protect against multiple sessions writing to the same storage
|
||||||
|
// simultaneously, we send a to-device message that shuts down all other
|
||||||
|
// running instances of the app. This isn't necessary if the app is running
|
||||||
|
// in a widget though, since then it'll be mostly stateless.
|
||||||
|
if (!widget && client) {
|
||||||
|
const loadTime = Date.now();
|
||||||
|
|
||||||
|
const onToDeviceEvent = (event: MatrixEvent) => {
|
||||||
|
if (event.getType() !== "org.matrix.call_duplicate_session") return;
|
||||||
|
|
||||||
|
const content = event.getContent();
|
||||||
|
|
||||||
|
if (content.session_id === client.getSessionId()) return;
|
||||||
|
|
||||||
|
if (content.timestamp > loadTime) {
|
||||||
|
client?.stopClient();
|
||||||
|
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: translatedError(
|
||||||
|
"This application has been opened in another tab.",
|
||||||
|
t
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.on(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
||||||
|
|
||||||
|
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||||
|
[client.getUserId()]: {
|
||||||
|
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [client, t]);
|
||||||
|
|
||||||
|
const context = useMemo<ClientState>(
|
||||||
|
() => ({
|
||||||
|
loading,
|
||||||
|
isAuthenticated,
|
||||||
|
isPasswordlessUser,
|
||||||
|
client,
|
||||||
|
changePassword,
|
||||||
|
logout,
|
||||||
|
userName,
|
||||||
|
setClient,
|
||||||
|
error: undefined,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
loading,
|
||||||
|
isAuthenticated,
|
||||||
|
isPasswordlessUser,
|
||||||
|
client,
|
||||||
|
changePassword,
|
||||||
|
logout,
|
||||||
|
userName,
|
||||||
|
setClient,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.matrixclient = client;
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorView error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useClient = () => useContext(ClientContext);
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import styles from "./Facepile.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { Avatar } from "./Avatar";
|
|
||||||
import { getAvatarUrl } from "./matrix-utils";
|
|
||||||
|
|
||||||
export function Facepile({ className, client, participants, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.facepile, className)}
|
|
||||||
title={participants.map((member) => member.name).join(", ")}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{participants.slice(0, 3).map((member, i) => {
|
|
||||||
const avatarUrl = member.user?.avatarUrl;
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
key={member.userId}
|
|
||||||
size="xs"
|
|
||||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 22)}
|
|
||||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
|
||||||
className={styles.avatar}
|
|
||||||
style={{ left: i * 22 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{participants.length > 3 && (
|
|
||||||
<Avatar
|
|
||||||
key="additional"
|
|
||||||
size="xs"
|
|
||||||
fallback={`+${participants.length - 3}`}
|
|
||||||
className={styles.avatar}
|
|
||||||
style={{ left: 3 * 22 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
.facepile {
|
.facepile {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.facepile.xs {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile.sm {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile.md {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.facepile .avatar {
|
.facepile .avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
border: 1px solid var(--bgColor2);
|
border: 1px solid var(--system);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile.md .avatar {
|
||||||
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/Facepile.tsx
Normal file
98
src/Facepile.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { HTMLAttributes, useMemo } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import styles from "./Facepile.module.css";
|
||||||
|
import { Avatar, Size, sizes } from "./Avatar";
|
||||||
|
|
||||||
|
const overlapMap: Partial<Record<Size, number>> = {
|
||||||
|
[Size.XS]: 2,
|
||||||
|
[Size.SM]: 4,
|
||||||
|
[Size.MD]: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
className: string;
|
||||||
|
client: MatrixClient;
|
||||||
|
participants: RoomMember[];
|
||||||
|
max?: number;
|
||||||
|
size?: Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Facepile({
|
||||||
|
className,
|
||||||
|
client,
|
||||||
|
participants,
|
||||||
|
max = 3,
|
||||||
|
size = Size.XS,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const _size = sizes.get(size);
|
||||||
|
const _overlap = overlapMap[size];
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
return participants.reduce<string | null>(
|
||||||
|
(prev, curr) =>
|
||||||
|
prev === null
|
||||||
|
? curr.name
|
||||||
|
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
||||||
|
null
|
||||||
|
) as string;
|
||||||
|
}, [participants, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.facepile, styles[size], className)}
|
||||||
|
title={title}
|
||||||
|
style={{
|
||||||
|
width:
|
||||||
|
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
||||||
|
_overlap,
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{participants.slice(0, max).map((member, i) => {
|
||||||
|
const avatarUrl = member.user?.avatarUrl;
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
key={member.userId}
|
||||||
|
size={size}
|
||||||
|
src={avatarUrl}
|
||||||
|
fallback={member.name.slice(0, 1).toUpperCase()}
|
||||||
|
className={styles.avatar}
|
||||||
|
style={{ left: i * (_size - _overlap) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{participants.length > max && (
|
||||||
|
<Avatar
|
||||||
|
key="additional"
|
||||||
|
size={size}
|
||||||
|
fallback={`+${participants.length - max}`}
|
||||||
|
className={styles.avatar}
|
||||||
|
style={{ left: max * (_size - _overlap) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import React, { useCallback, useEffect } from "react";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import styles from "./FullScreenView.module.css";
|
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { LinkButton, Button } from "./button";
|
|
||||||
|
|
||||||
export function FullScreenView({ className, children }) {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.page, className)}>
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav />
|
|
||||||
</Header>
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorView({ error }) {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.error(error);
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const onReload = useCallback(() => {
|
|
||||||
window.location = "/";
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullScreenView>
|
|
||||||
<h1>Error</h1>
|
|
||||||
<p>{error.message}</p>
|
|
||||||
{location.pathname === "/" ? (
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
variant="default"
|
|
||||||
className={styles.homeLink}
|
|
||||||
onPress={onReload}
|
|
||||||
>
|
|
||||||
Return to home screen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<LinkButton
|
|
||||||
size="lg"
|
|
||||||
variant="default"
|
|
||||||
className={styles.homeLink}
|
|
||||||
to="/"
|
|
||||||
>
|
|
||||||
Return to home screen
|
|
||||||
</LinkButton>
|
|
||||||
)}
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoadingView() {
|
|
||||||
return (
|
|
||||||
<FullScreenView>
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeLink {
|
/* Make the buttons the same width */
|
||||||
|
.wideButton {
|
||||||
width: 291px;
|
width: 291px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fixed height to avoid content jumping around*/
|
||||||
|
.sendLogsSection {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|||||||
144
src/FullScreenView.tsx
Normal file
144
src/FullScreenView.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import React, { ReactNode, useCallback, useEffect } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||||
|
import { LinkButton, Button } from "./button";
|
||||||
|
import { useSubmitRageshake } from "./settings/submit-rageshake";
|
||||||
|
import { ErrorMessage } from "./input/Input";
|
||||||
|
import styles from "./FullScreenView.module.css";
|
||||||
|
import { translatedError, TranslatedError } from "./TranslatedError";
|
||||||
|
|
||||||
|
interface FullScreenViewProps {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FullScreenView({ className, children }: FullScreenViewProps) {
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.page, className)}>
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<HeaderLogo />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav />
|
||||||
|
</Header>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorViewProps {
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorView({ error }: ErrorViewProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const onReload = useCallback(() => {
|
||||||
|
window.location.href = "/";
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullScreenView>
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>
|
||||||
|
{error instanceof TranslatedError
|
||||||
|
? error.translatedMessage
|
||||||
|
: error.message}
|
||||||
|
</p>
|
||||||
|
{location.pathname === "/" ? (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
className={styles.homeLink}
|
||||||
|
onPress={onReload}
|
||||||
|
>
|
||||||
|
{t("Return to home screen")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<LinkButton
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
className={styles.homeLink}
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
{t("Return to home screen")}
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
</FullScreenView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CrashView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
|
|
||||||
|
const sendDebugLogs = useCallback(() => {
|
||||||
|
submitRageshake({
|
||||||
|
description: "**Soft Crash**",
|
||||||
|
sendLogs: true,
|
||||||
|
});
|
||||||
|
}, [submitRageshake]);
|
||||||
|
|
||||||
|
const onReload = useCallback(() => {
|
||||||
|
window.location.href = "/";
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let logsComponent: JSX.Element | null = null;
|
||||||
|
if (sent) {
|
||||||
|
logsComponent = <div>{t("Thanks! We'll get right on it.")}</div>;
|
||||||
|
} else if (sending) {
|
||||||
|
logsComponent = <div>{t("Sending…")}</div>;
|
||||||
|
} else {
|
||||||
|
logsComponent = (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
onPress={sendDebugLogs}
|
||||||
|
className={styles.wideButton}
|
||||||
|
>
|
||||||
|
{t("Send debug logs")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullScreenView>
|
||||||
|
<Trans>
|
||||||
|
<h1>Oops, something's gone wrong.</h1>
|
||||||
|
<p>Submitting debug logs will help us track down the problem.</p>
|
||||||
|
</Trans>
|
||||||
|
<div className={styles.sendLogsSection}>{logsComponent}</div>
|
||||||
|
{error && (
|
||||||
|
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
className={styles.wideButton}
|
||||||
|
onPress={onReload}
|
||||||
|
>
|
||||||
|
{t("Return to home screen")}
|
||||||
|
</Button>
|
||||||
|
</FullScreenView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullScreenView>
|
||||||
|
<h1>{t("Loading…")}</h1>
|
||||||
|
</FullScreenView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import styles from "./Header.module.css";
|
|
||||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
|
||||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
|
||||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
|
||||||
import { useButton } from "@react-aria/button";
|
|
||||||
import { Subtitle } from "./typography/Typography";
|
|
||||||
import { Avatar } from "./Avatar";
|
|
||||||
|
|
||||||
export function Header({ children, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<header className={classNames(styles.header, className)} {...rest}>
|
|
||||||
{children}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LeftNav({ children, className, hideMobile, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.nav,
|
|
||||||
styles.leftNav,
|
|
||||||
{ [styles.hideMobile]: hideMobile },
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RightNav({ children, className, hideMobile, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.nav,
|
|
||||||
styles.rightNav,
|
|
||||||
{ [styles.hideMobile]: hideMobile },
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeaderLogo({ className }) {
|
|
||||||
return (
|
|
||||||
<Link className={classNames(styles.headerLogo, className)} to="/">
|
|
||||||
<Logo />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RoomHeaderInfo({ roomName }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.roomAvatar}>
|
|
||||||
<Avatar
|
|
||||||
size="md"
|
|
||||||
bgKey={roomName}
|
|
||||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
|
||||||
/>
|
|
||||||
<VideoIcon width={16} height={16} />
|
|
||||||
</div>
|
|
||||||
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
|
|
||||||
const ref = useRef();
|
|
||||||
const { buttonProps } = useButton(rest, ref);
|
|
||||||
return (
|
|
||||||
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
|
||||||
<ArrowLeftIcon width={16} height={16} />
|
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -104,6 +104,24 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.versionMismatchWarning {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionMismatchWarning::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
mask-image: url("./icons/AlertTriangleFilled.svg");
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
background-color: var(--alert);
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.headerLogo,
|
.headerLogo,
|
||||||
.roomAvatar,
|
.roomAvatar,
|
||||||
|
|||||||
180
src/Header.tsx
Normal file
180
src/Header.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import React, { HTMLAttributes, ReactNode, useCallback, useRef } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useButton } from "@react-aria/button";
|
||||||
|
import { AriaButtonProps } from "@react-types/button";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import styles from "./Header.module.css";
|
||||||
|
import { useModalTriggerState } from "./Modal";
|
||||||
|
import { Button } from "./button";
|
||||||
|
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
||||||
|
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||||
|
import { Subtitle } from "./typography/Typography";
|
||||||
|
import { Avatar, Size } from "./Avatar";
|
||||||
|
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
|
||||||
|
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||||
|
|
||||||
|
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ children, className, ...rest }: HeaderProps) {
|
||||||
|
return (
|
||||||
|
<header className={classNames(styles.header, className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeftNavProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
hideMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LeftNav({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
hideMobile,
|
||||||
|
...rest
|
||||||
|
}: LeftNavProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.nav,
|
||||||
|
styles.leftNav,
|
||||||
|
{ [styles.hideMobile]: hideMobile },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RightNavProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
hideMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RightNav({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
hideMobile,
|
||||||
|
...rest
|
||||||
|
}: RightNavProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.nav,
|
||||||
|
styles.rightNav,
|
||||||
|
{ [styles.hideMobile]: hideMobile },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderLogoProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderLogo({ className }: HeaderLogoProps) {
|
||||||
|
return (
|
||||||
|
<Link className={classNames(styles.headerLogo, className)} to="/">
|
||||||
|
<Logo />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomHeaderInfo {
|
||||||
|
roomName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomHeaderInfo({ roomName, avatarUrl }: RoomHeaderInfo) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.roomAvatar}>
|
||||||
|
<Avatar
|
||||||
|
size={Size.MD}
|
||||||
|
src={avatarUrl}
|
||||||
|
bgKey={roomName}
|
||||||
|
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||||
|
/>
|
||||||
|
<VideoIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomSetupHeaderInfoProps extends AriaButtonProps<"button"> {
|
||||||
|
roomName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
isEmbedded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomSetupHeaderInfo({
|
||||||
|
roomName,
|
||||||
|
avatarUrl,
|
||||||
|
isEmbedded,
|
||||||
|
...rest
|
||||||
|
}: RoomSetupHeaderInfoProps) {
|
||||||
|
const ref = useRef();
|
||||||
|
const { buttonProps } = useButton(rest, ref);
|
||||||
|
|
||||||
|
if (isEmbedded) {
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
||||||
|
<ArrowLeftIcon width={16} height={16} />
|
||||||
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionMismatchWarningProps {
|
||||||
|
users: Set<string>;
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VersionMismatchWarning({
|
||||||
|
users,
|
||||||
|
room,
|
||||||
|
}: VersionMismatchWarningProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
|
const onDetailsClick = useCallback(() => {
|
||||||
|
modalState.open();
|
||||||
|
}, [modalState]);
|
||||||
|
|
||||||
|
if (users.size === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.versionMismatchWarning}>
|
||||||
|
{t("Incompatible versions!")}
|
||||||
|
<Button variant="link" onClick={onDetailsClick}>
|
||||||
|
{t("Details")}
|
||||||
|
</Button>
|
||||||
|
{modalState.isOpen && (
|
||||||
|
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/IncompatibleVersionModal.tsx
Normal file
53
src/IncompatibleVersionModal.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from "./Modal";
|
||||||
|
import { Body } from "./typography/Typography";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userIds: Set<string>;
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IncompatibleVersionModal: React.FC<Props> = ({
|
||||||
|
userIds,
|
||||||
|
room,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const userLis = useMemo(
|
||||||
|
() => [...userIds].map((u) => <li>{room.getMember(u)?.name ?? u}</li>),
|
||||||
|
[userIds, room]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={t("Incompatible versions")} isDismissable {...rest}>
|
||||||
|
<ModalContent>
|
||||||
|
<Body>
|
||||||
|
<Trans>
|
||||||
|
Other users are trying to join this call from incompatible versions.
|
||||||
|
These users should ensure that they have refreshed their browsers:
|
||||||
|
<ul>{userLis}</ul>
|
||||||
|
</Trans>
|
||||||
|
</Body>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
6
src/IndexedDBWorker.ts
Normal file
6
src/IndexedDBWorker.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const remoteWorker = new IndexedDBStoreWorker((self as any).postMessage);
|
||||||
|
|
||||||
|
self.onmessage = remoteWorker.onMessage;
|
||||||
90
src/LazyEventEmitter.ts
Normal file
90
src/LazyEventEmitter.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event emitter that lets events pile up in a backlog until a listener is
|
||||||
|
* present, at which point any events that were missed are re-emitted.
|
||||||
|
*/
|
||||||
|
export class LazyEventEmitter extends EventEmitter {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private eventBacklogs = new Map<string | symbol, NonEmptyArray<any[]>>();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public emit(type: string | symbol, ...args: any[]): boolean {
|
||||||
|
const hasListeners = super.emit(type, ...args);
|
||||||
|
|
||||||
|
if (!hasListeners) {
|
||||||
|
// The event was missed, so add it to the backlog
|
||||||
|
const backlog = this.eventBacklogs.get(type);
|
||||||
|
if (backlog) {
|
||||||
|
backlog.push(args);
|
||||||
|
} else {
|
||||||
|
// Start a new backlog
|
||||||
|
this.eventBacklogs.set(type, [args]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public on(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||||
|
super.on(type, listener);
|
||||||
|
|
||||||
|
const backlog = this.eventBacklogs.get(type);
|
||||||
|
if (backlog) {
|
||||||
|
// That was the first listener for this type, so let's send it all the
|
||||||
|
// events that have piled up
|
||||||
|
for (const args of backlog) super.emit(type, ...args);
|
||||||
|
// Backlog is now clear
|
||||||
|
this.eventBacklogs.delete(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(
|
||||||
|
type: string | symbol,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
listener: (...args: any[]) => void
|
||||||
|
): this {
|
||||||
|
return this.on(type, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public once(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||||
|
super.once(type, listener);
|
||||||
|
|
||||||
|
const backlog = this.eventBacklogs.get(type);
|
||||||
|
if (backlog) {
|
||||||
|
// That was the first listener for this type, so let's send it the first
|
||||||
|
// of the events that have piled up
|
||||||
|
super.emit(type, ...backlog[0]);
|
||||||
|
// Clear the event from the backlog
|
||||||
|
if (backlog.length === 1) {
|
||||||
|
this.eventBacklogs.delete(type);
|
||||||
|
} else {
|
||||||
|
backlog.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import React, { useRef } from "react";
|
|
||||||
import { useListBox, useOption } from "@react-aria/listbox";
|
|
||||||
import styles from "./ListBox.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export function ListBox(props) {
|
|
||||||
const ref = useRef();
|
|
||||||
let { listBoxRef = ref, state } = props;
|
|
||||||
const { listBoxProps } = useListBox(props, state, listBoxRef);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
{...listBoxProps}
|
|
||||||
ref={listBoxRef}
|
|
||||||
className={classNames(styles.listBox, props.className)}
|
|
||||||
>
|
|
||||||
{[...state.collection].map((item) => (
|
|
||||||
<Option
|
|
||||||
key={item.key}
|
|
||||||
item={item}
|
|
||||||
state={state}
|
|
||||||
className={props.optionClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Option({ item, state, className }) {
|
|
||||||
const ref = useRef();
|
|
||||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
|
||||||
{ key: item.key },
|
|
||||||
state,
|
|
||||||
ref
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
{...optionProps}
|
|
||||||
ref={ref}
|
|
||||||
className={classNames(styles.option, className, {
|
|
||||||
[styles.selected]: isSelected,
|
|
||||||
[styles.focused]: isFocused,
|
|
||||||
[styles.disables]: isDisabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.rendered}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--quinary-content);
|
||||||
background-color: var(--bgColor1);
|
background-color: var(--background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -28,6 +28,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.option.disabled {
|
.option.disabled {
|
||||||
color: var(--textColor2);
|
color: var(--quaternary-content);
|
||||||
background-color: var(--bgColor3);
|
background-color: var(--bgColor3);
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/ListBox.tsx
Normal file
89
src/ListBox.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
|
||||||
|
import { ListState } from "@react-stately/list";
|
||||||
|
import { Node } from "@react-types/shared";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import styles from "./ListBox.module.css";
|
||||||
|
|
||||||
|
interface ListBoxProps<T> extends AriaListBoxOptions<T> {
|
||||||
|
optionClassName: string;
|
||||||
|
state: ListState<T>;
|
||||||
|
className?: string;
|
||||||
|
listBoxRef?: React.MutableRefObject<HTMLUListElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListBox<T>({
|
||||||
|
state,
|
||||||
|
optionClassName,
|
||||||
|
className,
|
||||||
|
listBoxRef,
|
||||||
|
...rest
|
||||||
|
}: ListBoxProps<T>) {
|
||||||
|
const ref = useRef<HTMLUListElement>();
|
||||||
|
if (!listBoxRef) listBoxRef = ref;
|
||||||
|
|
||||||
|
const { listBoxProps } = useListBox(rest, state, listBoxRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
{...listBoxProps}
|
||||||
|
ref={listBoxRef}
|
||||||
|
className={classNames(styles.listBox, className)}
|
||||||
|
>
|
||||||
|
{[...state.collection].map((item) => (
|
||||||
|
<Option
|
||||||
|
key={item.key}
|
||||||
|
item={item}
|
||||||
|
state={state}
|
||||||
|
className={optionClassName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionProps<T> {
|
||||||
|
className: string;
|
||||||
|
state: ListState<T>;
|
||||||
|
item: Node<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Option<T>({ item, state, className }: OptionProps<T>) {
|
||||||
|
const ref = useRef();
|
||||||
|
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||||
|
{ key: item.key },
|
||||||
|
state,
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...optionProps}
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(styles.option, className, {
|
||||||
|
[styles.selected]: isSelected,
|
||||||
|
[styles.focused]: isFocused,
|
||||||
|
[styles.disables]: isDisabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.rendered}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
.menuItem.focused,
|
.menuItem.focused,
|
||||||
.menuItem:hover {
|
.menuItem:hover {
|
||||||
background-color: var(--bgColor4);
|
background-color: var(--quinary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem.focused:first-child,
|
.menuItem.focused:first-child,
|
||||||
@@ -39,3 +39,12 @@
|
|||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkIcon * {
|
||||||
|
stroke: var(--primary-content);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { Key, useRef, useState } from "react";
|
||||||
import styles from "./Menu.module.css";
|
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
|
||||||
import { useMenu, useMenuItem } from "@react-aria/menu";
|
import { TreeState, useTreeState } from "@react-stately/tree";
|
||||||
import { useTreeState } from "@react-stately/tree";
|
|
||||||
import { mergeProps } from "@react-aria/utils";
|
import { mergeProps } from "@react-aria/utils";
|
||||||
import { useFocus } from "@react-aria/interactions";
|
import { useFocus } from "@react-aria/interactions";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Node } from "@react-types/shared";
|
||||||
|
|
||||||
export function Menu({ className, onAction, ...rest }) {
|
import styles from "./Menu.module.css";
|
||||||
const state = useTreeState({ ...rest, selectionMode: "none" });
|
|
||||||
|
interface MenuProps<T> extends AriaMenuOptions<T> {
|
||||||
|
className?: String;
|
||||||
|
onClose?: () => void;
|
||||||
|
onAction: (value: Key) => void;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Menu<T extends object>({
|
||||||
|
className,
|
||||||
|
onAction,
|
||||||
|
onClose,
|
||||||
|
label,
|
||||||
|
...rest
|
||||||
|
}: MenuProps<T>) {
|
||||||
|
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
|
||||||
const menuRef = useRef();
|
const menuRef = useRef();
|
||||||
const { menuProps } = useMenu(rest, state, menuRef);
|
const { menuProps } = useMenu<T>(rest, state, menuRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
@@ -23,19 +38,25 @@ export function Menu({ className, onAction, ...rest }) {
|
|||||||
item={item}
|
item={item}
|
||||||
state={state}
|
state={state}
|
||||||
onAction={onAction}
|
onAction={onAction}
|
||||||
onClose={rest.onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem({ item, state, onAction, onClose }) {
|
interface MenuItemProps<T> {
|
||||||
|
item: Node<T>;
|
||||||
|
state: TreeState<T>;
|
||||||
|
onAction: (value: Key) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const { menuItemProps } = useMenuItem(
|
const { menuItemProps } = useMenuItem(
|
||||||
{
|
{
|
||||||
key: item.key,
|
key: item.key,
|
||||||
isDisabled: item.isDisabled,
|
|
||||||
onAction,
|
onAction,
|
||||||
onClose,
|
onClose,
|
||||||
},
|
},
|
||||||
132
src/Modal.jsx
132
src/Modal.jsx
@@ -1,132 +0,0 @@
|
|||||||
import React, { useRef, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
useOverlay,
|
|
||||||
usePreventScroll,
|
|
||||||
useModal,
|
|
||||||
OverlayContainer,
|
|
||||||
} from "@react-aria/overlays";
|
|
||||||
import { useOverlayTriggerState } from "@react-stately/overlays";
|
|
||||||
import { useDialog } from "@react-aria/dialog";
|
|
||||||
import { FocusScope } from "@react-aria/focus";
|
|
||||||
import { useButton } from "@react-aria/button";
|
|
||||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
|
||||||
import styles from "./Modal.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export function Modal(props) {
|
|
||||||
const { title, children, className, mobileFullScreen } = props;
|
|
||||||
const modalRef = useRef();
|
|
||||||
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
|
|
||||||
usePreventScroll();
|
|
||||||
const { modalProps } = useModal();
|
|
||||||
const { dialogProps, titleProps } = useDialog(props, modalRef);
|
|
||||||
const closeButtonRef = useRef();
|
|
||||||
const { buttonProps: closeButtonProps } = useButton({
|
|
||||||
onPress: () => props.onClose(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayContainer>
|
|
||||||
<div className={styles.modalOverlay} {...underlayProps}>
|
|
||||||
<FocusScope contain restoreFocus autoFocus>
|
|
||||||
<div
|
|
||||||
{...overlayProps}
|
|
||||||
{...dialogProps}
|
|
||||||
{...modalProps}
|
|
||||||
ref={modalRef}
|
|
||||||
className={classNames(
|
|
||||||
styles.modal,
|
|
||||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.modalHeader}>
|
|
||||||
<h3 {...titleProps}>{title}</h3>
|
|
||||||
<button
|
|
||||||
{...closeButtonProps}
|
|
||||||
ref={closeButtonRef}
|
|
||||||
className={styles.closeButton}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</FocusScope>
|
|
||||||
</div>
|
|
||||||
</OverlayContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModalContent({ children, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.content, className)} {...rest}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useModalTriggerState() {
|
|
||||||
const modalState = useOverlayTriggerState({});
|
|
||||||
const modalProps = useMemo(
|
|
||||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
|
||||||
[modalState]
|
|
||||||
);
|
|
||||||
return { modalState, modalProps };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useToggleModalButton(modalState, ref) {
|
|
||||||
return useButton(
|
|
||||||
{
|
|
||||||
onPress: () => modalState.toggle(),
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useOpenModalButton(modalState, ref) {
|
|
||||||
return useButton(
|
|
||||||
{
|
|
||||||
onPress: () => modalState.open(),
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCloseModalButton(modalState, ref) {
|
|
||||||
return useButton(
|
|
||||||
{
|
|
||||||
onPress: () => modalState.close(),
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModalTrigger({ children }) {
|
|
||||||
const { modalState, modalProps } = useModalState();
|
|
||||||
const buttonRef = useRef();
|
|
||||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Array.isArray(children) ||
|
|
||||||
children.length > 2 ||
|
|
||||||
typeof children[1] !== "function"
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [modalTrigger, modal] = children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<modalTrigger.type
|
|
||||||
{...modalTrigger.props}
|
|
||||||
{...buttonProps}
|
|
||||||
ref={buttonRef}
|
|
||||||
/>
|
|
||||||
{modalState.isOpen && modal(modalProps)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modalHeader h3 {
|
.modalHeader h3 {
|
||||||
|
font-weight: 600;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
204
src/Modal.tsx
Normal file
204
src/Modal.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable jsx-a11y/no-autofocus */
|
||||||
|
|
||||||
|
import React, { useRef, useMemo, ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
useOverlay,
|
||||||
|
usePreventScroll,
|
||||||
|
useModal,
|
||||||
|
OverlayContainer,
|
||||||
|
OverlayProps,
|
||||||
|
} from "@react-aria/overlays";
|
||||||
|
import {
|
||||||
|
OverlayTriggerState,
|
||||||
|
useOverlayTriggerState,
|
||||||
|
} from "@react-stately/overlays";
|
||||||
|
import { useDialog } from "@react-aria/dialog";
|
||||||
|
import { FocusScope } from "@react-aria/focus";
|
||||||
|
import { ButtonAria, useButton } from "@react-aria/button";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { AriaDialogProps } from "@react-types/dialog";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||||
|
import styles from "./Modal.module.css";
|
||||||
|
|
||||||
|
export interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
mobileFullScreen?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Modal({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
mobileFullScreen,
|
||||||
|
onClose,
|
||||||
|
...rest
|
||||||
|
}: ModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const modalRef = useRef();
|
||||||
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
|
{ ...rest, onClose },
|
||||||
|
modalRef
|
||||||
|
);
|
||||||
|
usePreventScroll();
|
||||||
|
const { modalProps } = useModal();
|
||||||
|
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||||
|
const closeButtonRef = useRef();
|
||||||
|
const { buttonProps: closeButtonProps } = useButton(
|
||||||
|
{
|
||||||
|
onPress: () => onClose(),
|
||||||
|
},
|
||||||
|
closeButtonRef
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayContainer>
|
||||||
|
<div className={styles.modalOverlay} {...underlayProps}>
|
||||||
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
<div
|
||||||
|
{...overlayProps}
|
||||||
|
{...dialogProps}
|
||||||
|
{...modalProps}
|
||||||
|
ref={modalRef}
|
||||||
|
className={classNames(
|
||||||
|
styles.modal,
|
||||||
|
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<h3 {...titleProps}>{title}</h3>
|
||||||
|
<button
|
||||||
|
{...closeButtonProps}
|
||||||
|
ref={closeButtonRef}
|
||||||
|
className={styles.closeButton}
|
||||||
|
title={t("Close")}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</div>
|
||||||
|
</OverlayContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalContentProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalContent({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: ModalContentProps) {
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.content, className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useModalTriggerState(): {
|
||||||
|
modalState: OverlayTriggerState;
|
||||||
|
modalProps: { isOpen: boolean; onClose: () => void };
|
||||||
|
} {
|
||||||
|
const modalState = useOverlayTriggerState({});
|
||||||
|
const modalProps = useMemo(
|
||||||
|
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||||
|
[modalState]
|
||||||
|
);
|
||||||
|
return { modalState, modalProps };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToggleModalButton(
|
||||||
|
modalState: OverlayTriggerState,
|
||||||
|
ref: React.RefObject<HTMLButtonElement>
|
||||||
|
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
|
return useButton(
|
||||||
|
{
|
||||||
|
onPress: () => modalState.toggle(),
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOpenModalButton(
|
||||||
|
modalState: OverlayTriggerState,
|
||||||
|
ref: React.RefObject<HTMLButtonElement>
|
||||||
|
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
|
return useButton(
|
||||||
|
{
|
||||||
|
onPress: () => modalState.open(),
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCloseModalButton(
|
||||||
|
modalState: OverlayTriggerState,
|
||||||
|
ref: React.RefObject<HTMLButtonElement>
|
||||||
|
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
|
return useButton(
|
||||||
|
{
|
||||||
|
onPress: () => modalState.close(),
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalTriggerProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalTrigger({ children }: ModalTriggerProps) {
|
||||||
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
const buttonRef = useRef();
|
||||||
|
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Array.isArray(children) ||
|
||||||
|
children.length > 2 ||
|
||||||
|
typeof children[1] !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [modalTrigger, modal] = children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<modalTrigger.type
|
||||||
|
{...modalTrigger.props}
|
||||||
|
{...buttonProps}
|
||||||
|
ref={buttonRef}
|
||||||
|
/>
|
||||||
|
{modalState.isOpen && modal(modalProps)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
|
||||||
import { FieldRow, InputField } from "./input/Input";
|
|
||||||
import { usePageTitle } from "./usePageTitle";
|
|
||||||
|
|
||||||
export function SequenceDiagramViewerPage() {
|
|
||||||
usePageTitle("Inspector");
|
|
||||||
|
|
||||||
const [debugLog, setDebugLog] = useState();
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState();
|
|
||||||
const onChangeDebugLog = useCallback((e) => {
|
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
|
||||||
e.target.files[0].text().then((text) => {
|
|
||||||
setDebugLog(JSON.parse(text));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 20 }}>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="file"
|
|
||||||
id="debugLog"
|
|
||||||
name="debugLog"
|
|
||||||
label="Debug Log"
|
|
||||||
onChange={onChangeDebugLog}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{debugLog && (
|
|
||||||
<SequenceDiagramViewer
|
|
||||||
localUserId={debugLog.localUserId}
|
|
||||||
selectedUserId={selectedUserId}
|
|
||||||
onSelectUserId={setSelectedUserId}
|
|
||||||
remoteUserIds={debugLog.remoteUserIds}
|
|
||||||
events={debugLog.eventsByUserId[selectedUserId]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
69
src/SequenceDiagramViewerPage.tsx
Normal file
69
src/SequenceDiagramViewerPage.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SequenceDiagramViewer,
|
||||||
|
SequenceDiagramMatrixEvent,
|
||||||
|
} from "./room/GroupCallInspector";
|
||||||
|
import { FieldRow, InputField } from "./input/Input";
|
||||||
|
import { usePageTitle } from "./usePageTitle";
|
||||||
|
|
||||||
|
interface DebugLog {
|
||||||
|
localUserId: string;
|
||||||
|
eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
||||||
|
remoteUserIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SequenceDiagramViewerPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Inspector"));
|
||||||
|
|
||||||
|
const [debugLog, setDebugLog] = useState<DebugLog>();
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||||
|
const onChangeDebugLog = useCallback((e) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
e.target.files[0].text().then((text: string) => {
|
||||||
|
setDebugLog(JSON.parse(text));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
type="file"
|
||||||
|
id="debugLog"
|
||||||
|
name="debugLog"
|
||||||
|
label={t("Debug log")}
|
||||||
|
onChange={onChangeDebugLog}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{debugLog && (
|
||||||
|
<SequenceDiagramViewer
|
||||||
|
localUserId={debugLog.localUserId}
|
||||||
|
selectedUserId={selectedUserId}
|
||||||
|
onSelectUserId={setSelectedUserId}
|
||||||
|
remoteUserIds={debugLog.remoteUserIds}
|
||||||
|
events={debugLog.eventsByUserId[selectedUserId]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import React, { forwardRef, useRef } from "react";
|
|
||||||
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
|
||||||
import { FocusableProvider } from "@react-aria/focus";
|
|
||||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
|
||||||
import styles from "./Tooltip.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
|
||||||
|
|
||||||
export const Tooltip = forwardRef(
|
|
||||||
({ position, state, className, ...props }, ref) => {
|
|
||||||
let { tooltipProps } = useTooltip(props, state);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.tooltip, className)}
|
|
||||||
{...mergeProps(props, tooltipProps)}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
|
||||||
const tooltipState = useTooltipTriggerState(rest);
|
|
||||||
const triggerRef = useObjectRef(ref);
|
|
||||||
const overlayRef = useRef();
|
|
||||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
|
||||||
rest,
|
|
||||||
tooltipState,
|
|
||||||
triggerRef
|
|
||||||
);
|
|
||||||
|
|
||||||
const { overlayProps } = useOverlayPosition({
|
|
||||||
placement: rest.placement || "top",
|
|
||||||
targetRef: triggerRef,
|
|
||||||
overlayRef,
|
|
||||||
isOpen: tooltipState.isOpen,
|
|
||||||
offset: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Array.isArray(children) ||
|
|
||||||
children.length > 2 ||
|
|
||||||
typeof children[1] !== "function"
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"TooltipTrigger must have two props. The first being a button and the second being a render prop."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tooltipTrigger, tooltip] = children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
|
||||||
{<tooltipTrigger.type {...mergeProps(tooltipTrigger.props, rest)} />}
|
|
||||||
{tooltipState.isOpen && (
|
|
||||||
<OverlayContainer>
|
|
||||||
<Tooltip
|
|
||||||
state={tooltipState}
|
|
||||||
{...mergeProps(tooltipProps, overlayProps)}
|
|
||||||
ref={overlayRef}
|
|
||||||
>
|
|
||||||
{tooltip()}
|
|
||||||
</Tooltip>
|
|
||||||
</OverlayContainer>
|
|
||||||
)}
|
|
||||||
</FocusableProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
TooltipTrigger.defaultProps = {
|
|
||||||
delay: 250,
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 10px;
|
padding: 10px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 135px;
|
max-width: 135px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/Tooltip.tsx
Normal file
114
src/Tooltip.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
TooltipTriggerState,
|
||||||
|
useTooltipTriggerState,
|
||||||
|
} from "@react-stately/tooltip";
|
||||||
|
import { FocusableProvider } from "@react-aria/focus";
|
||||||
|
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||||
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||||
|
import { Placement } from "@react-types/overlays";
|
||||||
|
|
||||||
|
import styles from "./Tooltip.module.css";
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
className?: string;
|
||||||
|
state: TooltipTriggerState;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
||||||
|
(
|
||||||
|
{ state, className, children, ...rest }: TooltipProps,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
const { tooltipProps } = useTooltip(rest, state);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.tooltip, className)}
|
||||||
|
{...mergeProps(rest, tooltipProps)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TooltipTriggerProps {
|
||||||
|
children: ReactElement;
|
||||||
|
placement?: Placement;
|
||||||
|
delay?: number;
|
||||||
|
tooltip: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
||||||
|
(
|
||||||
|
{ children, placement, tooltip, ...rest }: TooltipTriggerProps,
|
||||||
|
ref: ForwardedRef<HTMLElement>
|
||||||
|
) => {
|
||||||
|
const tooltipTriggerProps = { delay: 250, ...rest };
|
||||||
|
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
|
||||||
|
const triggerRef = useObjectRef<HTMLElement>(ref);
|
||||||
|
const overlayRef = useRef();
|
||||||
|
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||||
|
tooltipTriggerProps,
|
||||||
|
tooltipState,
|
||||||
|
triggerRef
|
||||||
|
);
|
||||||
|
|
||||||
|
const { overlayProps } = useOverlayPosition({
|
||||||
|
placement: placement || "top",
|
||||||
|
targetRef: triggerRef,
|
||||||
|
overlayRef,
|
||||||
|
isOpen: tooltipState.isOpen,
|
||||||
|
offset: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
||||||
|
<children.type
|
||||||
|
{...mergeProps<typeof children.props | typeof rest>(
|
||||||
|
children.props,
|
||||||
|
rest
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{tooltipState.isOpen && (
|
||||||
|
<OverlayContainer>
|
||||||
|
<Tooltip
|
||||||
|
state={tooltipState}
|
||||||
|
ref={overlayRef}
|
||||||
|
{...mergeProps(tooltipProps, overlayProps)}
|
||||||
|
>
|
||||||
|
{tooltip()}
|
||||||
|
</Tooltip>
|
||||||
|
</OverlayContainer>
|
||||||
|
)}
|
||||||
|
</FocusableProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
41
src/TranslatedError.ts
Normal file
41
src/TranslatedError.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error with messages in both English and the user's preferred language.
|
||||||
|
*/
|
||||||
|
// Abstract to force consumers to use the function below rather than calling the
|
||||||
|
// constructor directly
|
||||||
|
export abstract class TranslatedError extends Error {
|
||||||
|
/**
|
||||||
|
* The error message in the user's preferred language.
|
||||||
|
*/
|
||||||
|
public readonly translatedMessage: string;
|
||||||
|
|
||||||
|
public constructor(messageKey: string, translationFn: typeof i18n.t) {
|
||||||
|
super(translationFn(messageKey, { lng: "en-GB" }));
|
||||||
|
this.translatedMessage = translationFn(messageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranslatedErrorImpl extends TranslatedError {}
|
||||||
|
|
||||||
|
// i18next-parser can't detect calls to a constructor, so we expose a bare
|
||||||
|
// function instead
|
||||||
|
export const translatedError = (messageKey: string, t: typeof i18n.t) =>
|
||||||
|
new TranslatedErrorImpl(messageKey, t);
|
||||||
109
src/UrlParams.ts
Normal file
109
src/UrlParams.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
export interface UrlParams {
|
||||||
|
roomAlias: string | null;
|
||||||
|
roomId: string | null;
|
||||||
|
viaServers: string[];
|
||||||
|
// Whether the app is running in embedded mode, and should keep the user
|
||||||
|
// confined to the current room
|
||||||
|
isEmbedded: boolean;
|
||||||
|
// Whether the app should pause before joining the call until it sees an
|
||||||
|
// io.element.join widget action, allowing it to be preloaded
|
||||||
|
preload: boolean;
|
||||||
|
// Whether to hide the room header when in a call
|
||||||
|
hideHeader: boolean;
|
||||||
|
// Whether to hide the screen-sharing button
|
||||||
|
hideScreensharing: boolean;
|
||||||
|
// Whether to start a walkie-talkie call instead of a video call
|
||||||
|
isPtt: boolean;
|
||||||
|
// Whether to use end-to-end encryption
|
||||||
|
e2eEnabled: boolean;
|
||||||
|
// The user's ID (only used in matryoshka mode)
|
||||||
|
userId: string | null;
|
||||||
|
// The display name to use for auto-registration
|
||||||
|
displayName: string | null;
|
||||||
|
// The device's ID (only used in matryoshka mode)
|
||||||
|
deviceId: string | null;
|
||||||
|
// The base URL of the homeserver to use for media lookups in matryoshka mode
|
||||||
|
baseUrl: string | null;
|
||||||
|
// The BCP 47 code of the language the app should use
|
||||||
|
lang: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the app parameters for the current URL.
|
||||||
|
* @param query The URL query string
|
||||||
|
* @param fragment The URL fragment string
|
||||||
|
* @returns The app parameters encoded in the URL
|
||||||
|
*/
|
||||||
|
export const getUrlParams = (
|
||||||
|
query: string = window.location.search,
|
||||||
|
fragment: string = window.location.hash
|
||||||
|
): UrlParams => {
|
||||||
|
const fragmentQueryStart = fragment.indexOf("?");
|
||||||
|
const fragmentParams = new URLSearchParams(
|
||||||
|
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
|
||||||
|
);
|
||||||
|
const queryParams = new URLSearchParams(query);
|
||||||
|
|
||||||
|
// Normally, URL params should be encoded in the fragment so as to avoid
|
||||||
|
// leaking them to the server. However, we also check the normal query
|
||||||
|
// string for backwards compatibility with versions that only used that.
|
||||||
|
const hasParam = (name: string): boolean =>
|
||||||
|
fragmentParams.has(name) || queryParams.has(name);
|
||||||
|
const getParam = (name: string): string | null =>
|
||||||
|
fragmentParams.get(name) ?? queryParams.get(name);
|
||||||
|
const getAllParams = (name: string): string[] => [
|
||||||
|
...fragmentParams.getAll(name),
|
||||||
|
...queryParams.getAll(name),
|
||||||
|
];
|
||||||
|
|
||||||
|
// The part of the fragment before the ?
|
||||||
|
const fragmentRoute =
|
||||||
|
fragmentQueryStart === -1
|
||||||
|
? fragment
|
||||||
|
: fragment.substring(0, fragmentQueryStart);
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null,
|
||||||
|
roomId: getParam("roomId"),
|
||||||
|
viaServers: getAllParams("via"),
|
||||||
|
isEmbedded: hasParam("embed"),
|
||||||
|
preload: hasParam("preload"),
|
||||||
|
hideHeader: hasParam("hideHeader"),
|
||||||
|
hideScreensharing: hasParam("hideScreensharing"),
|
||||||
|
isPtt: hasParam("ptt"),
|
||||||
|
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||||
|
userId: getParam("userId"),
|
||||||
|
displayName: getParam("displayName"),
|
||||||
|
deviceId: getParam("deviceId"),
|
||||||
|
baseUrl: getParam("baseUrl"),
|
||||||
|
lang: getParam("lang"),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to simplify use of getUrlParams.
|
||||||
|
* @returns The app parameters for the current URL
|
||||||
|
*/
|
||||||
|
export const useUrlParams = (): UrlParams => {
|
||||||
|
const { hash, search } = useLocation();
|
||||||
|
return useMemo(() => getUrlParams(search, hash), [search, hash]);
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.userButton svg * {
|
.userButton svg * {
|
||||||
fill: var(--textColor1);
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button, LinkButton } from "./button";
|
import { Button, LinkButton } from "./button";
|
||||||
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
||||||
import { Menu } from "./Menu";
|
import { Menu } from "./Menu";
|
||||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
import { TooltipTrigger } from "./Tooltip";
|
||||||
import { Avatar } from "./Avatar";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
||||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
||||||
import styles from "./UserMenu.module.css";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import { Body } from "./typography/Typography";
|
import { Body } from "./typography/Typography";
|
||||||
|
import styles from "./UserMenu.module.css";
|
||||||
|
|
||||||
|
interface UserMenuProps {
|
||||||
|
preventNavigation: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isPasswordlessUser: boolean;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
onAction: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function UserMenu({
|
export function UserMenu({
|
||||||
preventNavigation,
|
preventNavigation,
|
||||||
@@ -19,7 +30,8 @@ export function UserMenu({
|
|||||||
displayName,
|
displayName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
onAction,
|
onAction,
|
||||||
}) {
|
}: UserMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
@@ -35,7 +47,7 @@ export function UserMenu({
|
|||||||
if (isPasswordlessUser && !preventNavigation) {
|
if (isPasswordlessUser && !preventNavigation) {
|
||||||
arr.push({
|
arr.push({
|
||||||
key: "login",
|
key: "login",
|
||||||
label: "Sign In",
|
label: t("Sign in"),
|
||||||
icon: LoginIcon,
|
icon: LoginIcon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -43,14 +55,16 @@ export function UserMenu({
|
|||||||
if (!isPasswordlessUser && !preventNavigation) {
|
if (!isPasswordlessUser && !preventNavigation) {
|
||||||
arr.push({
|
arr.push({
|
||||||
key: "logout",
|
key: "logout",
|
||||||
label: "Sign Out",
|
label: t("Sign out"),
|
||||||
icon: LogoutIcon,
|
icon: LogoutIcon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation]);
|
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation, t]);
|
||||||
|
|
||||||
|
const tooltip = useCallback(() => t("Profile"), [t]);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
@@ -62,11 +76,11 @@ export function UserMenu({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<TooltipTrigger placement="bottom left">
|
<TooltipTrigger tooltip={tooltip} placement="bottom left">
|
||||||
<Button variant="icon" className={styles.userButton}>
|
<Button variant="icon" className={styles.userButton}>
|
||||||
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size={Size.SM}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
@@ -75,12 +89,11 @@ export function UserMenu({
|
|||||||
<UserIcon />
|
<UserIcon />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Profile"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="User menu" onAction={onAction}>
|
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
||||||
{items.map(({ key, icon: Icon, label }) => (
|
{items.map(({ key, icon: Icon, label }) => (
|
||||||
<Item key={key} textValue={label} className={styles.menuItem}>
|
<Item key={key} textValue={label}>
|
||||||
<Icon width={24} height={24} className={styles.menuIcon} />
|
<Icon width={24} height={24} className={styles.menuIcon} />
|
||||||
<Body overflowEllipsis>{label}</Body>
|
<Body overflowEllipsis>{label}</Body>
|
||||||
</Item>
|
</Item>
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { useClient } from "./ClientContext";
|
import { useClient } from "./ClientContext";
|
||||||
import { useProfile } from "./profile/useProfile";
|
import { useProfile } from "./profile/useProfile";
|
||||||
import { useModalTriggerState } from "./Modal";
|
import { useModalTriggerState } from "./Modal";
|
||||||
import { ProfileModal } from "./profile/ProfileModal";
|
import { ProfileModal } from "./profile/ProfileModal";
|
||||||
import { UserMenu } from "./UserMenu";
|
import { UserMenu } from "./UserMenu";
|
||||||
|
|
||||||
export function UserMenuContainer({ preventNavigation }) {
|
interface Props {
|
||||||
|
preventNavigation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||||
@@ -15,7 +20,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
|||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
const onAction = useCallback(
|
const onAction = useCallback(
|
||||||
(value) => {
|
(value: string) => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "user":
|
case "user":
|
||||||
modalState.open();
|
modalState.open();
|
||||||
@@ -43,14 +48,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
|||||||
displayName || (userName ? userName.replace("@", "") : undefined)
|
displayName || (userName ? userName.replace("@", "") : undefined)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{modalState.isOpen && (
|
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
|
||||||
<ProfileModal
|
|
||||||
client={client}
|
|
||||||
isAuthenticated={isAuthenticated}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
{...modalProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.authLinks a {
|
.authLinks a {
|
||||||
color: #0dbd8b;
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,9 +14,19 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState, useMemo } from "react";
|
import React, {
|
||||||
|
FC,
|
||||||
|
FormEvent,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
||||||
@@ -24,27 +34,31 @@ import styles from "./LoginPage.module.css";
|
|||||||
import { useInteractiveLogin } from "./useInteractiveLogin";
|
import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function LoginPage() {
|
export const LoginPage: FC = () => {
|
||||||
usePageTitle("Login");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Login"));
|
||||||
|
|
||||||
const [_, login] = useInteractiveLogin();
|
const { setClient } = useClient();
|
||||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
const login = useInteractiveLogin();
|
||||||
const usernameRef = useRef();
|
const homeserver = defaultHomeserver; // TODO: Make this configurable
|
||||||
const passwordRef = useRef();
|
const usernameRef = useRef<HTMLInputElement>();
|
||||||
|
const passwordRef = useRef<HTMLInputElement>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState<Error>();
|
||||||
|
|
||||||
// TODO: Handle hitting login page with authenticated client
|
// TODO: Handle hitting login page with authenticated client
|
||||||
|
|
||||||
const onSubmitLoginForm = useCallback(
|
const onSubmitLoginForm = useCallback(
|
||||||
(e) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||||
.then(() => {
|
.then(([client, session]) => {
|
||||||
|
setClient(client, session);
|
||||||
|
|
||||||
if (location.state && location.state.from) {
|
if (location.state && location.state.from) {
|
||||||
history.push(location.state.from);
|
history.push(location.state.from);
|
||||||
} else {
|
} else {
|
||||||
@@ -56,13 +70,13 @@ export function LoginPage() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[login, location, history, homeserver]
|
[login, location, history, homeserver, setClient]
|
||||||
);
|
);
|
||||||
|
|
||||||
const homeserverHost = useMemo(() => {
|
const homeserverHost = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
return new URL(homeserver).host;
|
return new URL(homeserver).host;
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
return defaultHomeserverHost;
|
return defaultHomeserverHost;
|
||||||
}
|
}
|
||||||
}, [homeserver]);
|
}, [homeserver]);
|
||||||
@@ -81,8 +95,8 @@ export function LoginPage() {
|
|||||||
<InputField
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
placeholder="Username"
|
placeholder={t("Username")}
|
||||||
label="Username"
|
label={t("Username")}
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
prefix="@"
|
prefix="@"
|
||||||
@@ -93,18 +107,18 @@ export function LoginPage() {
|
|||||||
<InputField
|
<InputField
|
||||||
type="password"
|
type="password"
|
||||||
ref={passwordRef}
|
ref={passwordRef}
|
||||||
placeholder="Password"
|
placeholder={t("Password")}
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button type="submit" disabled={loading}>
|
<Button type="submit" disabled={loading}>
|
||||||
{loading ? "Logging in..." : "Login"}
|
{loading ? t("Logging in…") : t("Login")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</form>
|
</form>
|
||||||
@@ -112,13 +126,15 @@ export function LoginPage() {
|
|||||||
<div className={styles.authLinks}>
|
<div className={styles.authLinks}>
|
||||||
<p>Not registered yet?</p>
|
<p>Not registered yet?</p>
|
||||||
<p>
|
<p>
|
||||||
<Link to="/register">Create an account</Link>
|
<Trans>
|
||||||
{" Or "}
|
<Link to="/register">Create an account</Link>
|
||||||
<Link to="/">Access as a guest</Link>
|
{" Or "}
|
||||||
|
<Link to="/">Access as a guest</Link>
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,8 +14,20 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, {
|
||||||
|
ChangeEvent,
|
||||||
|
FC,
|
||||||
|
FormEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { captureException } from "@sentry/react";
|
||||||
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@@ -28,68 +40,70 @@ import { useRecaptcha } from "./useRecaptcha";
|
|||||||
import { Caption, Link } from "../typography/Typography";
|
import { Caption, Link } from "../typography/Typography";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function RegisterPage() {
|
export const RegisterPage: FC = () => {
|
||||||
usePageTitle("Register");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Register"));
|
||||||
|
|
||||||
const { loading, isAuthenticated, isPasswordlessUser, client } = useClient();
|
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
||||||
const confirmPasswordRef = useRef();
|
useClient();
|
||||||
|
const confirmPasswordRef = useRef<HTMLInputElement>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [registering, setRegistering] = useState(false);
|
const [registering, setRegistering] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState<Error>();
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
const [privacyPolicyUrl, recaptchaKey, register] =
|
||||||
useInteractiveRegistration();
|
useInteractiveRegistration();
|
||||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||||
|
|
||||||
const onSubmitRegisterForm = useCallback(
|
const onSubmitRegisterForm = useCallback(
|
||||||
(e) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target as HTMLFormElement);
|
||||||
const userName = data.get("userName");
|
const userName = data.get("userName") as string;
|
||||||
const password = data.get("password");
|
const password = data.get("password") as string;
|
||||||
const passwordConfirmation = data.get("passwordConfirmation");
|
const passwordConfirmation = data.get("passwordConfirmation") as string;
|
||||||
|
|
||||||
if (password !== passwordConfirmation) {
|
if (password !== passwordConfirmation) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
const submit = async () => {
|
||||||
setRegistering(true);
|
setRegistering(true);
|
||||||
|
|
||||||
let roomIds;
|
|
||||||
|
|
||||||
if (client && isPasswordlessUser) {
|
|
||||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
|
||||||
roomIds = Array.from(groupCalls).map(
|
|
||||||
(groupCall) => groupCall.room.roomId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
const newClient = await register(
|
const [newClient, session] = await register(
|
||||||
userName,
|
userName,
|
||||||
password,
|
password,
|
||||||
userName,
|
userName,
|
||||||
recaptchaResponse
|
recaptchaResponse
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roomIds) {
|
if (client && isPasswordlessUser) {
|
||||||
for (const roomId of roomIds) {
|
// Migrate the user's rooms
|
||||||
|
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
|
||||||
|
const roomId = groupCall.room.roomId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await newClient.joinRoom(roomId);
|
await newClient.joinRoom(roomId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Couldn't join room ${roomId}`, error);
|
if (error.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
|
await sleep(error.data.retry_after_ms);
|
||||||
|
await newClient.joinRoom(roomId);
|
||||||
|
} else {
|
||||||
|
captureException(error);
|
||||||
|
console.error(`Couldn't join room ${roomId}`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
setClient(newClient, session);
|
||||||
|
};
|
||||||
|
|
||||||
submit()
|
submit()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (location.state && location.state.from) {
|
if (location.state?.from) {
|
||||||
history.push(location.state.from);
|
history.push(location.state?.from);
|
||||||
} else {
|
} else {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
@@ -100,26 +114,31 @@ export function RegisterPage() {
|
|||||||
reset();
|
reset();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[register, location, history, isPasswordlessUser, reset, execute, client]
|
[
|
||||||
|
register,
|
||||||
|
location,
|
||||||
|
history,
|
||||||
|
isPasswordlessUser,
|
||||||
|
reset,
|
||||||
|
execute,
|
||||||
|
client,
|
||||||
|
setClient,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!confirmPasswordRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
||||||
confirmPasswordRef.current.setCustomValidity("Passwords must match");
|
confirmPasswordRef.current?.setCustomValidity(t("Passwords must match"));
|
||||||
} else {
|
} else {
|
||||||
confirmPasswordRef.current.setCustomValidity("");
|
confirmPasswordRef.current?.setCustomValidity("");
|
||||||
}
|
}
|
||||||
}, [password, passwordConfirmation]);
|
}, [password, passwordConfirmation, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [history, isAuthenticated, isPasswordlessUser, registering]);
|
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
@@ -137,8 +156,8 @@ export function RegisterPage() {
|
|||||||
<InputField
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
name="userName"
|
name="userName"
|
||||||
placeholder="Username"
|
placeholder={t("Username")}
|
||||||
label="Username"
|
label={t("Username")}
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
prefix="@"
|
prefix="@"
|
||||||
@@ -150,10 +169,12 @@ export function RegisterPage() {
|
|||||||
required
|
required
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPassword(e.target.value)
|
||||||
|
}
|
||||||
value={password}
|
value={password}
|
||||||
placeholder="Password"
|
placeholder={t("Password")}
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
@@ -161,50 +182,56 @@ export function RegisterPage() {
|
|||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
name="passwordConfirmation"
|
name="passwordConfirmation"
|
||||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordConfirmation(e.target.value)
|
||||||
|
}
|
||||||
value={passwordConfirmation}
|
value={passwordConfirmation}
|
||||||
placeholder="Confirm Password"
|
placeholder={t("Confirm password")}
|
||||||
label="Confirm Password"
|
label={t("Confirm password")}
|
||||||
ref={confirmPasswordRef}
|
ref={confirmPasswordRef}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Caption>
|
<Caption>
|
||||||
This site is protected by ReCAPTCHA and the Google{" "}
|
<Trans>
|
||||||
<Link href="https://www.google.com/policies/privacy/">
|
This site is protected by ReCAPTCHA and the Google{" "}
|
||||||
Privacy Policy
|
<Link href="https://www.google.com/policies/privacy/">
|
||||||
</Link>{" "}
|
Privacy Policy
|
||||||
and{" "}
|
</Link>{" "}
|
||||||
<Link href="https://policies.google.com/terms">
|
and{" "}
|
||||||
Terms of Service
|
<Link href="https://policies.google.com/terms">
|
||||||
</Link>{" "}
|
Terms of Service
|
||||||
apply.
|
</Link>{" "}
|
||||||
<br />
|
apply.
|
||||||
By clicking "Register", you agree to our{" "}
|
<br />
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
By clicking "Register", you agree to our{" "}
|
||||||
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
|
</Trans>
|
||||||
</Caption>
|
</Caption>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button type="submit" disabled={registering}>
|
<Button type="submit" disabled={registering}>
|
||||||
{registering ? "Registering..." : "Register"}
|
{registering ? t("Registering…") : t("Register")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<div id={recaptchaId} />
|
<div id={recaptchaId} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.authLinks}>
|
<div className={styles.authLinks}>
|
||||||
<p>Already have an account?</p>
|
<Trans>
|
||||||
<p>
|
<p>Already have an account?</p>
|
||||||
<Link to="/login">Log in</Link>
|
<p>
|
||||||
{" Or "}
|
<Link to="/login">Log in</Link>
|
||||||
<Link to="/">Access as a guest</Link>
|
{" Or "}
|
||||||
</p>
|
<Link to="/">Access as a guest</Link>
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
uniqueNamesGenerator,
|
uniqueNamesGenerator,
|
||||||
adjectives,
|
adjectives,
|
||||||
@@ -126,12 +142,11 @@ const elements = [
|
|||||||
"oganesson",
|
"oganesson",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function generateRandomName(config) {
|
export function generateRandomName(): string {
|
||||||
return uniqueNamesGenerator({
|
return uniqueNamesGenerator({
|
||||||
dictionaries: [colors, adjectives, animals, elements],
|
dictionaries: [colors, adjectives, animals, elements],
|
||||||
style: "lowerCase",
|
style: "lowerCase",
|
||||||
length: 3,
|
length: 3,
|
||||||
separator: "-",
|
separator: "-",
|
||||||
...config,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
|
||||||
import { useState, useCallback } from "react";
|
|
||||||
import { useClient } from "../ClientContext";
|
|
||||||
import { initClient, defaultHomeserver } from "../matrix-utils";
|
|
||||||
|
|
||||||
export function useInteractiveLogin() {
|
|
||||||
const { setClient } = useClient();
|
|
||||||
const [state, setState] = useState({ loading: false });
|
|
||||||
|
|
||||||
const auth = useCallback(async (homeserver, username, password) => {
|
|
||||||
const authClient = matrix.createClient(homeserver);
|
|
||||||
|
|
||||||
const interactiveAuth = new InteractiveAuth({
|
|
||||||
matrixClient: authClient,
|
|
||||||
busyChanged(loading) {
|
|
||||||
setState((prev) => ({ ...prev, loading }));
|
|
||||||
},
|
|
||||||
async doRequest(_auth, _background) {
|
|
||||||
return authClient.login("m.login.password", {
|
|
||||||
identifier: {
|
|
||||||
type: "m.id.user",
|
|
||||||
user: username,
|
|
||||||
},
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user_id, access_token, device_id } =
|
|
||||||
await interactiveAuth.attemptAuth();
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
setClient(client, { user_id, access_token, device_id });
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [state, auth];
|
|
||||||
}
|
|
||||||
72
src/auth/useInteractiveLogin.ts
Normal file
72
src/auth/useInteractiveLogin.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||||
|
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { initClient, defaultHomeserver } from "../matrix-utils";
|
||||||
|
import { Session } from "../ClientContext";
|
||||||
|
|
||||||
|
export const useInteractiveLogin = () =>
|
||||||
|
useCallback<
|
||||||
|
(
|
||||||
|
homeserver: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
) => Promise<[MatrixClient, Session]>
|
||||||
|
>(async (homeserver: string, username: string, password: string) => {
|
||||||
|
const authClient = createClient({ baseUrl: homeserver });
|
||||||
|
|
||||||
|
const interactiveAuth = new InteractiveAuth({
|
||||||
|
matrixClient: authClient,
|
||||||
|
doRequest: () =>
|
||||||
|
authClient.login("m.login.password", {
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: username,
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
stateUpdated: null,
|
||||||
|
requestEmailToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: This claims to return an IAuthData which contains none of these
|
||||||
|
// things - the js-sdk types may be wrong?
|
||||||
|
/* eslint-disable camelcase,@typescript-eslint/no-explicit-any */
|
||||||
|
const { user_id, access_token, device_id } =
|
||||||
|
(await interactiveAuth.attemptAuth()) as any;
|
||||||
|
const session = {
|
||||||
|
user_id,
|
||||||
|
access_token,
|
||||||
|
device_id,
|
||||||
|
passwordlessUser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await initClient(
|
||||||
|
{
|
||||||
|
baseUrl: defaultHomeserver,
|
||||||
|
accessToken: access_token,
|
||||||
|
userId: user_id,
|
||||||
|
deviceId: device_id,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
return [client, session];
|
||||||
|
}, []);
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
import { useClient } from "../ClientContext";
|
|
||||||
import { initClient, defaultHomeserver } from "../matrix-utils";
|
|
||||||
|
|
||||||
export function useInteractiveRegistration() {
|
|
||||||
const { setClient } = useClient();
|
|
||||||
const [state, setState] = useState({ privacyPolicyUrl: "#", loading: false });
|
|
||||||
|
|
||||||
const authClientRef = useRef();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
authClientRef.current = matrix.createClient(defaultHomeserver);
|
|
||||||
|
|
||||||
authClientRef.current.registerRequest({}).catch((error) => {
|
|
||||||
const privacyPolicyUrl =
|
|
||||||
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url;
|
|
||||||
|
|
||||||
const recaptchaKey = error.data?.params["m.login.recaptcha"]?.public_key;
|
|
||||||
|
|
||||||
if (privacyPolicyUrl || recaptchaKey) {
|
|
||||||
setState((prev) => ({ ...prev, privacyPolicyUrl, recaptchaKey }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const register = useCallback(
|
|
||||||
async (
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
displayName,
|
|
||||||
recaptchaResponse,
|
|
||||||
passwordlessUser
|
|
||||||
) => {
|
|
||||||
const interactiveAuth = new InteractiveAuth({
|
|
||||||
matrixClient: authClientRef.current,
|
|
||||||
busyChanged(loading) {
|
|
||||||
setState((prev) => ({ ...prev, loading }));
|
|
||||||
},
|
|
||||||
async doRequest(auth, _background) {
|
|
||||||
return authClientRef.current.registerRequest({
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
auth: auth || undefined,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
stateUpdated(nextStage, status) {
|
|
||||||
if (status.error) {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStage === "m.login.terms") {
|
|
||||||
interactiveAuth.submitAuthDict({
|
|
||||||
type: "m.login.terms",
|
|
||||||
});
|
|
||||||
} else if (nextStage === "m.login.recaptcha") {
|
|
||||||
interactiveAuth.submitAuthDict({
|
|
||||||
type: "m.login.recaptcha",
|
|
||||||
response: recaptchaResponse,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user_id, access_token, device_id } =
|
|
||||||
await interactiveAuth.attemptAuth();
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.setDisplayName(displayName);
|
|
||||||
|
|
||||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
|
||||||
|
|
||||||
if (passwordlessUser) {
|
|
||||||
session.tempPassword = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
setClient(client, session);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return [state, register];
|
|
||||||
}
|
|
||||||
127
src/auth/useInteractiveRegistration.ts
Normal file
127
src/auth/useInteractiveRegistration.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||||
|
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { initClient, defaultHomeserver } from "../matrix-utils";
|
||||||
|
import { Session } from "../ClientContext";
|
||||||
|
|
||||||
|
export const useInteractiveRegistration = (): [
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName: string,
|
||||||
|
recaptchaResponse: string,
|
||||||
|
passwordlessUser?: boolean
|
||||||
|
) => Promise<[MatrixClient, Session]>
|
||||||
|
] => {
|
||||||
|
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>();
|
||||||
|
const [recaptchaKey, setRecaptchaKey] = useState<string>();
|
||||||
|
|
||||||
|
const authClient = useRef<MatrixClient>();
|
||||||
|
if (!authClient.current) {
|
||||||
|
authClient.current = createClient({ baseUrl: defaultHomeserver });
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authClient.current.registerRequest({}).catch((error) => {
|
||||||
|
setPrivacyPolicyUrl(
|
||||||
|
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
|
||||||
|
);
|
||||||
|
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const register = useCallback(
|
||||||
|
async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName: string,
|
||||||
|
recaptchaResponse: string,
|
||||||
|
passwordlessUser?: boolean
|
||||||
|
): Promise<[MatrixClient, Session]> => {
|
||||||
|
const interactiveAuth = new InteractiveAuth({
|
||||||
|
matrixClient: authClient.current,
|
||||||
|
doRequest: (auth) =>
|
||||||
|
authClient.current.registerRequest({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
auth: auth || undefined,
|
||||||
|
}),
|
||||||
|
stateUpdated: (nextStage, status) => {
|
||||||
|
if (status.error) {
|
||||||
|
throw new Error(status.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStage === "m.login.terms") {
|
||||||
|
interactiveAuth.submitAuthDict({
|
||||||
|
type: "m.login.terms",
|
||||||
|
});
|
||||||
|
} else if (nextStage === "m.login.recaptcha") {
|
||||||
|
interactiveAuth.submitAuthDict({
|
||||||
|
type: "m.login.recaptcha",
|
||||||
|
response: recaptchaResponse,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestEmailToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: This claims to return an IAuthData which contains none of these
|
||||||
|
// things - the js-sdk types may be wrong?
|
||||||
|
/* eslint-disable camelcase,@typescript-eslint/no-explicit-any */
|
||||||
|
const { user_id, access_token, device_id } =
|
||||||
|
(await interactiveAuth.attemptAuth()) as any;
|
||||||
|
|
||||||
|
const client = await initClient(
|
||||||
|
{
|
||||||
|
baseUrl: defaultHomeserver,
|
||||||
|
accessToken: access_token,
|
||||||
|
userId: user_id,
|
||||||
|
deviceId: device_id,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.setDisplayName(displayName);
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
access_token,
|
||||||
|
passwordlessUser,
|
||||||
|
};
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
if (passwordlessUser) {
|
||||||
|
session.tempPassword = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = client.getUser(client.getUserId());
|
||||||
|
user.setRawDisplayName(displayName);
|
||||||
|
user.setDisplayName(displayName);
|
||||||
|
|
||||||
|
return [client, session];
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [privacyPolicyUrl, recaptchaKey, register];
|
||||||
|
};
|
||||||
@@ -1,49 +1,66 @@
|
|||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect, useCallback, useRef, useState } from "react";
|
import { useEffect, useCallback, useRef, useState } from "react";
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { translatedError } from "../TranslatedError";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
mxOnRecaptchaLoaded: () => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const RECAPTCHA_SCRIPT_URL =
|
const RECAPTCHA_SCRIPT_URL =
|
||||||
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
||||||
|
|
||||||
export function useRecaptcha(sitekey) {
|
interface RecaptchaPromiseRef {
|
||||||
|
resolve: (response: string) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRecaptcha = (sitekey: string) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [recaptchaId] = useState(() => randomString(16));
|
const [recaptchaId] = useState(() => randomString(16));
|
||||||
const promiseRef = useRef();
|
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sitekey) {
|
if (!sitekey) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRecaptchaLoaded = () => {
|
const onRecaptchaLoaded = () => {
|
||||||
if (!document.getElementById(recaptchaId)) {
|
if (!document.getElementById(recaptchaId)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.grecaptcha.render(recaptchaId, {
|
window.grecaptcha.render(recaptchaId, {
|
||||||
sitekey,
|
sitekey,
|
||||||
size: "invisible",
|
size: "invisible",
|
||||||
callback: (response) => {
|
callback: (response: string) => promiseRef.current?.resolve(response),
|
||||||
if (promiseRef.current) {
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
promiseRef.current.resolve(response);
|
"error-callback": () => promiseRef.current?.reject(new Error()),
|
||||||
}
|
|
||||||
},
|
|
||||||
"error-callback": (error) => {
|
|
||||||
if (promiseRef.current) {
|
|
||||||
promiseRef.current.reject(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (typeof window.grecaptcha?.render === "function") {
|
||||||
typeof window.grecaptcha !== "undefined" &&
|
|
||||||
typeof window.grecaptcha.render === "function"
|
|
||||||
) {
|
|
||||||
onRecaptchaLoaded();
|
onRecaptchaLoaded();
|
||||||
} else {
|
} else {
|
||||||
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
||||||
|
|
||||||
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
||||||
const scriptTag = document.createElement("script");
|
const scriptTag = document.createElement("script") as HTMLScriptElement;
|
||||||
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
||||||
scriptTag.async = true;
|
scriptTag.async = true;
|
||||||
document.body.appendChild(scriptTag);
|
document.body.appendChild(scriptTag);
|
||||||
@@ -58,14 +75,14 @@ export function useRecaptcha(sitekey) {
|
|||||||
|
|
||||||
if (!window.grecaptcha) {
|
if (!window.grecaptcha) {
|
||||||
console.log("Recaptcha not loaded");
|
console.log("Recaptcha not loaded");
|
||||||
return Promise.reject(new Error("Recaptcha not loaded"));
|
return Promise.reject(translatedError("Recaptcha not loaded", t));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const observer = new MutationObserver((mutationsList) => {
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
for (const item of mutationsList) {
|
for (const item of mutationsList) {
|
||||||
if (item.target?.style?.visibility !== "visible") {
|
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
|
||||||
reject(new Error("Recaptcha dismissed"));
|
reject(translatedError("Recaptcha dismissed", t));
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -85,7 +102,7 @@ export function useRecaptcha(sitekey) {
|
|||||||
|
|
||||||
window.grecaptcha.execute();
|
window.grecaptcha.execute();
|
||||||
|
|
||||||
const iframe = document.querySelector(
|
const iframe = document.querySelector<HTMLIFrameElement>(
|
||||||
'iframe[src*="recaptcha/api2/bframe"]'
|
'iframe[src*="recaptcha/api2/bframe"]'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,13 +112,11 @@ export function useRecaptcha(sitekey) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [recaptchaId, sitekey]);
|
}, [sitekey, t]);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
if (window.grecaptcha) {
|
window.grecaptcha?.reset();
|
||||||
window.grecaptcha.reset();
|
}, []);
|
||||||
}
|
|
||||||
}, [recaptchaId]);
|
|
||||||
|
|
||||||
return { execute, reset, recaptchaId };
|
return { execute, reset, recaptchaId };
|
||||||
}
|
};
|
||||||
59
src/auth/useRegisterPasswordlessUser.ts
Normal file
59
src/auth/useRegisterPasswordlessUser.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
|
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||||
|
import { generateRandomName } from "../auth/generateRandomName";
|
||||||
|
import { useRecaptcha } from "../auth/useRecaptcha";
|
||||||
|
|
||||||
|
export interface UseRegisterPasswordlessUserType {
|
||||||
|
privacyPolicyUrl: string;
|
||||||
|
registerPasswordlessUser: (displayName: string) => Promise<void>;
|
||||||
|
recaptchaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
||||||
|
const { setClient } = useClient();
|
||||||
|
const [privacyPolicyUrl, recaptchaKey, register] =
|
||||||
|
useInteractiveRegistration();
|
||||||
|
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||||
|
|
||||||
|
const registerPasswordlessUser = useCallback(
|
||||||
|
async (displayName: string) => {
|
||||||
|
try {
|
||||||
|
const recaptchaResponse = await execute();
|
||||||
|
const userName = generateRandomName();
|
||||||
|
const [client, session] = await register(
|
||||||
|
userName,
|
||||||
|
randomString(16),
|
||||||
|
displayName,
|
||||||
|
recaptchaResponse,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
setClient(client, session);
|
||||||
|
} catch (e) {
|
||||||
|
reset();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[execute, reset, register, setClient]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId };
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import React, { forwardRef } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./Button.module.css";
|
|
||||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
|
||||||
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
|
||||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
|
||||||
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
|
||||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
|
||||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
|
||||||
import { useButton } from "@react-aria/button";
|
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
|
||||||
|
|
||||||
export const variantToClassName = {
|
|
||||||
default: [styles.button],
|
|
||||||
toolbar: [styles.toolbarButton],
|
|
||||||
toolbarSecondary: [styles.toolbarButtonSecondary],
|
|
||||||
icon: [styles.iconButton],
|
|
||||||
secondary: [styles.secondary],
|
|
||||||
copy: [styles.copyButton],
|
|
||||||
iconCopy: [styles.iconCopyButton],
|
|
||||||
secondaryCopy: [styles.copyButton],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sizeToClassName = {
|
|
||||||
lg: [styles.lg],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Button = forwardRef(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
variant = "default",
|
|
||||||
size,
|
|
||||||
on,
|
|
||||||
off,
|
|
||||||
iconStyle,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
onPress,
|
|
||||||
onPressStart,
|
|
||||||
...rest
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const buttonRef = useObjectRef(ref);
|
|
||||||
const { buttonProps } = useButton(
|
|
||||||
{ onPress, onPressStart, ...rest },
|
|
||||||
buttonRef
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
|
||||||
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
|
||||||
let filteredButtonProps = buttonProps;
|
|
||||||
|
|
||||||
if (rest.type === "submit" && !rest.onPress) {
|
|
||||||
const { onKeyDown, onKeyUp, ...filtered } = buttonProps;
|
|
||||||
filteredButtonProps = filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames(
|
|
||||||
variantToClassName[variant],
|
|
||||||
sizeToClassName[size],
|
|
||||||
styles[iconStyle],
|
|
||||||
className,
|
|
||||||
{
|
|
||||||
[styles.on]: on,
|
|
||||||
[styles.off]: off,
|
|
||||||
[styles.secondaryCopy]: variant === "secondaryCopy",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{...mergeProps(rest, filteredButtonProps)}
|
|
||||||
ref={buttonRef}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function MicButton({ muted, ...rest }) {
|
|
||||||
return (
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
|
||||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
|
||||||
</Button>
|
|
||||||
{() => (muted ? "Unmute microphone" : "Mute microphone")}
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VideoButton({ muted, ...rest }) {
|
|
||||||
return (
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
|
||||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
|
||||||
</Button>
|
|
||||||
{() => (muted ? "Turn on camera" : "Turn off camera")}
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScreenshareButton({ enabled, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
|
||||||
<ScreenshareIcon />
|
|
||||||
</Button>
|
|
||||||
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HangupButton({ className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button
|
|
||||||
variant="toolbar"
|
|
||||||
className={classNames(styles.hangupButton, className)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<HangupIcon />
|
|
||||||
</Button>
|
|
||||||
{() => "Leave"}
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,9 @@ limitations under the License.
|
|||||||
.iconButton,
|
.iconButton,
|
||||||
.iconCopyButton,
|
.iconCopyButton,
|
||||||
.secondary,
|
.secondary,
|
||||||
.copyButton {
|
.secondaryHangup,
|
||||||
|
.copyButton,
|
||||||
|
.dropdownButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -34,6 +36,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
|
.secondaryHangup,
|
||||||
.button,
|
.button,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
padding: 7px 15px;
|
padding: 7px 15px;
|
||||||
@@ -43,8 +46,8 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
color: #fff;
|
color: var(--primary-content);
|
||||||
background-color: var(--primaryColor);
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus,
|
.button:focus,
|
||||||
@@ -53,6 +56,7 @@ limitations under the License.
|
|||||||
.iconButton:focus,
|
.iconButton:focus,
|
||||||
.iconCopyButton:focus,
|
.iconCopyButton:focus,
|
||||||
.secondary:focus,
|
.secondary:focus,
|
||||||
|
.secondaryHangup:focus,
|
||||||
.copyButton:focus {
|
.copyButton:focus {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
@@ -62,46 +66,46 @@ limitations under the License.
|
|||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton:hover,
|
.toolbarButton:hover,
|
||||||
.toolbarButtonSecondary:hover {
|
.toolbarButtonSecondary:hover {
|
||||||
background-color: var(--bgColor4);
|
background-color: var(--quinary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.on,
|
.toolbarButton.on,
|
||||||
.toolbarButton.off {
|
.toolbarButton.off {
|
||||||
background-color: #ffffff;
|
background-color: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButtonSecondary.on {
|
.toolbarButtonSecondary.on {
|
||||||
background-color: #0dbd8b;
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton:not(.stroke) svg * {
|
.iconButton:not(.stroke) svg * {
|
||||||
fill: #ffffff;
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton:not(.stroke):hover svg * {
|
.iconButton:not(.stroke):hover svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton.on:not(.stroke) svg * {
|
.iconButton.on:not(.stroke) svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton.on.stroke svg * {
|
.iconButton.on.stroke svg * {
|
||||||
stroke: #0dbd8b;
|
stroke: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hangupButton,
|
.hangupButton,
|
||||||
.hangupButton:hover {
|
.hangupButton:hover {
|
||||||
background-color: #ff5b55;
|
background-color: var(--alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.on svg * {
|
.toolbarButton.on svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.off svg * {
|
.toolbarButton.off svg * {
|
||||||
@@ -109,19 +113,25 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButtonSecondary.on svg * {
|
.toolbarButtonSecondary.on svg * {
|
||||||
fill: #ffffff;
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
color: #0dbd8b;
|
color: var(--accent);
|
||||||
border: 2px solid #0dbd8b;
|
border: 2px solid var(--accent);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryHangup {
|
||||||
|
color: var(--alert);
|
||||||
|
border: 2px solid var(--alert);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton.secondaryCopy {
|
.copyButton.secondaryCopy {
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
border-color: var(--textColor1);
|
border-color: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton {
|
.copyButton {
|
||||||
@@ -144,12 +154,12 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.copyButton:not(.on) svg * {
|
.copyButton:not(.on) svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton.on {
|
.copyButton.on {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: #0dbd8b;
|
background-color: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,23 +168,49 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.copyButton.secondaryCopy:not(.on) svg * {
|
.copyButton.secondaryCopy:not(.on) svg * {
|
||||||
fill: var(--textColor1);
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCopyButton svg * {
|
.iconCopyButton svg * {
|
||||||
fill: var(--textColor3);
|
fill: var(--tertiary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCopyButton:hover svg * {
|
.iconCopyButton:hover svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCopyButton.on svg *,
|
.iconCopyButton.on svg *,
|
||||||
.iconCopyButton.on:hover svg * {
|
.iconCopyButton.on:hover svg * {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: #0dbd8b;
|
stroke: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton {
|
||||||
|
color: var(--primary-content);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton:hover,
|
||||||
|
.dropdownButton.on {
|
||||||
|
background-color: var(--quinary-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton svg {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton svg * {
|
||||||
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg {
|
.lg {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.linkButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
307
src/button/Button.tsx
Normal file
307
src/button/Button.tsx
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import React, { forwardRef, useCallback } from "react";
|
||||||
|
import { PressEvent } from "@react-types/shared";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useButton } from "@react-aria/button";
|
||||||
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import styles from "./Button.module.css";
|
||||||
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
|
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
||||||
|
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
|
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||||
|
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||||
|
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||||
|
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||||
|
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||||
|
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
||||||
|
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
|
||||||
|
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
|
||||||
|
import { TooltipTrigger } from "../Tooltip";
|
||||||
|
import { VolumeIcon } from "./VolumeIcon";
|
||||||
|
|
||||||
|
export type ButtonVariant =
|
||||||
|
| "default"
|
||||||
|
| "toolbar"
|
||||||
|
| "toolbarSecondary"
|
||||||
|
| "icon"
|
||||||
|
| "secondary"
|
||||||
|
| "copy"
|
||||||
|
| "secondaryCopy"
|
||||||
|
| "iconCopy"
|
||||||
|
| "secondaryHangup"
|
||||||
|
| "dropdown"
|
||||||
|
| "link";
|
||||||
|
|
||||||
|
export const variantToClassName = {
|
||||||
|
default: [styles.button],
|
||||||
|
toolbar: [styles.toolbarButton],
|
||||||
|
toolbarSecondary: [styles.toolbarButtonSecondary],
|
||||||
|
icon: [styles.iconButton],
|
||||||
|
secondary: [styles.secondary],
|
||||||
|
copy: [styles.copyButton],
|
||||||
|
secondaryCopy: [styles.secondaryCopy, styles.copyButton],
|
||||||
|
iconCopy: [styles.iconCopyButton],
|
||||||
|
secondaryHangup: [styles.secondaryHangup],
|
||||||
|
dropdown: [styles.dropdownButton],
|
||||||
|
link: [styles.linkButton],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ButtonSize = "lg";
|
||||||
|
|
||||||
|
export const sizeToClassName: { lg: string[] } = {
|
||||||
|
lg: [styles.lg],
|
||||||
|
};
|
||||||
|
interface Props {
|
||||||
|
variant: ButtonVariant;
|
||||||
|
size: ButtonSize;
|
||||||
|
on: () => void;
|
||||||
|
off: () => void;
|
||||||
|
iconStyle: string;
|
||||||
|
className: string;
|
||||||
|
children: Element[];
|
||||||
|
onPress: (e: PressEvent) => void;
|
||||||
|
onPressStart: (e: PressEvent) => void;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}
|
||||||
|
export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
variant = "default",
|
||||||
|
size,
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
iconStyle,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
onPressStart,
|
||||||
|
...rest
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const buttonRef = useObjectRef<HTMLButtonElement>(ref);
|
||||||
|
const { buttonProps } = useButton(
|
||||||
|
{ onPress, onPressStart, ...rest },
|
||||||
|
buttonRef
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
||||||
|
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
||||||
|
let filteredButtonProps = buttonProps;
|
||||||
|
|
||||||
|
if (rest.type === "submit" && !rest.onPress) {
|
||||||
|
const { ...filtered } = buttonProps;
|
||||||
|
filteredButtonProps = filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
variantToClassName[variant],
|
||||||
|
sizeToClassName[size],
|
||||||
|
styles[iconStyle],
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
[styles.on]: on,
|
||||||
|
[styles.off]: off,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{...mergeProps(rest, filteredButtonProps)}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
{variant === "dropdown" && <ArrowDownIcon />}
|
||||||
|
</>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function MicButton({
|
||||||
|
muted,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
muted: boolean;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger
|
||||||
|
tooltip={() => (muted ? t("Unmute microphone") : t("Mute microphone"))}
|
||||||
|
>
|
||||||
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
|
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoButton({
|
||||||
|
muted,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
muted: boolean;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger
|
||||||
|
tooltip={() => (muted ? t("Turn on camera") : t("Turn off camera"))}
|
||||||
|
>
|
||||||
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
|
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenshareButton({
|
||||||
|
enabled,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
enabled: boolean;
|
||||||
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger
|
||||||
|
tooltip={() => (enabled ? t("Stop sharing screen") : t("Share screen"))}
|
||||||
|
>
|
||||||
|
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||||
|
<ScreenshareIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HangupButton({
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Leave"), [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
|
<Button
|
||||||
|
variant="toolbar"
|
||||||
|
className={classNames(styles.hangupButton, className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<HangupIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsButton({
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Settings"), [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
|
<Button variant="toolbar" {...rest}>
|
||||||
|
<SettingsIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InviteButton({
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Invite"), [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
|
<Button variant="toolbar" {...rest}>
|
||||||
|
<AddUserIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AudioButtonProps extends Omit<Props, "variant"> {
|
||||||
|
/**
|
||||||
|
* A number between 0 and 1
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Local volume"), [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
|
<Button variant="icon" {...rest}>
|
||||||
|
<VolumeIcon volume={volume} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FullscreenButtonProps extends Omit<Props, "variant"> {
|
||||||
|
fullscreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FullscreenButton({
|
||||||
|
fullscreen,
|
||||||
|
...rest
|
||||||
|
}: FullscreenButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => {
|
||||||
|
return fullscreen ? t("Exit full screen") : t("Full screen");
|
||||||
|
}, [fullscreen, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
|
<Button variant="icon" {...rest}>
|
||||||
|
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import useClipboard from "react-use-clipboard";
|
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
|
||||||
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
|
|
||||||
import { Button } from "./Button";
|
|
||||||
|
|
||||||
export function CopyButton({
|
|
||||||
value,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
copiedMessage,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...rest}
|
|
||||||
variant={variant === "icon" ? "iconCopy" : variant || "copy"}
|
|
||||||
on={isCopied}
|
|
||||||
className={className}
|
|
||||||
onPress={setCopied}
|
|
||||||
iconStyle={isCopied ? "stroke" : "fill"}
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<>
|
|
||||||
{variant !== "icon" && <span>{copiedMessage || "Copied!"}</span>}
|
|
||||||
<CheckIcon />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{variant !== "icon" && <span>{children || value}</span>}
|
|
||||||
<CopyIcon />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
65
src/button/CopyButton.tsx
Normal file
65
src/button/CopyButton.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import useClipboard from "react-use-clipboard";
|
||||||
|
|
||||||
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
|
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
|
||||||
|
import { Button, ButtonVariant } from "./Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
children?: JSX.Element | string;
|
||||||
|
className?: string;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
copiedMessage?: string;
|
||||||
|
}
|
||||||
|
export function CopyButton({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
copiedMessage,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
variant={variant === "icon" ? "iconCopy" : variant || "copy"}
|
||||||
|
on={isCopied}
|
||||||
|
className={className}
|
||||||
|
onPress={setCopied}
|
||||||
|
iconStyle={isCopied ? "stroke" : "fill"}
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<>
|
||||||
|
{variant !== "icon" && <span>{copiedMessage || t("Copied!")}</span>}
|
||||||
|
<CheckIcon />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{variant !== "icon" && <span>{children || value}</span>}
|
||||||
|
<CopyIcon />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { variantToClassName, sizeToClassName } from "./Button";
|
|
||||||
|
|
||||||
export function LinkButton({ className, variant, size, children, ...rest }) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
variantToClassName[variant || "secondary"],
|
|
||||||
sizeToClassName[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
58
src/button/LinkButton.tsx
Normal file
58
src/button/LinkButton.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { HTMLAttributes } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import * as H from "history";
|
||||||
|
|
||||||
|
import {
|
||||||
|
variantToClassName,
|
||||||
|
sizeToClassName,
|
||||||
|
ButtonVariant,
|
||||||
|
ButtonSize,
|
||||||
|
} from "./Button";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||||
|
children: JSX.Element | string;
|
||||||
|
to: H.LocationDescriptor | ((location: H.Location) => H.LocationDescriptor);
|
||||||
|
size?: ButtonSize;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinkButton({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
variantToClassName[variant || "secondary"],
|
||||||
|
sizeToClassName[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
to={to}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/button/VolumeIcon.tsx
Normal file
35
src/button/VolumeIcon.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
|
||||||
|
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
|
||||||
|
import { ReactComponent as Audio } from "../icons/Audio.svg";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Number between 0 and 1
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VolumeIcon({ volume }: Props) {
|
||||||
|
if (volume <= 0) return <AudioMuted />;
|
||||||
|
if (volume <= 0.5) return <AudioLow />;
|
||||||
|
return <Audio />;
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./Button";
|
|
||||||
export * from "./CopyButton";
|
|
||||||
export * from "./LinkButton";
|
|
||||||
19
src/button/index.ts
Normal file
19
src/button/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./Button";
|
||||||
|
export * from "./CopyButton";
|
||||||
|
export * from "./LinkButton";
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
import styles from "./Form.module.css";
|
|
||||||
|
|
||||||
export const Form = forwardRef(({ children, className, ...rest }, ref) => {
|
|
||||||
return (
|
|
||||||
<form {...rest} className={classNames(styles.form, className)} ref={ref}>
|
|
||||||
{children}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
40
src/form/Form.tsx
Normal file
40
src/form/Form.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React, { FormEventHandler, forwardRef } from "react";
|
||||||
|
|
||||||
|
import styles from "./Form.module.css";
|
||||||
|
|
||||||
|
interface FormProps {
|
||||||
|
className: string;
|
||||||
|
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||||
|
children: JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Form = forwardRef<HTMLFormElement, FormProps>(
|
||||||
|
({ children, className, onSubmit }, ref) => {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className={classNames(styles.form, className)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
.callTile {
|
.callTile {
|
||||||
height: 95px;
|
height: 95px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,38 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { Facepile } from "../Facepile";
|
import { Facepile } from "../Facepile";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
import styles from "./CallList.module.css";
|
import styles from "./CallList.module.css";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { Body, Caption } from "../typography/Typography";
|
import { Body, Caption } from "../typography/Typography";
|
||||||
|
import { GroupCallRoom } from "./useGroupCallRooms";
|
||||||
|
|
||||||
export function CallList({ rooms, client, disableFacepile }) {
|
interface CallListProps {
|
||||||
|
rooms: GroupCallRoom[];
|
||||||
|
client: MatrixClient;
|
||||||
|
disableFacepile?: boolean;
|
||||||
|
}
|
||||||
|
export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.callList}>
|
<div className={styles.callList}>
|
||||||
@@ -32,7 +57,14 @@ export function CallList({ rooms, client, disableFacepile }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
interface CallTileProps {
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
roomId: string;
|
||||||
|
participants: RoomMember[];
|
||||||
|
client: MatrixClient;
|
||||||
|
disableFacepile?: boolean;
|
||||||
|
}
|
||||||
function CallTile({
|
function CallTile({
|
||||||
name,
|
name,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
@@ -40,12 +72,12 @@ function CallTile({
|
|||||||
participants,
|
participants,
|
||||||
client,
|
client,
|
||||||
disableFacepile,
|
disableFacepile,
|
||||||
}) {
|
}: CallTileProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.callTile}>
|
<div className={styles.callTile}>
|
||||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="lg"
|
size={Size.LG}
|
||||||
bgKey={name}
|
bgKey={name}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
fallback={name.slice(0, 1).toUpperCase()}
|
fallback={name.slice(0, 1).toUpperCase()}
|
||||||
3
src/home/CallTypeDropdown.module.css
Normal file
3
src/home/CallTypeDropdown.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
74
src/home/CallTypeDropdown.tsx
Normal file
74
src/home/CallTypeDropdown.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Headline } from "../typography/Typography";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||||
|
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
|
import styles from "./CallTypeDropdown.module.css";
|
||||||
|
import commonStyles from "./common.module.css";
|
||||||
|
import menuStyles from "../Menu.module.css";
|
||||||
|
import { Menu } from "../Menu";
|
||||||
|
|
||||||
|
export enum CallType {
|
||||||
|
Video = "video",
|
||||||
|
Radio = "radio",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
callType: CallType;
|
||||||
|
setCallType: (value: CallType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverMenuTrigger placement="bottom">
|
||||||
|
<Button variant="dropdown" className={commonStyles.headline}>
|
||||||
|
<Headline className={styles.label}>
|
||||||
|
{callType === CallType.Video
|
||||||
|
? t("Video call")
|
||||||
|
: t("Walkie-talkie call")}
|
||||||
|
</Headline>
|
||||||
|
</Button>
|
||||||
|
{(props: JSX.IntrinsicAttributes) => (
|
||||||
|
<Menu {...props} label={t("Call type menu")} onAction={setCallType}>
|
||||||
|
<Item key={CallType.Video} textValue={t("Video call")}>
|
||||||
|
<VideoIcon />
|
||||||
|
<span>{t("Video call")}</span>
|
||||||
|
{callType === CallType.Video && (
|
||||||
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
|
)}
|
||||||
|
</Item>
|
||||||
|
<Item key={CallType.Radio} textValue={t("Walkie-talkie call")}>
|
||||||
|
<MicIcon />
|
||||||
|
<span>{t("Walkie-talkie call")}</span>
|
||||||
|
{callType === CallType.Radio && (
|
||||||
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
|
)}
|
||||||
|
</Item>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</PopoverMenuTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,6 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
import { UnauthenticatedView } from "./UnauthenticatedView";
|
import { UnauthenticatedView } from "./UnauthenticatedView";
|
||||||
@@ -22,7 +24,8 @@ import { RegisteredView } from "./RegisteredView";
|
|||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
usePageTitle("Home");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Home"));
|
||||||
|
|
||||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||||
useClient();
|
useClient();
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Modal, ModalContent } from "../Modal";
|
|
||||||
import { Button } from "../button";
|
|
||||||
import { FieldRow } from "../input/Input";
|
|
||||||
import styles from "./JoinExistingCallModal.module.css";
|
|
||||||
|
|
||||||
export function JoinExistingCallModal({ onJoin, ...rest }) {
|
|
||||||
return (
|
|
||||||
<Modal title="Join existing call?" isDismissable {...rest}>
|
|
||||||
<ModalContent>
|
|
||||||
<p>This call already exists, would you like to join?</p>
|
|
||||||
<FieldRow rightAlign className={styles.buttons}>
|
|
||||||
<Button onPress={rest.onClose}>No</Button>
|
|
||||||
<Button onPress={onJoin}>Yes, join call</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
46
src/home/JoinExistingCallModal.tsx
Normal file
46
src/home/JoinExistingCallModal.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { PressEvent } from "@react-types/shared";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from "../Modal";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { FieldRow } from "../input/Input";
|
||||||
|
import styles from "./JoinExistingCallModal.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onJoin: (e: PressEvent) => void;
|
||||||
|
onClose: (e: PressEvent) => void;
|
||||||
|
// TODO: add used parameters for <Modal>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}
|
||||||
|
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={t("Join existing call?")} isDismissable {...rest}>
|
||||||
|
<ModalContent>
|
||||||
|
<p>{t("This call already exists, would you like to join?")}</p>
|
||||||
|
<FieldRow rightAlign className={styles.buttons}>
|
||||||
|
<Button onPress={onClose}>{t("No")}</Button>
|
||||||
|
<Button onPress={onJoin}>{t("Yes, join call")}</Button>
|
||||||
|
</FieldRow>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fieldRow {
|
.fieldRow {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldRow:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user