Compare commits
901 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33980511c | ||
|
|
42aeecd964 | ||
|
|
45dbaa968a | ||
|
|
db66700595 | ||
|
|
f93c022c27 | ||
|
|
84a92845c3 | ||
|
|
8731f83fb5 | ||
|
|
7b71e9b20f | ||
|
|
8a245533bb | ||
|
|
43e80dd9e7 | ||
|
|
9b93d45ea0 | ||
|
|
f1c050c327 | ||
|
|
99dbcffcaf | ||
|
|
57da08e6ff | ||
|
|
e1d6f99f25 | ||
|
|
956388807f | ||
|
|
9bd6f346e9 | ||
|
|
b1083baacf | ||
|
|
5b5c649b49 | ||
|
|
2346ad9b7e | ||
|
|
feeb9c4e7c | ||
|
|
0767a6f9dd | ||
|
|
77b139226b | ||
|
|
658185a119 | ||
|
|
37d9e48c0a | ||
|
|
a1c61fc1fd | ||
|
|
68a50146b1 | ||
|
|
effe892757 | ||
|
|
f5bc29a226 | ||
|
|
4189fc7e84 | ||
|
|
45dee7906d | ||
|
|
f7506ae9b7 | ||
|
|
7bfe3f9c47 | ||
|
|
2954290a48 | ||
|
|
1dbd0e76ad | ||
|
|
afc4c4ac4e | ||
|
|
368f69f52d | ||
|
|
de0a68fbcd | ||
|
|
d72b9a8d82 | ||
|
|
f34733bfb2 | ||
|
|
6d7b7de76e | ||
|
|
9b3835c166 | ||
|
|
54d903933b | ||
|
|
dc1e086ea2 | ||
|
|
a3b8dfcdf2 | ||
|
|
e6eb2e093c | ||
|
|
d4caa1585b | ||
|
|
363ea2e669 | ||
|
|
c25874ced5 | ||
|
|
7932d7a471 | ||
|
|
e42a83bbc4 | ||
|
|
cb5f7a3f84 | ||
|
|
99b3880afc | ||
|
|
18b5ae9d4a | ||
|
|
14a1ff7fe4 | ||
|
|
0d22ef2104 | ||
|
|
5ceda993d4 | ||
|
|
7038a76fc1 | ||
|
|
1a329966ba | ||
|
|
31decc6577 | ||
|
|
878c183548 | ||
|
|
4e295f7708 | ||
|
|
b2b233ae00 | ||
|
|
86ec677675 | ||
|
|
1104aa2412 | ||
|
|
d9575a0dcf | ||
|
|
7b4650f5f4 | ||
|
|
595eb85431 | ||
|
|
4e7e707a92 | ||
|
|
6186d1d7d2 | ||
|
|
9b493da519 | ||
|
|
b81889bf15 | ||
|
|
610e320031 | ||
|
|
983f8eb737 | ||
|
|
a2040bf4c2 | ||
|
|
d8d72d023c | ||
|
|
faf4d1b49b | ||
|
|
9045ba925a | ||
|
|
ff77fa2543 | ||
|
|
66d867b5c7 | ||
|
|
66ecb7c4e9 | ||
|
|
e01136e6bb | ||
|
|
ee6438a4bd | ||
|
|
c4c99c4bcb | ||
|
|
cef88e2894 | ||
|
|
88f4b889a1 | ||
|
|
e909ff5ad0 | ||
|
|
fcaa126147 | ||
|
|
b8af9a0733 | ||
|
|
97c294687e | ||
|
|
50e9e33922 | ||
|
|
2b74c2d9ce | ||
|
|
736aa95133 | ||
|
|
b39b3c072d | ||
|
|
d2a5baf403 | ||
|
|
e1090377f9 | ||
|
|
669b1403fc | ||
|
|
a4076cd528 | ||
|
|
877726dc3c | ||
|
|
c7a2c7110a | ||
|
|
efe9e6c2b3 | ||
|
|
9bdd5b0e58 | ||
|
|
fbf7b5d022 | ||
|
|
7ad84de9c2 | ||
|
|
bf94a5dcaf | ||
|
|
537341da3a | ||
|
|
247ed95976 | ||
|
|
4e9abfa3b0 | ||
|
|
d26de7d27f | ||
|
|
3891b042e3 | ||
|
|
821622f71c | ||
|
|
71dcc94166 | ||
|
|
1ea9432769 | ||
|
|
fa4b4eabdf | ||
|
|
39c30ebf56 | ||
|
|
3ef84c069c | ||
|
|
4ee6e450b7 | ||
|
|
50c373e091 | ||
|
|
5fe92ee30b | ||
|
|
c0d338a504 | ||
|
|
7859f3813e | ||
|
|
b4d2b8159b | ||
|
|
9d0e77adf7 | ||
|
|
afa7ae69d2 | ||
|
|
79506653eb | ||
|
|
f5fea48ccd | ||
|
|
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 | ||
|
|
e76a805c8f | ||
|
|
9fc4af2bd7 | ||
|
|
0f3a7f9fd9 | ||
|
|
1cc634509b | ||
|
|
cb07ce32cb | ||
|
|
6866d662f7 | ||
|
|
51a2027d64 | ||
|
|
0f6b8f9bb1 | ||
|
|
63229ce2d7 | ||
|
|
1d620910c5 | ||
|
|
47357b3fc6 | ||
|
|
3ed35f9477 | ||
|
|
a369444b62 | ||
|
|
742d658021 | ||
|
|
681c24a0ca | ||
|
|
fc057bf988 | ||
|
|
51561e2f4e | ||
|
|
4168540017 | ||
|
|
942630c2fc | ||
|
|
9251cd9964 | ||
|
|
145826d1f3 | ||
|
|
5e42881c5c | ||
|
|
0824bfb4ed | ||
|
|
6ec9e4b666 | ||
|
|
ec447429c5 | ||
|
|
9c3e4907c8 | ||
|
|
cde352bcae | ||
|
|
2d2400edae | ||
|
|
7c80682b08 | ||
|
|
1d8cd8c3c8 | ||
|
|
a33d1364b6 | ||
|
|
a189f3ad98 | ||
|
|
f3cee359c0 | ||
|
|
be45c0319e | ||
|
|
dec47d21c0 | ||
|
|
c4f335ebb6 | ||
|
|
35c11660a3 | ||
|
|
089c891a55 | ||
|
|
3f60cd0386 | ||
|
|
ef8021e1a8 | ||
|
|
8ab68ed8c8 | ||
|
|
76b2e8b29e | ||
|
|
91366585ff | ||
|
|
21e4516bc3 | ||
|
|
f8b4331ec7 | ||
|
|
f7cb015390 | ||
|
|
f2c3c82d3a | ||
|
|
48f3f430da | ||
|
|
d6fb0e836d | ||
|
|
fddc8a1209 | ||
|
|
6032f6ba44 | ||
|
|
d1368f4622 | ||
|
|
6da369f3fe | ||
|
|
289f7285ae | ||
|
|
da69dd8320 | ||
|
|
d7d38c1ba9 | ||
|
|
abae58489c | ||
|
|
d4fec73d64 | ||
|
|
a8cb9f290a | ||
|
|
251f6a92a9 | ||
|
|
9163d5a25d | ||
|
|
3ee4058dce | ||
|
|
a8d6f21af9 | ||
|
|
78eff5fa9e | ||
|
|
6311a869f9 | ||
|
|
98355edf92 | ||
|
|
c71f37a8f8 | ||
|
|
97ab7ee2c0 | ||
|
|
d6567658c0 | ||
|
|
8df13ee7c8 | ||
|
|
36d59c98c0 | ||
|
|
f6b3d6830e | ||
|
|
a7ba511278 | ||
|
|
5819654bc7 | ||
|
|
3d571a00c6 | ||
|
|
3c30ca5f95 | ||
|
|
cb2cce243a | ||
|
|
19fe760833 | ||
|
|
5f4ac97787 | ||
|
|
e9fc90c55b | ||
|
|
096460ecfe | ||
|
|
86ccc2431e | ||
|
|
bcd58aae90 | ||
|
|
c609d42554 | ||
|
|
b3b73e9874 | ||
|
|
f6c5484d1b | ||
|
|
4efcc53628 | ||
|
|
4be14159c5 | ||
|
|
22f8fef87d | ||
|
|
d1e645fbc0 | ||
|
|
aa6bbbaaa0 | ||
|
|
627c64dca3 | ||
|
|
7d08ea2143 | ||
|
|
3cb59aebf5 | ||
|
|
2b1a523973 | ||
|
|
546ab06d60 | ||
|
|
0e407c08df | ||
|
|
ca18873a1b | ||
|
|
ebf61511f1 | ||
|
|
73eacdb23f | ||
|
|
3fac266013 | ||
|
|
6621e20da3 | ||
|
|
0adc4b3d66 | ||
|
|
8a452d80e2 | ||
|
|
71986f6001 | ||
|
|
eb4207e41d | ||
|
|
0fe38000f5 | ||
|
|
550c45b69e | ||
|
|
8be578763d | ||
|
|
3ec01293e6 | ||
|
|
3d3663c540 | ||
|
|
095b0287f0 | ||
|
|
d59f0e748d | ||
|
|
24ccfa0dd8 | ||
|
|
0e273a6dc5 | ||
|
|
f4936f221f | ||
|
|
ef8c28f274 | ||
|
|
eb620e9220 | ||
|
|
87e5cafb77 | ||
|
|
ffc5208865 | ||
|
|
fe724783ff | ||
|
|
658424efa0 | ||
|
|
ab73a351f8 | ||
|
|
d45d37b18a | ||
|
|
66e5ec976b | ||
|
|
b65874a6fc | ||
|
|
28b7e76ce0 | ||
|
|
31845dea10 | ||
|
|
b1a75a5033 | ||
|
|
032d623acb |
25
.env
25
.env
@@ -1,25 +0,0 @@
|
|||||||
####
|
|
||||||
# App Config
|
|
||||||
# Environment files are documented here:
|
|
||||||
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
|
||||||
####
|
|
||||||
|
|
||||||
# Used for determining the homeserver to use for short urls etc.
|
|
||||||
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
|
|
||||||
|
|
||||||
# The room id for the space to use for listing public group call rooms
|
|
||||||
# VITE_PUBLIC_SPACE_ROOM_ID=!hjdfshkdskjdsk:myhomeserver.com
|
|
||||||
|
|
||||||
# The Sentry DSN to use for error reporting. Leave undefined to disable.
|
|
||||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
|
||||||
|
|
||||||
# VITE_CUSTOM_THEME=true
|
|
||||||
# VITE_PRIMARY_COLOR=#0dbd8b
|
|
||||||
# VITE_BG_COLOR_1=#ffffff
|
|
||||||
# VITE_BG_COLOR_2=#f0f1f4
|
|
||||||
# VITE_BG_COLOR_3=#dbdfe4
|
|
||||||
# VITE_BG_COLOR_4=#d1d3d7
|
|
||||||
# VITE_INPUT_BORDER_COLOR=#e7e7e7
|
|
||||||
# VITE_INPUT_BORDER_COLOR_FOCUSED=#238cf5
|
|
||||||
# VITE_TEXT_COLOR_1=#17191c
|
|
||||||
# VITE_TEXT_COLOR_2=#61708b
|
|
||||||
31
.env.example
Normal file
31
.env.example
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
####
|
||||||
|
# App Config
|
||||||
|
# Environment files are documented here:
|
||||||
|
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||||
|
####
|
||||||
|
|
||||||
|
# Used for determining the homeserver to use for short urls etc.
|
||||||
|
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
|
||||||
|
# VITE_FALLBACK_STUN_ALLOWED=false
|
||||||
|
|
||||||
|
# Used for submitting debug logs to an external rageshake server
|
||||||
|
# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit
|
||||||
|
|
||||||
|
# The Sentry DSN to use for error reporting. Leave undefined to disable.
|
||||||
|
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||||
|
|
||||||
|
# VITE_CUSTOM_THEME=true
|
||||||
|
# VITE_THEME_ACCENT=#0dbd8b
|
||||||
|
# VITE_THEME_ACCENT_20=#0dbd8b33
|
||||||
|
# VITE_THEME_ALERT=#ff5b55
|
||||||
|
# VITE_THEME_ALERT_20=#ff5b5533
|
||||||
|
# VITE_THEME_LINKS=#0086e6
|
||||||
|
# VITE_THEME_PRIMARY_CONTENT=#ffffff
|
||||||
|
# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
|
||||||
|
# VITE_THEME_TERTIARY_CONTENT=#8e99a4
|
||||||
|
# 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
|
||||||
42
.eslintrc.cjs
Normal file
42
.eslintrc.cjs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
// We're aiming to convert this code to strict mode
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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
|
||||||
24
.github/workflows/lint.yaml
vendored
Normal file
24
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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: i18n
|
||||||
|
run: "yarn run i18n: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 }}
|
||||||
|
|||||||
18
.github/workflows/test.yaml
vendored
Normal file
18
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
name: Run jest tests
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
jobs:
|
||||||
|
jest:
|
||||||
|
name: Run jest tests
|
||||||
|
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: Jest
|
||||||
|
run: "yarn run test"
|
||||||
27
.github/workflows/triage-incoming.yaml
vendored
Normal file
27
.github/workflows/triage-incoming.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Move new issues into triage board
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [ opened ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-to-project:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: octokit/graphql-action@v2.x
|
||||||
|
id: add_to_project
|
||||||
|
with:
|
||||||
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
|
query: |
|
||||||
|
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||||
|
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||||
|
item {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projectid: ${{ env.PROJECT_ID }}
|
||||||
|
contentid: ${{ github.event.issue.node_id }}
|
||||||
|
env:
|
||||||
|
PROJECT_ID: "PVT_kwDOAM0swc4AH1sa"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
4
.gitignore
vendored
4
.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 @@
|
|||||||
|
{}
|
||||||
25
.storybook/main.js
Normal file
25
.storybook/main.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const svgrPlugin = require("vite-plugin-svgr");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
|
framework: "@storybook/react",
|
||||||
|
core: {
|
||||||
|
builder: "storybook-builder-vite",
|
||||||
|
},
|
||||||
|
async viteFinal(config) {
|
||||||
|
config.plugins = config.plugins.filter(
|
||||||
|
(item) =>
|
||||||
|
!(
|
||||||
|
Array.isArray(item) &&
|
||||||
|
item.length > 0 &&
|
||||||
|
item[0].name === "vite-plugin-mdx"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
config.plugins.push(svgrPlugin());
|
||||||
|
config.resolve = config.resolve || {};
|
||||||
|
config.resolve.dedupe = config.resolve.dedupe || [];
|
||||||
|
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
25
.storybook/preview.jsx
Normal file
25
.storybook/preview.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { addDecorator } from "@storybook/react";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { usePageFocusStyle } from "../src/usePageFocusStyle";
|
||||||
|
import { OverlayProvider } from "@react-aria/overlays";
|
||||||
|
import "../src/index.css";
|
||||||
|
|
||||||
|
export const parameters = {
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
addDecorator((story) => {
|
||||||
|
usePageFocusStyle();
|
||||||
|
return (
|
||||||
|
<MemoryRouter initialEntries={["/"]}>
|
||||||
|
<OverlayProvider>{story()}</OverlayProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
});
|
||||||
21
.vscode/settings.json
vendored
21
.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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
CONTRIBUTING.md
Normal file
4
CONTRIBUTING.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Contributing code to Element
|
||||||
|
============================
|
||||||
|
|
||||||
|
Element follows the same pattern as the [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md).
|
||||||
18
Dockerfile
18
Dockerfile
@@ -1,14 +1,18 @@
|
|||||||
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 nginx:alpine
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
COPY --from=builder /src/matrix-video-chat/dist /app
|
COPY --from=builder /src/element-call/dist /app
|
||||||
|
COPY config/default.conf /etc/nginx/conf.d/
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html \
|
USER root
|
||||||
&& ln -s /app /usr/share/nginx/html
|
|
||||||
|
RUN rm -rf /usr/share/nginx/html
|
||||||
|
|
||||||
|
USER 101
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -1,45 +1,79 @@
|
|||||||
# Matrix Video Chat
|
# Element Call
|
||||||
|
|
||||||
Testbed for full mesh video chat.
|
[](https://matrix.to/#/#webrtc:matrix.org)
|
||||||
|
[](https://translate.element.io/engage/element-call/)
|
||||||
|
|
||||||
## Getting Started
|
Full mesh group calls powered by [Matrix](https://matrix.org), implementing [MatrixRTC](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md).
|
||||||
|
|
||||||
`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.
|
To try it out, visit our hosted version at [call.element.io](https://call.element.io). You can also find the latest development version continuously deployed to [element-call.netlify.app](https://element-call.netlify.app).
|
||||||
|
|
||||||
First clone, install, and link `matrix-js-sdk`
|
## Host it yourself
|
||||||
|
|
||||||
|
Until prebuilt tarballs are available, you'll need to build Element Call from source. First, clone and install the package:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/vector-im/element-call.git
|
||||||
|
cd element-call
|
||||||
|
yarn
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now edit the configuration in `.env` to your liking. The most important thing is to set `VITE_DEFAULT_HOMESERVER` to the homeserver that the app should use, such as `https://call.ems.host`.
|
||||||
|
|
||||||
|
Next, build the project:
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
If all went well, you can now find the build output under `dist` as a series of static files. These can be hosted using any web server of your choice.
|
||||||
|
|
||||||
|
Because Element Call uses client-side routing, your server must be able to route any requests to non-existing paths back to `/index.html`. For example, in Nginx you can achieve this with the `try_files` directive:
|
||||||
|
|
||||||
|
```
|
||||||
|
server {
|
||||||
|
...
|
||||||
|
location / {
|
||||||
|
...
|
||||||
|
try_files $uri /$uri /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Element Call is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/2553). To get started, clone, install, and link the package:
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
git checkout robertlong/group-call
|
git switch robertlong/group-call
|
||||||
yarn
|
yarn
|
||||||
yarn link
|
yarn link
|
||||||
```
|
```
|
||||||
|
|
||||||
Then clone, install, link `matrix-js-sdk` into `matrix-react-sdk`, and link `matrix-react-sdk`
|
Next, we can set up this project:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
git clone https://github.com/vector-im/element-call.git
|
||||||
cd matrix-react-sdk
|
cd element-call
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn
|
yarn
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
By default, the app expects you to have [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008. If you wish to use another homeserver, you can set it in your `.env` file.
|
||||||
|
|
||||||
Finally we can set up this project.
|
You're now ready to launch the development server:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/vector-im/matrix-video-chat.git
|
|
||||||
cd matrix-video-chat
|
|
||||||
yarn
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
Configuration options are documented in the `.env` file.
|
Configuration options are documented in the `.env` file.
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
|
||||||
|
If you'd like to help translate Element Call, head over to [translate.element.io](https://translate.element.io/engage/element-call/). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts.
|
||||||
|
|||||||
15
babel.config.cjs
Normal file
15
babel.config.cjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
targets: {
|
||||||
|
node: "current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@babel/preset-react",
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
],
|
||||||
|
plugins: ["babel-plugin-transform-vite-meta-env"],
|
||||||
|
};
|
||||||
10
config/default.conf
Normal file
10
config/default.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /app;
|
||||||
|
try_files $uri /$uri /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
};
|
||||||
16
index.html
16
index.html
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
||||||
<title>Matrix Video Chat</title>
|
|
||||||
<script>
|
|
||||||
window.global = window;
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
83
package.json
83
package.json
@@ -1,11 +1,24 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
|
"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",
|
||||||
|
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@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",
|
||||||
@@ -15,6 +28,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",
|
||||||
@@ -22,23 +36,78 @@
|
|||||||
"@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",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"i18next-http-backend": "^1.4.4",
|
||||||
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#7ec726e10be835588d4b188fcd3d137b4690d79a",
|
||||||
|
"matrix-widget-api": "^1.0.0",
|
||||||
|
"mermaid": "^8.13.8",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
|
"pako": "^2.0.4",
|
||||||
|
"postcss-preset-env": "^7",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.0",
|
"react": "18",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "18",
|
||||||
|
"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",
|
||||||
|
"sdp-transform": "^2.14.1",
|
||||||
|
"unique-names-generator": "^4.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@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",
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@types/request": "^2.48.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
||||||
|
"@typescript-eslint/parser": "^5.22.0",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
|
"babel-plugin-transform-vite-meta-env": "^1.0.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",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^29.2.2",
|
||||||
|
"jest-environment-jsdom": "^29.2.2",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
|
"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-svgr": "^0.4.0"
|
"vite-plugin-svgr": "^0.4.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||||
|
],
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"/node_modules/(?!d3)+$",
|
||||||
|
"/node_modules/(?!internmap)+$"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"\\.(css|less|svg)+$": "identity-obj-proxy",
|
||||||
|
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
|
||||||
|
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
20
public/index.html
Normal file
20
public/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||||
|
<title>
|
||||||
|
<%- title %>
|
||||||
|
</title>
|
||||||
|
<script>
|
||||||
|
window.global = window;
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
131
public/locales/bg/app.json
Normal file
131
public/locales/bg/app.json
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"<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": "Копирай и сподели връзка към разговора",
|
||||||
|
"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}} - уоки-токи разговор"
|
||||||
|
}
|
||||||
83
public/locales/cs/app.json
Normal file
83
public/locales/cs/app.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"Copy and share this call link": "Zkopírujte a sdílejte odkaz na hovor",
|
||||||
|
"Copied!": "Zkopírováno!",
|
||||||
|
"Connection lost": "Připojení ztraceno",
|
||||||
|
"Confirm password": "Potvrdit heslo",
|
||||||
|
"Close": "Zavřít",
|
||||||
|
"Change layout": "Změnit rozložení",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Oprávnění k přístupu ke kameře/mikrofonu jsou nutná pro připojení k hovoru.",
|
||||||
|
"Camera {{n}}": "Kamera {{n}}",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Call link copied": "Odkaz na hovor zkopírován",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Accept microphone permissions to join the call.": "Povolte přístup k mikrofonu pro připojení k hovoru.",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Povolte přístup ke kameře/mikrofonu pro připojení do hovoru.",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Vytvořit účet</0> Or <2>Jako host</2>",
|
||||||
|
"Your recent calls": "Vaše nedávné hovory",
|
||||||
|
"You can't talk at the same time": "Teď nemůžete mluvit",
|
||||||
|
"Yes, join call": "Ano, připojit se",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC není podporováno nebo je zakázáno tímto prohlížečem.",
|
||||||
|
"Waiting for other participants…": "Čekání na další účastníky…",
|
||||||
|
"Waiting for network": "Čekání na připojení",
|
||||||
|
"Video call name": "Jméno videohovoru",
|
||||||
|
"Video call": "Videohovor",
|
||||||
|
"Video": "Video",
|
||||||
|
"Version: {{version}}": "Verze: {{version}}",
|
||||||
|
"Username": "Uživatelské jméno",
|
||||||
|
"User menu": "Uživatelské menu",
|
||||||
|
"User ID": "ID uživatele",
|
||||||
|
"Unmute microphone": "Zapnout mikrofon",
|
||||||
|
"Turn on camera": "Zapnout kameru",
|
||||||
|
"Turn off camera": "Vypnout kameru",
|
||||||
|
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
|
||||||
|
"Thanks! We'll get right on it.": "Děkujeme! Hned se na to vrhneme.",
|
||||||
|
"Take me Home": "Domovská obrazovka",
|
||||||
|
"Submitting feedback…": "Odesílání zpětné vazby…",
|
||||||
|
"Submit feedback": "Dát feedback",
|
||||||
|
"Stop sharing screen": "Zastavit sdílení obrazovek",
|
||||||
|
"Speaker {{n}}": "Reproduktor {{n}}",
|
||||||
|
"Speaker": "Reproduktor",
|
||||||
|
"Spatial audio": "Prostorový zvuk",
|
||||||
|
"Sign out": "Odhlásit se",
|
||||||
|
"Sign in": "Přihlásit se",
|
||||||
|
"Show call inspector": "Zobrazit inspektor hovoru",
|
||||||
|
"Share screen": "Sdílet obrazovku",
|
||||||
|
"Settings": "Nastavení",
|
||||||
|
"Sending…": "Posílání…",
|
||||||
|
"Sending debug logs…": "Posílání ladícího záznamu…",
|
||||||
|
"Send debug logs": "Poslat ladící záznam",
|
||||||
|
"Select an option": "Vyberte možnost",
|
||||||
|
"Saving…": "Ukládání…",
|
||||||
|
"Save": "Uložit",
|
||||||
|
"Return to home screen": "Vrátit se na domácí obrazovku",
|
||||||
|
"Remove": "Odstranit",
|
||||||
|
"Registering…": "Registrování…",
|
||||||
|
"Register": "Registrace",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Press and hold to talk": "Stiskněte a držte pro mluvení",
|
||||||
|
"Press and hold spacebar to talk": "Stiskněte a držte mezerník pro mluvení",
|
||||||
|
"Passwords must match": "Hesla se musí shodovat",
|
||||||
|
"Password": "Heslo",
|
||||||
|
"Not now, return to home screen": "Teď ne, vrátit se na domovskou obrazovku",
|
||||||
|
"No": "Ne",
|
||||||
|
"Mute microphone": "Ztlumit mikrofon",
|
||||||
|
"More": "Více",
|
||||||
|
"Microphone permissions needed to join the call.": "Přístup k mikrofonu je nutný pro připojení se k hovoru.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Login to your account": "Přihlásit se ke svůmu účtu",
|
||||||
|
"Login": "Přihlášení",
|
||||||
|
"Logging in…": "Přihlašování se…",
|
||||||
|
"Local volume": "Lokální hlasitost",
|
||||||
|
"Loading…": "Načítání…",
|
||||||
|
"Loading room…": "Načítání místnosti…",
|
||||||
|
"Leave": "Opustit hovor",
|
||||||
|
"Join call now": "Připojit se k hovoru",
|
||||||
|
"Join call": "Připojit se k hovoru",
|
||||||
|
"Invite people": "Pozvat lidi",
|
||||||
|
"Invite": "Pozvat",
|
||||||
|
"Inspector": "Insepktor",
|
||||||
|
"Incompatible versions!": "Nekompatibilní verze!",
|
||||||
|
"Incompatible versions": "Nekompatibilní verze"
|
||||||
|
}
|
||||||
134
public/locales/de/app.json
Normal file
134
public/locales/de/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"<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 ein Profilbild 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 es 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>": "Wenn du auf „Los geht’s“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
|
||||||
|
"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",
|
||||||
|
"Create account": "Konto erstellen",
|
||||||
|
"Debug log": "Debug-Protokoll",
|
||||||
|
"Debug log request": "Debug-Log Anfrage",
|
||||||
|
"Description (optional)": "Beschreibung (optional)",
|
||||||
|
"Details": "Details",
|
||||||
|
"Developer": "Entwickler",
|
||||||
|
"Display name": "Anzeigename",
|
||||||
|
"Download debug logs": "Debug-Protokolle 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.": "Du hast ein Problem? Hilf uns, es zu beheben.",
|
||||||
|
"Home": "Startseite",
|
||||||
|
"Include debug logs": "Debug-Protokolle einschließen",
|
||||||
|
"Incompatible versions": "Inkompatible Versionen",
|
||||||
|
"Incompatible versions!": "Inkompatible Versionen!",
|
||||||
|
"Inspector": "Inspektor",
|
||||||
|
"Invite": "Einladen",
|
||||||
|
"Invite people": "Personen einladen",
|
||||||
|
"Join call": "Anruf beitreten",
|
||||||
|
"Join call now": "Anruf beitreten",
|
||||||
|
"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": "Melde dich mit deinem Konto an",
|
||||||
|
"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": "Halte zum Sprechen die Leertaste gedrückt",
|
||||||
|
"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": "Rückmeldung geben",
|
||||||
|
"Submitting feedback…": "Sende Rückmeldung …",
|
||||||
|
"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 wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> und <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.)": "Dies wird die Audiowiedergabe eines Sprechers wirken lassen, als käme sie von der Stelle des zugehörigen Videobildes. (Experimentelle Funktion: Dies könnte 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 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 letzten Anrufe",
|
||||||
|
"{{count}} people connected|one": "{{count}} Person verbunden",
|
||||||
|
"{{count}} people connected|other": "{{count}} Personen 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": "Name des Walkie-Talkie-Anrufs",
|
||||||
|
"Sending debug logs…": "Sende Debug-Protokolle …",
|
||||||
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Anruf beitreten</0><1>Oder</1><2>Anruflink kopieren und später beitreten</2>",
|
||||||
|
"{{name}} (Connecting...)": "{{name}} (verbindet sich …)"
|
||||||
|
}
|
||||||
134
public/locales/en-GB/app.json
Normal file
134
public/locales/en-GB/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"{{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}} (Connecting...)": "{{name}} (Connecting...)",
|
||||||
|
"{{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>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</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",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
134
public/locales/es/app.json
Normal file
134
public/locales/es/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"<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>¿Por qué no mantienes tu cuenta estableciendo una contraseña?</0><1>Podrás mantener tu nombre y establecer un avatar para usarlo en futuras llamadas</1>",
|
||||||
|
"Press and hold to talk over {{name}}": "Mantén pulsado para hablar por encima de {{name}}",
|
||||||
|
"Your recent calls": "Tus llamadas recientes",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "Tu navegador no soporta o está bloqueando WebRTC.",
|
||||||
|
"This call already exists, would you like to join?": "Esta llamada ya existe, ¿te gustaría unirte?",
|
||||||
|
"Register": "Registrarse",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "¿No estás registrado todavía? <2>Crear una cuenta</2>",
|
||||||
|
"Login to your account": "Iniciar sesión en tu cuenta",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Se necesitan los permisos de cámara/micrófono para unirse a la llamada.",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptarás nuestros <2>Términos y condiciones</2>",
|
||||||
|
"Accept microphone permissions to join the call.": "Acepta el permiso del micrófono para unirte a la llamada.",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones</2>",
|
||||||
|
"You can't talk at the same time": "No podéis hablar a la vez",
|
||||||
|
"Yes, join call": "Si, unirse a la llamada",
|
||||||
|
"Walkie-talkie call name": "Nombre de la llamada Walkie-talkie",
|
||||||
|
"Walkie-talkie call": "Llamada Walkie-talkie",
|
||||||
|
"Waiting for other participants…": "Esperando a los otros participantes…",
|
||||||
|
"Waiting for network": "Esperando a la red",
|
||||||
|
"Video call name": "Nombre de la videollamada",
|
||||||
|
"Video call": "Videollamada",
|
||||||
|
"Video": "Video",
|
||||||
|
"Version: {{version}}": "Versión: {{version}}",
|
||||||
|
"Username": "Nombre de usuario",
|
||||||
|
"User menu": "Menú de usuario",
|
||||||
|
"User ID": "ID de usuario",
|
||||||
|
"Unmute microphone": "Desilenciar el micrófono",
|
||||||
|
"Turn on camera": "Encender la cámara",
|
||||||
|
"Turn off camera": "Apagar la cámara",
|
||||||
|
"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.)": "Esto hará que el audio de la persona que hable parezca que viene de dondé esté posicionado en la pantalla. (Función experimental: esto puede afectar a la estabilidad del 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>": "Este sitio está protegido por ReCAPTCHA y se aplica <2>la Política de Privacidad</2> y <6>los Términos de Servicio</6> de Google.<9></9>Al hacer clic en \"Registrar\" aceptarás nuestros <12>Términos y condiciones</12>",
|
||||||
|
"Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.",
|
||||||
|
"Talking…": "Hablando…",
|
||||||
|
"Talk over speaker": "Hablar por encima",
|
||||||
|
"Take me Home": "Volver al inicio",
|
||||||
|
"Submitting feedback…": "Enviando comentarios…",
|
||||||
|
"Submit feedback": "Enviar comentarios",
|
||||||
|
"Stop sharing screen": "Dejar de compartir pantalla",
|
||||||
|
"Spotlight": "Foco",
|
||||||
|
"Speaker {{n}}": "Altavoz {{n}}",
|
||||||
|
"Speaker": "Altavoz",
|
||||||
|
"Spatial audio": "Audio espacial",
|
||||||
|
"Sign out": "Cerrar sesión",
|
||||||
|
"Sign in": "Iniciar sesión",
|
||||||
|
"Show call inspector": "Mostrar inspector de llamada",
|
||||||
|
"Share screen": "Compartir pantalla",
|
||||||
|
"Settings": "Ajustes",
|
||||||
|
"Sending…": "Enviando…",
|
||||||
|
"Sending debug logs…": "Enviando registros de depuración…",
|
||||||
|
"Send debug logs": "Enviar registros de depuración",
|
||||||
|
"Select an option": "Selecciona una opción",
|
||||||
|
"Saving…": "Guardando…",
|
||||||
|
"Save": "Guardar",
|
||||||
|
"Return to home screen": "Volver a la pantalla de inicio",
|
||||||
|
"Remove": "Eliminar",
|
||||||
|
"Release to stop": "Suelta para parar",
|
||||||
|
"Release spacebar key to stop": "Suelta la barra espaciadora para parar",
|
||||||
|
"Registering…": "Registrando…",
|
||||||
|
"Recaptcha not loaded": "No se ha cargado el Recaptcha",
|
||||||
|
"Recaptcha dismissed": "Recaptcha cancelado",
|
||||||
|
"Profile": "Perfil",
|
||||||
|
"Press and hold to talk": "Mantén pulsado para hablar",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Mantén pulsada la barra espaciadora para hablar por encima de {{name}}",
|
||||||
|
"Press and hold spacebar to talk": "Mantén pulsada la barra espaciadora para hablar",
|
||||||
|
"Passwords must match": "Las contraseñas deben coincidir",
|
||||||
|
"Password": "Contraseña",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Otros usuarios están intentando unirse a la llamada con versiones incompatibles. Estos usuarios deberán asegurarse de que han refrescado sus navegadores:<1>{userLis}</1>",
|
||||||
|
"Not now, return to home screen": "Ahora no, volver a la pantalla de inicio",
|
||||||
|
"No": "No",
|
||||||
|
"Mute microphone": "Silenciar micrófono",
|
||||||
|
"More menu": "Menú Más",
|
||||||
|
"More": "Más",
|
||||||
|
"Microphone permissions needed to join the call.": "Se necesitan permisos del micrófono para unirse a la llamada.",
|
||||||
|
"Microphone {{n}}": "Micrófono {{n}}",
|
||||||
|
"Microphone": "Micrófono",
|
||||||
|
"Login": "Iniciar sesión",
|
||||||
|
"Logging in…": "Iniciando sesión…",
|
||||||
|
"Local volume": "Volumen local",
|
||||||
|
"Loading…": "Cargando…",
|
||||||
|
"Loading room…": "Cargando sala…",
|
||||||
|
"Leave": "Abandonar",
|
||||||
|
"Join existing call?": "¿Unirse a llamada existente?",
|
||||||
|
"Join call now": "Unirse a la llamada ahora",
|
||||||
|
"Join call": "Unirse a la llamada",
|
||||||
|
"Invite people": "Invitar a gente",
|
||||||
|
"Invite": "Invitar",
|
||||||
|
"Inspector": "Inspector",
|
||||||
|
"Incompatible versions!": "¡Versiones incompatibles!",
|
||||||
|
"Incompatible versions": "Versiones incompatibles",
|
||||||
|
"Include debug logs": "Incluir registros de depuración",
|
||||||
|
"Home": "Inicio",
|
||||||
|
"Having trouble? Help us fix it.": "¿Tienes problemas? Ayúdanos a resolverlos.",
|
||||||
|
"Grid layout menu": "Menú de distribución de cuadrícula",
|
||||||
|
"Go": "Empezar",
|
||||||
|
"Full screen": "Pantalla completa",
|
||||||
|
"Freedom": "Libre",
|
||||||
|
"Fetching group call timed out.": "Se ha agotado el tiempo de espera para obtener la llamada grupal.",
|
||||||
|
"Exit full screen": "Salir de pantalla completa",
|
||||||
|
"Entering room…": "Entrando a la sala…",
|
||||||
|
"Download debug logs": "Descargar registros de depuración",
|
||||||
|
"Display name": "Nombre a mostrar",
|
||||||
|
"Developer": "Desarrollador",
|
||||||
|
"Details": "Detalles",
|
||||||
|
"Description (optional)": "Descripción (opcional)",
|
||||||
|
"Debug log request": "Petición de registros de depuración",
|
||||||
|
"Debug log": "Registro de depuración",
|
||||||
|
"Create account": "Crear cuenta",
|
||||||
|
"Copy and share this call link": "Copiar y compartir el enlace de la llamada",
|
||||||
|
"Copied!": "¡Copiado!",
|
||||||
|
"Connection lost": "Conexión perdida",
|
||||||
|
"Confirm password": "Confirmar contraseña",
|
||||||
|
"Close": "Cerrar",
|
||||||
|
"Change layout": "Cambiar distribución",
|
||||||
|
"Camera {{n}}": "Cámara {{n}}",
|
||||||
|
"Camera": "Cámara",
|
||||||
|
"Call type menu": "Menú de tipo de llamada",
|
||||||
|
"Call link copied": "Enlace de la llamada copiado",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Otro usuario en esta llamada está teniendo problemas. Para diagnosticar estos problemas nos gustaría recopilar un registro de depuración.",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Acepta los permisos de cámara/micrófono para unirte a la llamada.",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, algo ha salido mal.</0><1>Enviar los registros de depuración nos ayudará a localizar el problema.</1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Crear una cuenta</0> o <2>Acceder como invitado</2>",
|
||||||
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Unirse ahora</0><1>Or</1><2>Copiar el enlace y unirse más tarde</2>",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>¿Ya tienes una cuenta?</0><1><0>Iniciar sesión</0> o <2>Acceder como invitado</2></1>",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Llamada de Walkie-talkie",
|
||||||
|
"{{name}} is talking…": "{{name}} está hablando…",
|
||||||
|
"{{name}} is presenting": "{{name}} está presentando",
|
||||||
|
"{{name}} (Connecting...)": "{{name}} (Conectando...)",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, tu llamada ha finalizado",
|
||||||
|
"{{count}} people connected|other": "{{count}} personas conectadas",
|
||||||
|
"{{count}} people connected|one": "{{count}} persona conectada"
|
||||||
|
}
|
||||||
134
public/locales/et/app.json
Normal file
134
public/locales/et/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Kõnega anna õigused kaamera/mikrofoni kasutamiseks.",
|
||||||
|
"Accept microphone permissions to join the call.": "Kõnega liitumiseks anna õigused mikrofoni kasutamiseks.",
|
||||||
|
"<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>Kas hoopis tahad salasõna seadistada ja sellega oma kasutajakonto alles jätta?</0><1>Siis saad säilitada oma nime ja määrata tunnuspildi, mida saad kasutada tulevastes kõnedes</1>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, midagi läks valesti.</0><1>Logide saatmine meile aitab meil probleemi lahendada.</1>",
|
||||||
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Liitu kõnega kohe</0><1> Või</1><2>Kopeeri kõne link ja liitu hiljem</2>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Loo konto</0> Või <2>Sisene külalisena</2>",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>On sul juba konto?</0><1><0>Logi sisse</0> Või <2>Logi sisse külalisena</2></1>",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - walkie-talkie-kõne",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is talking…": "{{nimi}} räägib…",
|
||||||
|
"{{name}} is presenting": "{{nimi}} esitab",
|
||||||
|
"{{name}} (Connecting...)": "{{nimi}} (ühendamisel...)",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, sinu kõne on nüüd lõppenud",
|
||||||
|
"{{count}} people connected|other": "{{count}} osalejat liitunud",
|
||||||
|
"{{count}} people connected|one": "{{count}} osaleja liitunud",
|
||||||
|
"Invite people": "Kutsu inimesi",
|
||||||
|
"Invite": "Kutsu",
|
||||||
|
"Inspector": "Inspektor",
|
||||||
|
"Incompatible versions!": "Ühildumatud versioonid!",
|
||||||
|
"Incompatible versions": "Ühildumatud versioonid",
|
||||||
|
"Include debug logs": "Lisa veatuvastuslogid",
|
||||||
|
"Home": "Avavaatesse",
|
||||||
|
"Having trouble? Help us fix it.": "Kas on probleeme? Aita meil asja parandada.",
|
||||||
|
"Grid layout menu": "Ruudustikvaate menüü",
|
||||||
|
"Go": "Jätka",
|
||||||
|
"Full screen": "Täisekraan",
|
||||||
|
"Freedom": "Vaba",
|
||||||
|
"Fetching group call timed out.": "Grupikõne kättesaamine aegus.",
|
||||||
|
"Exit full screen": "Välju täisekraanivaatest",
|
||||||
|
"Entering room…": "Ruumi sisenemine…",
|
||||||
|
"Download debug logs": "Lae alla veatuvastuslogid",
|
||||||
|
"Display name": "Kuvatav nimi",
|
||||||
|
"Developer": "Arendaja",
|
||||||
|
"Details": "Täpsemalt",
|
||||||
|
"Description (optional)": "Kirjeldus (valikuline)",
|
||||||
|
"Debug log request": "Veaotsingulogi päring",
|
||||||
|
"Debug log": "Veaotsingulogi",
|
||||||
|
"Create account": "Loo konto",
|
||||||
|
"Copy and share this call link": "Kopeeri ja jaga selle kõne linki",
|
||||||
|
"Copied!": "Kopeeritud!",
|
||||||
|
"Connection lost": "Ühendus on katkenud",
|
||||||
|
"Confirm password": "Kinnita salasõna",
|
||||||
|
"Close": "Sulge",
|
||||||
|
"Change layout": "Muuda paigutust",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Kõnega liitumiseks vajalikud kaamera/mikrofoni kasutamise load.",
|
||||||
|
"Camera {{n}}": "Kaamera {{n}}",
|
||||||
|
"Camera": "Kaamera",
|
||||||
|
"Call type menu": "Kõnetüübi valik",
|
||||||
|
"Call link copied": "Kõne link on kopeeritud",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Liitu kõnega“nõustud sa meie <2>kasutustingimustega</2>",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega</2>",
|
||||||
|
"Avatar": "Tunnuspilt",
|
||||||
|
"Audio": "Heli",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ühel teisel selles kõnes osalejal on lahenduse kasutamisel tekkinud probleem ning selle põhjuse leidmiseks me sooviksime koguda silumislogisid.",
|
||||||
|
"Press and hold spacebar to talk": "Rääkimiseks vajuta ja hoia all tühikuklahvi",
|
||||||
|
"Passwords must match": "Salasõnad ei klapi",
|
||||||
|
"Password": "Salasõna",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Pole veel registreerunud? <2>Loo kasutajakonto</2>",
|
||||||
|
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
|
||||||
|
"No": "Ei",
|
||||||
|
"Mute microphone": "Summuta mikrofon",
|
||||||
|
"Your recent calls": "Hiljutised kõned",
|
||||||
|
"You can't talk at the same time": "Üheaegselt ei saa rääkida",
|
||||||
|
"More menu": "Rohkem valikuid",
|
||||||
|
"More": "Rohkem",
|
||||||
|
"Microphone permissions needed to join the call.": "Kõnega liitumiseks on vaja lubada mikrofoni kasutamine.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Login to your account": "Logi oma kontosse sisse",
|
||||||
|
"Login": "Sisselogimine",
|
||||||
|
"Logging in…": "Sisselogimine …",
|
||||||
|
"Local volume": "Kohalik helitugevus",
|
||||||
|
"Loading…": "Laadimine …",
|
||||||
|
"Loading room…": "Ruumi laadimine …",
|
||||||
|
"Leave": "Lahku",
|
||||||
|
"Join existing call?": "Liitu juba käimasoleva kõnega?",
|
||||||
|
"Join call now": "Kõnega liitumine",
|
||||||
|
"Join call": "Kõnega liitumine",
|
||||||
|
"User ID": "Kasutajatunnus",
|
||||||
|
"Turn on camera": "Lülita kaamera sisse",
|
||||||
|
"Turn off camera": "Lülita kaamera välja",
|
||||||
|
"Submitting feedback…": "Tagasiside saatmine…",
|
||||||
|
"Take me Home": "Mine avalehele",
|
||||||
|
"Submit feedback": "Jaga tagasisidet",
|
||||||
|
"Stop sharing screen": "Lõpeta ekraani jagamine",
|
||||||
|
"Spotlight": "Rambivalgus",
|
||||||
|
"Speaker {{n}}": "Kõlar {{n}}",
|
||||||
|
"Speaker": "Kõlar",
|
||||||
|
"Spatial audio": "Ruumiline heli",
|
||||||
|
"Sign out": "Logi välja",
|
||||||
|
"Sign in": "Logi sisse",
|
||||||
|
"Show call inspector": "Näita kõneteavet",
|
||||||
|
"Share screen": "Jaga ekraani",
|
||||||
|
"Settings": "Seadistused",
|
||||||
|
"Sending…": "Saatmine…",
|
||||||
|
"Sending debug logs…": "Veaotsingulogide saatmine…",
|
||||||
|
"Send debug logs": "Saada veaotsingulogid",
|
||||||
|
"Select an option": "Vali oma eelistus",
|
||||||
|
"Saving…": "Salvestamine…",
|
||||||
|
"Save": "Salvesta",
|
||||||
|
"Return to home screen": "Tagasi avalehele",
|
||||||
|
"Remove": "Eemalda",
|
||||||
|
"Release to stop": "Peatamiseks vabasta klahv",
|
||||||
|
"Release spacebar key to stop": "Peatamiseks vabasta tühikuklahv",
|
||||||
|
"Registering…": "Registreerimine…",
|
||||||
|
"Register": "Registreeru",
|
||||||
|
"Recaptcha not loaded": "Robotilõks pole laetud",
|
||||||
|
"Recaptcha dismissed": "Robotilõks on vahele jäetud",
|
||||||
|
"Profile": "Profiil",
|
||||||
|
"Press and hold to talk over {{name}}": "{{name}} ülerääkimiseks vajuta ja hoia all",
|
||||||
|
"Press and hold to talk": "Rääkimiseks vajuta ja hoia all",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "{{name}} ülerääkimiseks vajuta ja hoia all tühikuklahvi",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Teised kasutajad üritavad selle kõnega liituda ühildumatuid versioone kasutades. Need kasutajad peaksid oma brauseris lehe uuestilaadimise tegema:<1>{userLis}</1>",
|
||||||
|
"Waiting for other participants…": "Ootame teiste osalejate lisandumist…",
|
||||||
|
"Waiting for network": "Ootame võrguühendust",
|
||||||
|
"Video call name": "Videokõne nimi",
|
||||||
|
"Video call": "Videokõne",
|
||||||
|
"Video": "Video",
|
||||||
|
"Version: {{version}}": "Versioon: {{version}}",
|
||||||
|
"Username": "Kasutajanimi",
|
||||||
|
"This call already exists, would you like to join?": "See kõne on juba olemas, kas soovid liituda?",
|
||||||
|
"Talking…": "Jutt käib…",
|
||||||
|
"Talk over speaker": "Räägi teisest kõnelejast üle",
|
||||||
|
"Thanks! We'll get right on it.": "Tänud! Tegeleme sellega esimesel võimalusel.",
|
||||||
|
"Unmute microphone": "Aktiveeri mikrofon",
|
||||||
|
"User menu": "Kasutajamenüü",
|
||||||
|
"Yes, join call": "Jah, liitu kõnega",
|
||||||
|
"Walkie-talkie call": "Walkie-talkie stiilis kõne",
|
||||||
|
"Walkie-talkie call name": "Walkie-talkie stiilis kõne nimi",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.",
|
||||||
|
"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.)": "Muudab kõneleja heli nii, nagu tuleks see sealt, kus on tema pilt ekraanil. (See on katseline funktsionaalsus ja võib mõjutada heli stabiilsust.)",
|
||||||
|
"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>": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika</2> ja <6>teenusetingimused</6>.<9></9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega</12>"
|
||||||
|
}
|
||||||
132
public/locales/fa/app.json
Normal file
132
public/locales/fa/app.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"Your recent calls": "تماسهای اخیر شما",
|
||||||
|
"Video call": "تماس تصویری",
|
||||||
|
"Video": "ویدیو",
|
||||||
|
"Username": "نام کاربری",
|
||||||
|
"User ID": "آی دی کاربر",
|
||||||
|
"Turn on camera": "روشن کردن دوربین",
|
||||||
|
"Turn off camera": "خاموش کردن دوربین",
|
||||||
|
"Take me Home": "مرا به خانه ببر",
|
||||||
|
"Speaker": "بلندگو",
|
||||||
|
"Sign out": "خروج",
|
||||||
|
"Sign in": "ورود",
|
||||||
|
"Settings": "تنظیمات",
|
||||||
|
"Save": "ذخیره",
|
||||||
|
"Profile": "پروفایل",
|
||||||
|
"Password": "رمز عبور",
|
||||||
|
"No": "خیر",
|
||||||
|
"Mute microphone": "بیصدا کردن میکروفون",
|
||||||
|
"More": "بیشتر",
|
||||||
|
"Microphone": "میکروفون",
|
||||||
|
"Login to your account": "به حساب کاربری خود وارد شوید",
|
||||||
|
"Login": "ورود",
|
||||||
|
"Loading…": "بارگزاری…",
|
||||||
|
"Loading room…": "بارگزاری اتاق…",
|
||||||
|
"Leave": "خروج",
|
||||||
|
"Join existing call?": "پیوست به تماس؟",
|
||||||
|
"Join call now": "الان به تماس بپیوند",
|
||||||
|
"Join call": "پیوستن به تماس",
|
||||||
|
"Invite people": "دعوت از افراد",
|
||||||
|
"Invite": "دعوت",
|
||||||
|
"Home": "خانه",
|
||||||
|
"Go": "رفتن",
|
||||||
|
"Full screen": "تمام صحفه",
|
||||||
|
"Freedom": "آزادی",
|
||||||
|
"Exit full screen": "خروج از حالت تمام صفحه",
|
||||||
|
"Entering room…": "درحال وارد شدن به اتاق…",
|
||||||
|
"Download debug logs": "دانلود لاگ عیبیابی",
|
||||||
|
"Display name": "نام نمایشی",
|
||||||
|
"Developer": "توسعه دهنده",
|
||||||
|
"Details": "جزئیات",
|
||||||
|
"Description (optional)": "توضیحات (اختیاری)",
|
||||||
|
"Debug log request": "درخواست لاگ عیبیابی",
|
||||||
|
"Debug log": "لاگ عیبیابی",
|
||||||
|
"Create account": "ساخت حساب کاربری",
|
||||||
|
"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 type menu": "منوی نوع تماس",
|
||||||
|
"Call link copied": "لینک تماس کپی شد",
|
||||||
|
"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> موافقت میکنید",
|
||||||
|
"Avatar": "آواتار",
|
||||||
|
"Audio": "صدا",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیبیابی را جمعآوری کنیم.",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"Accept microphone permissions to join the call.": "پذیرفتن دسترسی به میکروفون برای پیوستن به تماس.",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "پذیرفتن دسترسی دوربین/ میکروفون برای پیوستن به تماس.",
|
||||||
|
"<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>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>اوه، مشکلی پیش آمده.</0><1>ثبت کردن لاگ رفع اشکال به پیدا کردن مشکل توسط ما کمک میکند</1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>ساخت حساب کاربری</0> Or <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> Or <2>به عنوان یک میهمان وارد شوید</2></1>",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - تماس واکی-تاکی",
|
||||||
|
"{{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}} فرد متصل هستند",
|
||||||
|
"Local volume": "حجم داخلی",
|
||||||
|
"Inspector": "بازرس",
|
||||||
|
"Incompatible versions!": "نسخههای ناسازگار!",
|
||||||
|
"Incompatible versions": "نسخههای ناسازگار",
|
||||||
|
"Spotlight": "نور افکن",
|
||||||
|
"Speaker {{n}}": "بلندگو {{n}}",
|
||||||
|
"Show call inspector": "نمایش بازرس تماس",
|
||||||
|
"Share screen": "اشتراک گذاری صفحه نمایش",
|
||||||
|
"Sending…": "در حال ارسال…",
|
||||||
|
"Sending debug logs…": "در حال ارسال باگهای عیبیابی…",
|
||||||
|
"Send debug logs": "ارسال لاگهای عیبیابی",
|
||||||
|
"Select an option": "یک گزینه را انتخاب کنید",
|
||||||
|
"Saving…": "در حال ذخیره…",
|
||||||
|
"Return to home screen": "برگشت به صفحه اصلی",
|
||||||
|
"Remove": "حذف",
|
||||||
|
"Release to stop": "برای توقف رها کنید",
|
||||||
|
"Release spacebar key to stop": "اسپیس بار را برای توقف رها کنید",
|
||||||
|
"Registering…": "ثبتنام…",
|
||||||
|
"Register": "ثبتنام",
|
||||||
|
"Recaptcha not loaded": "کپچا بارگیری نشد",
|
||||||
|
"Recaptcha dismissed": "بازکپچا رد شد",
|
||||||
|
"Press and hold to talk over {{name}}": "برای صحبت فشار دهید و نگهدارید {{name}}",
|
||||||
|
"Press and hold to talk": "برای صحبت فشار دهید و نگهدارید",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "برای صحبت کردن دکمه اسپیس بار را فشار دهید و نگه دارید {{name}}",
|
||||||
|
"Press and hold spacebar to talk": "برای صحبت کردن کلید فاصله را فشار داده و نگه دارید",
|
||||||
|
"Passwords must match": "رمز عبور باید همخوانی داشته باشد",
|
||||||
|
"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>",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "هنوز ثبتنام نکردهاید؟ <2>ساخت حساب کاربری</2>",
|
||||||
|
"Not now, return to home screen": "الان نه، به صفحه اصلی برگردید",
|
||||||
|
"More menu": "تنظیمات بیشتر",
|
||||||
|
"Microphone permissions needed to join the call.": "برای پیوستن به مکالمه دسترسی به میکروفون نیاز است.",
|
||||||
|
"Microphone {{n}}": "میکروفون {{n}}",
|
||||||
|
"Logging in…": "ورود…",
|
||||||
|
"Include debug logs": "شامل لاگهای عیبیابی",
|
||||||
|
"Having trouble? Help us fix it.": "با مشکلی رو به رو شدید؟ به ما کمک کنید رفعش کنیم.",
|
||||||
|
"Grid layout menu": "منوی طرحبندی شبکهای",
|
||||||
|
"Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.",
|
||||||
|
"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 other participants…": "در انتظار برای دیگر شرکتکنندگان…",
|
||||||
|
"Waiting for network": "در انتظار شبکه",
|
||||||
|
"Video call name": "نامِ تماسِ تصویری",
|
||||||
|
"Version: {{version}}": "نسخه: {{نسخه}}",
|
||||||
|
"User menu": "فهرست کاربر",
|
||||||
|
"Unmute microphone": "میکروفون را باصدا کنید",
|
||||||
|
"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 محافظت می شود و <2>خط مشی رازداری</2> و <6>شرایط خدمات</6> Google اعمال می شود.<9></9>با کلیک کردن بر روی \"ثبت نام\"، شما با <12 >شرایط و ضوابط </12> ما موافقت می کنید",
|
||||||
|
"This call already exists, would you like to join?": "این تماس از قبل وجود دارد، میخواهید بپیوندید؟",
|
||||||
|
"Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.",
|
||||||
|
"Talking…": "در حال صحبت کردن…",
|
||||||
|
"Talk over speaker": "روی بلندگو صحبت کنید",
|
||||||
|
"Submitting feedback…": "در حال ارسال بازخورد…",
|
||||||
|
"Submit feedback": "بازخورد ارائه دهید",
|
||||||
|
"Stop sharing screen": "توقف اشتراکگذاری صفحه نمایش",
|
||||||
|
"Spatial audio": "صدای فضایی"
|
||||||
|
}
|
||||||
134
public/locales/fr/app.json
Normal file
134
public/locales/fr/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"<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",
|
||||||
|
"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…",
|
||||||
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Rejoindre l’appel maintenant</0><1>Ou</1><2>Copier le lien de l’appel et rejoindre plus tard</2>",
|
||||||
|
"{{name}} (Connecting...)": "{{name}} (Connexion…)"
|
||||||
|
}
|
||||||
134
public/locales/id/app.json
Normal file
134
public/locales/id/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"<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",
|
||||||
|
"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",
|
||||||
|
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
|
||||||
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
|
||||||
|
"{{name}} (Connecting...)": "{{name}} (Menghubungkan...)"
|
||||||
|
}
|
||||||
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}} - 워키토키 전화"
|
||||||
|
}
|
||||||
130
public/locales/pl/app.json
Normal file
130
public/locales/pl/app.json
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"More menu": "Menu \"więcej\"",
|
||||||
|
"Login": "Zaloguj się",
|
||||||
|
"Go": "Kontynuuj",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Warunki</2>",
|
||||||
|
"{{count}} people connected|other": "{{count}} ludzi połączono",
|
||||||
|
"Your recent calls": "Twoje ostatnie połączenia",
|
||||||
|
"You can't talk at the same time": "Nie możesz mówić w tym samym czasie",
|
||||||
|
"Yes, join call": "Tak, dołącz do połączenia",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC jest niewspierane lub zablokowane w tej przeglądarce.",
|
||||||
|
"Walkie-talkie call name": "Nazwa połączenia walkie-talkie",
|
||||||
|
"Walkie-talkie call": "Połączenie walkie-talkie",
|
||||||
|
"Waiting for other participants…": "Oczekiwanie na pozostałych uczestników…",
|
||||||
|
"Waiting for network": "Oczekiwanie na sieć",
|
||||||
|
"Video call name": "Nazwa połączenia wideo",
|
||||||
|
"Video call": "Połączenie wideo",
|
||||||
|
"Video": "Wideo",
|
||||||
|
"Version: {{version}}": "Wersja: {{version}}",
|
||||||
|
"Username": "Nazwa użytkownika",
|
||||||
|
"User menu": "Menu użytkownika",
|
||||||
|
"User ID": "ID użytkownika",
|
||||||
|
"Unmute microphone": "Wyłącz wyciszenie mikrofonu",
|
||||||
|
"Turn on camera": "Włącz kamerę",
|
||||||
|
"Turn off camera": "Wyłącz kamerę",
|
||||||
|
"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.)": "Sprawi to, że dźwięk mówcy będzie zdawał się dochodzić z jego miejsca na ekranie. (Funkcja eksperymentalna: może mieć wpływ na stabilność dźwięku.)",
|
||||||
|
"This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
|
||||||
|
"Thanks! We'll get right on it.": "Dziękujemy! Zaraz się tym zajmiemy.",
|
||||||
|
"Talking…": "Mówienie…",
|
||||||
|
"Take me Home": "Zabierz mnie do ekranu startowego",
|
||||||
|
"Submitting feedback…": "Przesyłanie opinii…",
|
||||||
|
"Submit feedback": "Prześlij opinię",
|
||||||
|
"Stop sharing screen": "Zatrzymaj udostępnianie ekranu",
|
||||||
|
"Spotlight": "Centrum uwagi",
|
||||||
|
"Speaker {{n}}": "Głośnik {{n}}",
|
||||||
|
"Speaker": "Głośnik",
|
||||||
|
"Spatial audio": "Dźwięk przestrzenny",
|
||||||
|
"Sign out": "Wyloguj się",
|
||||||
|
"Sign in": "Zaloguj się",
|
||||||
|
"Show call inspector": "Pokaż inspektora połączenia",
|
||||||
|
"Share screen": "Udostępnij ekran",
|
||||||
|
"Settings": "Ustawienia",
|
||||||
|
"Sending…": "Wysyłanie…",
|
||||||
|
"Sending debug logs…": "Wysyłanie dzienników debugowania…",
|
||||||
|
"Send debug logs": "Wyślij dzienniki debugowania",
|
||||||
|
"Select an option": "Wybierz opcję",
|
||||||
|
"Saving…": "Zapisywanie…",
|
||||||
|
"Save": "Zapisz",
|
||||||
|
"Return to home screen": "Powróć do ekranu domowego",
|
||||||
|
"Remove": "Usuń",
|
||||||
|
"Release to stop": "Puść przycisk, aby przestać",
|
||||||
|
"Release spacebar key to stop": "Puść spację, aby przestać",
|
||||||
|
"Registering…": "Rejestrowanie…",
|
||||||
|
"Register": "Zarejestruj",
|
||||||
|
"Recaptcha not loaded": "Recaptcha nie została załadowana",
|
||||||
|
"Recaptcha dismissed": "Recaptcha odrzucona",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Press and hold to talk over {{name}}": "Przytrzymaj, aby mówić wraz z {{name}}",
|
||||||
|
"Press and hold to talk": "Przytrzymaj, aby mówić",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Przytrzymaj spację, aby mówić wraz z {{name}}",
|
||||||
|
"Press and hold spacebar to talk": "Przytrzymaj spację, aby mówić",
|
||||||
|
"Passwords must match": "Hasła muszą być identyczne",
|
||||||
|
"Password": "Hasło",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Inni użytkownicy próbują dołączyć do tego połączenia przy użyciu niekompatybilnych wersji. Powinni oni upewnić się, że odświeżyli stronę w swoich przeglądarkach:<1>{userLis}</1>",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Nie masz konta? <2>Utwórz je</2>",
|
||||||
|
"Not now, return to home screen": "Nie teraz, powróć do ekranu domowego",
|
||||||
|
"No": "Nie",
|
||||||
|
"Mute microphone": "Wycisz mikrofon",
|
||||||
|
"More": "Więcej",
|
||||||
|
"Microphone permissions needed to join the call.": "Aby dołączyć do połączenia, potrzebne są uprawnienia do mikrofonu.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Login to your account": "Zaloguj się do swojego konta",
|
||||||
|
"Logging in…": "Logowanie…",
|
||||||
|
"Local volume": "Lokalna głośność",
|
||||||
|
"Loading…": "Ładowanie…",
|
||||||
|
"Loading room…": "Ładowanie pokoju…",
|
||||||
|
"Leave": "Opuść",
|
||||||
|
"Join existing call?": "Dołączyć do istniejącego połączenia?",
|
||||||
|
"Join call now": "Dołącz do połączenia teraz",
|
||||||
|
"Join call": "Dołącz do połączenia",
|
||||||
|
"Invite people": "Zaproś ludzi",
|
||||||
|
"Invite": "Zaproś",
|
||||||
|
"Inspector": "Inspektor",
|
||||||
|
"Incompatible versions!": "Niekompatybilne wersje!",
|
||||||
|
"Incompatible versions": "Niekompatybilne wersje",
|
||||||
|
"Include debug logs": "Dołącz dzienniki debugowania",
|
||||||
|
"Home": "Strona domowa",
|
||||||
|
"Having trouble? Help us fix it.": "Masz problem? Pomóż nam go naprawić.",
|
||||||
|
"Grid layout menu": "Menu układu siatki",
|
||||||
|
"Full screen": "Pełen ekran",
|
||||||
|
"Freedom": "Wolność",
|
||||||
|
"Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.",
|
||||||
|
"Exit full screen": "Zamknij pełny ekran",
|
||||||
|
"Entering room…": "Wchodzenie do pokoju…",
|
||||||
|
"Download debug logs": "Pobierz dzienniki debugowania",
|
||||||
|
"Display name": "Wyświetlana nazwa",
|
||||||
|
"Developer": "Deweloper",
|
||||||
|
"Details": "Szczegóły",
|
||||||
|
"Description (optional)": "Opis (opcjonalny)",
|
||||||
|
"Debug log request": "Prośba o dzienniki debugowania",
|
||||||
|
"Debug log": "Dzienniki debugowania",
|
||||||
|
"Create account": "Utwórz konto",
|
||||||
|
"Copy and share this call link": "Skopiuj i podziel się linkiem do połączenia",
|
||||||
|
"Copied!": "Skopiowano!",
|
||||||
|
"Connection lost": "Połączenie utracone",
|
||||||
|
"Confirm password": "Potwierdź hasło",
|
||||||
|
"Close": "Zamknij",
|
||||||
|
"Change layout": "Zmień układ",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Aby dołączyć do tego połączenia, potrzebne są uprawnienia do kamery/mikrofonu.",
|
||||||
|
"Camera {{n}}": "Kamera {{n}}",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Call type menu": "Menu rodzaju połączenia",
|
||||||
|
"Call link copied": "Skopiowano link do połączenia",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Warunki</2>",
|
||||||
|
"Avatar": "Awatar",
|
||||||
|
"Audio": "Dźwięk",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Inny użytkownik w tym połączeniu napotkał problem. Aby lepiej zdiagnozować tę usterkę, chcielibyśmy zebrać dzienniki debugowania.",
|
||||||
|
"Accept microphone permissions to join the call.": "Przyznaj uprawnienia do mikrofonu aby dołączyć do połączenia.",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Przyznaj uprawnienia do kamery/mikrofonu aby dołączyć do połączenia.",
|
||||||
|
"<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>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, coś poszło nie tak.</0><1>Przesłanie dzienników debugowania pomoże nam odnaleźć ten błąd.</1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> Albo <2>Dołącz jako gość</2>",
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> Albo <2>Dołącz jako gość</2></1>",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - połączenie walkie-talkie",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is talking…": "{{name}} mówi…",
|
||||||
|
"{{name}} is presenting": "{{name}} prezentuje",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, twoje połączenie zostało zakończone",
|
||||||
|
"{{count}} people connected|one": "{{count}} osoba połączona"
|
||||||
|
}
|
||||||
132
public/locales/ru/app.json
Normal file
132
public/locales/ru/app.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"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 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}} подключился"
|
||||||
|
}
|
||||||
102
public/locales/tr/app.json
Normal file
102
public/locales/tr/app.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"<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ş",
|
||||||
|
"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>"
|
||||||
|
}
|
||||||
134
public/locales/uk/app.json
Normal file
134
public/locales/uk/app.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"Loading…": "Завантаження…",
|
||||||
|
"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 other participants…": "Очікування на інших учасників…",
|
||||||
|
"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": "Вимкнути камеру",
|
||||||
|
"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 і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи кнопку «Зареєструватися», ви погоджуєтеся з нашими <12>Умовами та положеннями</12>",
|
||||||
|
"This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?",
|
||||||
|
"Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.",
|
||||||
|
"Talking…": "Говоріть…",
|
||||||
|
"Talk over speaker": "Говорити через динамік",
|
||||||
|
"Take me Home": "Перейти до Домівки",
|
||||||
|
"Submitting feedback…": "Надсилання відгуку…",
|
||||||
|
"Submit feedback": "Надіслати відгук",
|
||||||
|
"Stop sharing screen": "Припинити показ екрана",
|
||||||
|
"Spotlight": "У центрі уваги",
|
||||||
|
"Speaker {{n}}": "Динамік {{n}}",
|
||||||
|
"Speaker": "Динамік",
|
||||||
|
"Spatial audio": "Просторовий звук",
|
||||||
|
"Sign out": "Вийти",
|
||||||
|
"Sign in": "Увійти",
|
||||||
|
"Show call inspector": "Показати інспектора виклику",
|
||||||
|
"Share screen": "Поділитися екраном",
|
||||||
|
"Settings": "Налаштування",
|
||||||
|
"Sending…": "Надсилання…",
|
||||||
|
"Sending debug logs…": "Надсилання журналу зневадження…",
|
||||||
|
"Send debug logs": "Надіслати журнал зневадження",
|
||||||
|
"Select an option": "Вибрати опцію",
|
||||||
|
"Saving…": "Збереження…",
|
||||||
|
"Save": "Зберегти",
|
||||||
|
"Return to home screen": "Повернутися на екран домівки",
|
||||||
|
"Remove": "Вилучити",
|
||||||
|
"Release to stop": "Відпустіть, щоб закінчити",
|
||||||
|
"Release spacebar key to stop": "Відпустіть пробіл, щоб закінчити",
|
||||||
|
"Registering…": "Реєстрація…",
|
||||||
|
"Register": "Зареєструватися",
|
||||||
|
"Recaptcha not loaded": "Recaptcha не завантажено",
|
||||||
|
"Recaptcha dismissed": "Recaptcha не пройдено",
|
||||||
|
"Profile": "Профіль",
|
||||||
|
"Press and hold to talk over {{name}}": "Затисніть, щоб говорити одночасно з {{name}}",
|
||||||
|
"Press and hold to talk": "Затисніть, щоб говорити",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Щоб говорити одночасно з {{name}}, затисніть пробіл",
|
||||||
|
"Press and hold spacebar to talk": "Затисніть пробіл, щоб говорити",
|
||||||
|
"Passwords must match": "Паролі відрізняються",
|
||||||
|
"Password": "Пароль",
|
||||||
|
"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>",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Ще не зареєстровані? <2>Створіть обліковий запис</2>",
|
||||||
|
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
|
||||||
|
"No": "Ні",
|
||||||
|
"Mute microphone": "Заглушити мікрофон",
|
||||||
|
"More menu": "Усе меню",
|
||||||
|
"More": "Докладніше",
|
||||||
|
"Microphone permissions needed to join the call.": "Для участі у виклику необхідний дозвіл на користування мікрофоном.",
|
||||||
|
"Microphone {{n}}": "Мікрофон {{n}}",
|
||||||
|
"Microphone": "Мікрофон",
|
||||||
|
"Login to your account": "Увійдіть до свого облікового запису",
|
||||||
|
"Login": "Увійти",
|
||||||
|
"Logging in…": "Вхід…",
|
||||||
|
"Local volume": "Локальна гучність",
|
||||||
|
"Loading room…": "Завантаження кімнати…",
|
||||||
|
"Leave": "Вийти",
|
||||||
|
"Join existing call?": "Приєднатися до наявного виклику?",
|
||||||
|
"Join call now": "Приєднатися до виклику зараз",
|
||||||
|
"Join call": "Приєднатися до виклику",
|
||||||
|
"Invite people": "Запросити людей",
|
||||||
|
"Invite": "Запросити",
|
||||||
|
"Inspector": "Інспектор",
|
||||||
|
"Incompatible versions!": "Несумісні версії!",
|
||||||
|
"Incompatible versions": "Несумісні версії",
|
||||||
|
"Include debug logs": "Долучити журнали зневадження",
|
||||||
|
"Home": "Домівка",
|
||||||
|
"Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.",
|
||||||
|
"Grid layout menu": "Меню у вигляді сітки",
|
||||||
|
"Go": "Далі",
|
||||||
|
"Full screen": "Повноекранний режим",
|
||||||
|
"Freedom": "Свобода",
|
||||||
|
"Fetching group call timed out.": "Вичерпано час очікування групового виклику.",
|
||||||
|
"Exit full screen": "Вийти з повноекранного режиму",
|
||||||
|
"Entering room…": "Вхід у кімнату…",
|
||||||
|
"Download debug logs": "Завантажити журнали зневадження",
|
||||||
|
"Display name": "Показуване ім'я",
|
||||||
|
"Developer": "Розробнику",
|
||||||
|
"Details": "Подробиці",
|
||||||
|
"Description (optional)": "Опис (необов'язково)",
|
||||||
|
"Debug log request": "Запит журналу зневадження",
|
||||||
|
"Debug log": "Журнал зневадження",
|
||||||
|
"Create account": "Створити обліковий запис",
|
||||||
|
"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 type menu": "Меню типу виклику",
|
||||||
|
"Call link copied": "Посилання на виклик скопійовано",
|
||||||
|
"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>",
|
||||||
|
"Avatar": "Аватар",
|
||||||
|
"Audio": "Звук",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал зневадження.",
|
||||||
|
"Accept microphone permissions to join the call.": "Надайте дозволи на використання мікрофонів для приєднання до виклику.",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Надайте дозвіл на використання камери/мікрофона для приєднання до виклику.",
|
||||||
|
"<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>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</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> Or <2>Отримати доступ як гість</2></1>",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Виклик-рація",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{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}} під'єднується",
|
||||||
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
|
||||||
|
"{{name}} (Connecting...)": "{{name}} (З'єднання...)"
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
||||||
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_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
|
||||||
@@ -11,19 +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
|
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
yarn link
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
cd matrix-video-chat
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn install
|
|
||||||
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" />
|
||||||
105
src/App.jsx
105
src/App.jsx
@@ -1,105 +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, { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
BrowserRouter as Router,
|
|
||||||
Switch,
|
|
||||||
Route,
|
|
||||||
useLocation,
|
|
||||||
useHistory,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { OverlayProvider } from "@react-aria/overlays";
|
|
||||||
import { Home } from "./Home";
|
|
||||||
import { LoginPage } from "./LoginPage";
|
|
||||||
import { RegisterPage } from "./RegisterPage";
|
|
||||||
import { Room } from "./Room";
|
|
||||||
import {
|
|
||||||
ClientProvider,
|
|
||||||
defaultHomeserverHost,
|
|
||||||
} from "./ConferenceCallManagerHooks";
|
|
||||||
import { useFocusVisible } from "@react-aria/interactions";
|
|
||||||
import styles from "./App.module.css";
|
|
||||||
import { LoadingView } from "./FullScreenView";
|
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
|
||||||
|
|
||||||
export default function App({ history }) {
|
|
||||||
const { isFocusVisible } = useFocusVisible();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const classList = document.body.classList;
|
|
||||||
const hasClass = classList.contains(styles.hideFocus);
|
|
||||||
|
|
||||||
if (isFocusVisible && hasClass) {
|
|
||||||
classList.remove(styles.hideFocus);
|
|
||||||
} else if (!isFocusVisible && !hasClass) {
|
|
||||||
classList.add(styles.hideFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
classList.remove(styles.hideFocus);
|
|
||||||
};
|
|
||||||
}, [isFocusVisible]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router history={history}>
|
|
||||||
<ClientProvider>
|
|
||||||
<OverlayProvider>
|
|
||||||
<Switch>
|
|
||||||
<SentryRoute exact path="/">
|
|
||||||
<Home />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute exact path="/login">
|
|
||||||
<LoginPage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute exact path="/register">
|
|
||||||
<RegisterPage />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute path="/room/:roomId?">
|
|
||||||
<Room />
|
|
||||||
</SentryRoute>
|
|
||||||
<SentryRoute path="*">
|
|
||||||
<RoomRedirect />
|
|
||||||
</SentryRoute>
|
|
||||||
</Switch>
|
|
||||||
</OverlayProvider>
|
|
||||||
</ClientProvider>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomRedirect() {
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let roomId = pathname;
|
|
||||||
|
|
||||||
if (pathname.startsWith("/")) {
|
|
||||||
roomId = roomId.substr(1, roomId.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
|
||||||
roomId = `#${roomId}:${defaultHomeserverHost}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replace(`/room/${roomId}`);
|
|
||||||
}, [pathname, history]);
|
|
||||||
|
|
||||||
return <LoadingView />;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.hideFocus * {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
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 {
|
||||||
@@ -49,11 +49,12 @@
|
|||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: 42px;
|
border-radius: 42px;
|
||||||
font-size: 36px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xl {
|
.xl {
|
||||||
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,57 +0,0 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { CopyButton } from "./button";
|
|
||||||
import { Facepile } from "./Facepile";
|
|
||||||
import { Avatar } from "./Avatar";
|
|
||||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
|
||||||
import styles from "./CallList.module.css";
|
|
||||||
import { getRoomUrl } from "./ConferenceCallManagerHooks";
|
|
||||||
|
|
||||||
export function CallList({ title, rooms, client }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>{title}</h3>
|
|
||||||
<div className={styles.callList}>
|
|
||||||
{rooms.map(({ roomId, roomName, avatarUrl, participants }) => (
|
|
||||||
<CallTile
|
|
||||||
key={roomId}
|
|
||||||
client={client}
|
|
||||||
name={roomName}
|
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
roomId={roomId}
|
|
||||||
participants={participants}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CallTile({ name, avatarUrl, roomId, participants, client }) {
|
|
||||||
return (
|
|
||||||
<div className={styles.callTile}>
|
|
||||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
|
||||||
<Avatar
|
|
||||||
size="md"
|
|
||||||
bgKey={name}
|
|
||||||
src={avatarUrl}
|
|
||||||
fallback={<VideoIcon width={16} height={16} />}
|
|
||||||
className={styles.avatar}
|
|
||||||
/>
|
|
||||||
<div className={styles.callInfo}>
|
|
||||||
<h5>{name}</h5>
|
|
||||||
<p>{getRoomUrl(roomId)}</p>
|
|
||||||
{participants && (
|
|
||||||
<Facepile client={client} participants={participants} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={styles.copyButtonSpacer} />
|
|
||||||
</Link>
|
|
||||||
<CopyButton
|
|
||||||
className={styles.copyButton}
|
|
||||||
variant="icon"
|
|
||||||
value={getRoomUrl(roomId)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
365
src/ClientContext.tsx
Normal file
365
src/ClientContext.tsx
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
useRef,
|
||||||
|
} 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,
|
||||||
|
fallbackICEServerAllowed,
|
||||||
|
} 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 initializing = useRef(false);
|
||||||
|
const [
|
||||||
|
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||||
|
setState,
|
||||||
|
] = useState<ClientProviderState>({
|
||||||
|
loading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isPasswordlessUser: false,
|
||||||
|
client: undefined,
|
||||||
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// In case the component is mounted, unmounted, and remounted quickly (as
|
||||||
|
// React does in strict mode), we need to make sure not to doubly initialize
|
||||||
|
// the client
|
||||||
|
if (initializing.current) return;
|
||||||
|
initializing.current = true;
|
||||||
|
|
||||||
|
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,
|
||||||
|
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => (initializing.current = false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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,437 +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 EventEmitter from "events";
|
|
||||||
|
|
||||||
export class ConferenceCallDebugger extends EventEmitter {
|
|
||||||
constructor(client, groupCall) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.client = client;
|
|
||||||
this.groupCall = groupCall;
|
|
||||||
|
|
||||||
this.debugState = {
|
|
||||||
users: new Map(),
|
|
||||||
calls: new Map(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.bufferedEvents = [];
|
|
||||||
|
|
||||||
client.on("event", this._onEvent);
|
|
||||||
groupCall.on("call", this._onCall);
|
|
||||||
groupCall.on("debugstate", this._onDebugStateChanged);
|
|
||||||
groupCall.on("entered", this._onEntered);
|
|
||||||
groupCall.on("left", this._onLeft);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onEntered = () => {
|
|
||||||
const eventCount = this.bufferedEvents.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < eventCount; i++) {
|
|
||||||
const event = this.bufferedEvents.pop();
|
|
||||||
this._onEvent(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_onLeft = () => {
|
|
||||||
this.bufferedEvents = [];
|
|
||||||
this.debugState = {
|
|
||||||
users: new Map(),
|
|
||||||
calls: new Map(),
|
|
||||||
};
|
|
||||||
this.emit("debug");
|
|
||||||
};
|
|
||||||
|
|
||||||
_onEvent = (event) => {
|
|
||||||
if (!this.groupCall.entered) {
|
|
||||||
this.bufferedEvents.push(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomId = event.getRoomId();
|
|
||||||
const type = event.getType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
roomId === this.groupCall.room.roomId &&
|
|
||||||
(type.startsWith("m.call.") ||
|
|
||||||
type === "me.robertlong.call.info" ||
|
|
||||||
type === "m.room.member")
|
|
||||||
) {
|
|
||||||
const sender = event.getSender();
|
|
||||||
const { call_id } = event.getContent();
|
|
||||||
|
|
||||||
if (call_id) {
|
|
||||||
if (this.debugState.calls.has(call_id)) {
|
|
||||||
const callState = this.debugState.calls.get(call_id);
|
|
||||||
callState.events.push(event);
|
|
||||||
} else {
|
|
||||||
this.debugState.calls.set(call_id, {
|
|
||||||
state: "unknown",
|
|
||||||
events: [event],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debugState.users.has(sender)) {
|
|
||||||
const userState = this.debugState.users.get(sender);
|
|
||||||
userState.events.push(event);
|
|
||||||
} else {
|
|
||||||
this.debugState.users.set(sender, {
|
|
||||||
state: "unknown",
|
|
||||||
events: [event],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit("debug");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_onDebugStateChanged = (userId, callId, state) => {
|
|
||||||
if (userId) {
|
|
||||||
const userState = this.debugState.users.get(userId);
|
|
||||||
|
|
||||||
if (userState) {
|
|
||||||
userState.state = state;
|
|
||||||
} else {
|
|
||||||
this.debugState.users.set(userId, {
|
|
||||||
state,
|
|
||||||
events: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callId) {
|
|
||||||
const callState = this.debugState.calls.get(callId);
|
|
||||||
|
|
||||||
if (callState) {
|
|
||||||
callState.state = state;
|
|
||||||
} else {
|
|
||||||
this.debugState.calls.set(callId, {
|
|
||||||
state,
|
|
||||||
events: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit("debug");
|
|
||||||
};
|
|
||||||
|
|
||||||
_onCall = (call) => {
|
|
||||||
const peerConnection = call.peerConn;
|
|
||||||
|
|
||||||
if (!peerConnection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendWebRTCInfoEvent = async (eventType) => {
|
|
||||||
const event = {
|
|
||||||
call_id: call.callId,
|
|
||||||
eventType,
|
|
||||||
iceConnectionState: peerConnection.iceConnectionState,
|
|
||||||
iceGatheringState: peerConnection.iceGatheringState,
|
|
||||||
signalingState: peerConnection.signalingState,
|
|
||||||
selectedCandidatePair: null,
|
|
||||||
localCandidate: null,
|
|
||||||
remoteCandidate: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// getStats doesn't support selectors in Firefox so get all stats by passing null.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats#browser_compatibility
|
|
||||||
const stats = await peerConnection.getStats(null);
|
|
||||||
|
|
||||||
const statsArr = Array.from(stats.values());
|
|
||||||
|
|
||||||
// Matrix doesn't support floats so we convert time in seconds to ms
|
|
||||||
function secToMs(time) {
|
|
||||||
if (time === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.round(time * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function processTransportStats(transportStats) {
|
|
||||||
if (!transportStats) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
packetsSent: transportStats.packetsSent,
|
|
||||||
packetsReceived: transportStats.packetsReceived,
|
|
||||||
bytesSent: transportStats.bytesSent,
|
|
||||||
bytesReceived: transportStats.bytesReceived,
|
|
||||||
iceRole: transportStats.iceRole,
|
|
||||||
iceState: transportStats.iceState,
|
|
||||||
dtlsState: transportStats.dtlsState,
|
|
||||||
dtlsCipher: transportStats.dtlsCipher,
|
|
||||||
tlsVersion: transportStats.tlsVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processCandidateStats(candidateStats) {
|
|
||||||
if (!candidateStats) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Figure out how to normalize ip and address across browsers
|
|
||||||
// networkType property excluded for privacy reasons:
|
|
||||||
// https://www.w3.org/TR/webrtc-stats/#sotd
|
|
||||||
return {
|
|
||||||
priority:
|
|
||||||
candidateStats.priority && candidateStats.priority.toString(),
|
|
||||||
candidateType: candidateStats.candidateType,
|
|
||||||
protocol: candidateStats.protocol,
|
|
||||||
address: !!candidateStats.address
|
|
||||||
? candidateStats.address
|
|
||||||
: candidateStats.ip,
|
|
||||||
port: candidateStats.port,
|
|
||||||
url: candidateStats.url,
|
|
||||||
relayProtocol: candidateStats.relayProtocol,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processCandidatePair(candidatePairStats) {
|
|
||||||
if (!candidatePairStats) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localCandidateStats = statsArr.find(
|
|
||||||
(stat) => stat.id === candidatePairStats.localCandidateId
|
|
||||||
);
|
|
||||||
event.localCandidate = processCandidateStats(localCandidateStats);
|
|
||||||
|
|
||||||
const remoteCandidateStats = statsArr.find(
|
|
||||||
(stat) => stat.id === candidatePairStats.remoteCandidateId
|
|
||||||
);
|
|
||||||
event.remoteCandidate = processCandidateStats(remoteCandidateStats);
|
|
||||||
|
|
||||||
const transportStats = statsArr.find(
|
|
||||||
(stat) => stat.id === candidatePairStats.transportId
|
|
||||||
);
|
|
||||||
event.transport = processTransportStats(transportStats);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state: candidatePairStats.state,
|
|
||||||
bytesSent: candidatePairStats.bytesSent,
|
|
||||||
bytesReceived: candidatePairStats.bytesReceived,
|
|
||||||
requestsSent: candidatePairStats.requestsSent,
|
|
||||||
requestsReceived: candidatePairStats.requestsReceived,
|
|
||||||
responsesSent: candidatePairStats.responsesSent,
|
|
||||||
responsesReceived: candidatePairStats.responsesReceived,
|
|
||||||
currentRoundTripTime: secToMs(
|
|
||||||
candidatePairStats.currentRoundTripTime
|
|
||||||
),
|
|
||||||
totalRoundTripTime: secToMs(candidatePairStats.totalRoundTripTime),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Firefox uses the deprecated "selected" property for the nominated ice candidate.
|
|
||||||
const selectedCandidatePair = statsArr.find(
|
|
||||||
(stat) =>
|
|
||||||
stat.type === "candidate-pair" && (stat.selected || stat.nominated)
|
|
||||||
);
|
|
||||||
|
|
||||||
event.selectedCandidatePair = processCandidatePair(selectedCandidatePair);
|
|
||||||
|
|
||||||
function processCodecStats(codecStats) {
|
|
||||||
if (!codecStats) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payload type enums and MIME types listed here:
|
|
||||||
// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml
|
|
||||||
return {
|
|
||||||
mimeType: codecStats.mimeType,
|
|
||||||
clockRate: codecStats.clockRate,
|
|
||||||
payloadType: codecStats.payloadType,
|
|
||||||
channels: codecStats.channels,
|
|
||||||
sdpFmtpLine: codecStats.sdpFmtpLine,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processRTPStreamStats(rtpStreamStats) {
|
|
||||||
const codecStats = statsArr.find(
|
|
||||||
(stat) => stat.id === rtpStreamStats.codecId
|
|
||||||
);
|
|
||||||
const codec = processCodecStats(codecStats);
|
|
||||||
|
|
||||||
return {
|
|
||||||
kind: rtpStreamStats.kind,
|
|
||||||
codec,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processInboundRTPStats(inboundRTPStats) {
|
|
||||||
const rtpStreamStats = processRTPStreamStats(inboundRTPStats);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rtpStreamStats,
|
|
||||||
decoderImplementation: inboundRTPStats.decoderImplementation,
|
|
||||||
bytesReceived: inboundRTPStats.bytesReceived,
|
|
||||||
packetsReceived: inboundRTPStats.packetsReceived,
|
|
||||||
packetsLost: inboundRTPStats.packetsLost,
|
|
||||||
jitter: secToMs(inboundRTPStats.jitter),
|
|
||||||
frameWidth: inboundRTPStats.frameWidth,
|
|
||||||
frameHeight: inboundRTPStats.frameHeight,
|
|
||||||
frameBitDepth: inboundRTPStats.frameBitDepth,
|
|
||||||
framesPerSecond:
|
|
||||||
inboundRTPStats.framesPerSecond &&
|
|
||||||
inboundRTPStats.framesPerSecond.toString(),
|
|
||||||
framesReceived: inboundRTPStats.framesReceived,
|
|
||||||
framesDecoded: inboundRTPStats.framesDecoded,
|
|
||||||
framesDropped: inboundRTPStats.framesDropped,
|
|
||||||
totalSamplesDecoded: inboundRTPStats.totalSamplesDecoded,
|
|
||||||
totalDecodeTime: secToMs(inboundRTPStats.totalDecodeTime),
|
|
||||||
totalProcessingDelay: secToMs(inboundRTPStats.totalProcessingDelay),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processOutboundRTPStats(outboundRTPStats) {
|
|
||||||
const rtpStreamStats = processRTPStreamStats(outboundRTPStats);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rtpStreamStats,
|
|
||||||
encoderImplementation: outboundRTPStats.encoderImplementation,
|
|
||||||
bytesSent: outboundRTPStats.bytesSent,
|
|
||||||
packetsSent: outboundRTPStats.packetsSent,
|
|
||||||
frameWidth: outboundRTPStats.frameWidth,
|
|
||||||
frameHeight: outboundRTPStats.frameHeight,
|
|
||||||
frameBitDepth: outboundRTPStats.frameBitDepth,
|
|
||||||
framesPerSecond:
|
|
||||||
outboundRTPStats.framesPerSecond &&
|
|
||||||
outboundRTPStats.framesPerSecond.toString(),
|
|
||||||
framesSent: outboundRTPStats.framesSent,
|
|
||||||
framesEncoded: outboundRTPStats.framesEncoded,
|
|
||||||
qualityLimitationReason: outboundRTPStats.qualityLimitationReason,
|
|
||||||
qualityLimitationResolutionChanges:
|
|
||||||
outboundRTPStats.qualityLimitationResolutionChanges,
|
|
||||||
totalEncodeTime: secToMs(outboundRTPStats.totalEncodeTime),
|
|
||||||
totalPacketSendDelay: secToMs(outboundRTPStats.totalPacketSendDelay),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processRemoteInboundRTPStats(remoteInboundRTPStats) {
|
|
||||||
const rtpStreamStats = processRTPStreamStats(remoteInboundRTPStats);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rtpStreamStats,
|
|
||||||
packetsReceived: remoteInboundRTPStats.packetsReceived,
|
|
||||||
packetsLost: remoteInboundRTPStats.packetsLost,
|
|
||||||
jitter: secToMs(remoteInboundRTPStats.jitter),
|
|
||||||
framesDropped: remoteInboundRTPStats.framesDropped,
|
|
||||||
roundTripTime: secToMs(remoteInboundRTPStats.roundTripTime),
|
|
||||||
totalRoundTripTime: secToMs(remoteInboundRTPStats.totalRoundTripTime),
|
|
||||||
fractionLost:
|
|
||||||
remoteInboundRTPStats.fractionLost !== undefined &&
|
|
||||||
remoteInboundRTPStats.fractionLost.toString(),
|
|
||||||
reportsReceived: remoteInboundRTPStats.reportsReceived,
|
|
||||||
roundTripTimeMeasurements:
|
|
||||||
remoteInboundRTPStats.roundTripTimeMeasurements,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function processRemoteOutboundRTPStats(remoteOutboundRTPStats) {
|
|
||||||
const rtpStreamStats = processRTPStreamStats(remoteOutboundRTPStats);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rtpStreamStats,
|
|
||||||
encoderImplementation: remoteOutboundRTPStats.encoderImplementation,
|
|
||||||
bytesSent: remoteOutboundRTPStats.bytesSent,
|
|
||||||
packetsSent: remoteOutboundRTPStats.packetsSent,
|
|
||||||
roundTripTime: secToMs(remoteOutboundRTPStats.roundTripTime),
|
|
||||||
totalRoundTripTime: secToMs(
|
|
||||||
remoteOutboundRTPStats.totalRoundTripTime
|
|
||||||
),
|
|
||||||
reportsSent: remoteOutboundRTPStats.reportsSent,
|
|
||||||
roundTripTimeMeasurements:
|
|
||||||
remoteOutboundRTPStats.roundTripTimeMeasurements,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
event.inboundRTP = statsArr
|
|
||||||
.filter((stat) => stat.type === "inbound-rtp")
|
|
||||||
.map(processInboundRTPStats);
|
|
||||||
|
|
||||||
event.outboundRTP = statsArr
|
|
||||||
.filter((stat) => stat.type === "outbound-rtp")
|
|
||||||
.map(processOutboundRTPStats);
|
|
||||||
|
|
||||||
event.remoteInboundRTP = statsArr
|
|
||||||
.filter((stat) => stat.type === "remote-inbound-rtp")
|
|
||||||
.map(processRemoteInboundRTPStats);
|
|
||||||
|
|
||||||
event.remoteOutboundRTP = statsArr
|
|
||||||
.filter((stat) => stat.type === "remote-outbound-rtp")
|
|
||||||
.map(processRemoteOutboundRTPStats);
|
|
||||||
|
|
||||||
this.client.sendEvent(
|
|
||||||
this.groupCall.room.roomId,
|
|
||||||
"me.robertlong.call.info",
|
|
||||||
event
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let statsTimeout;
|
|
||||||
|
|
||||||
const sendStats = () => {
|
|
||||||
if (
|
|
||||||
call.state === "ended" ||
|
|
||||||
peerConnection.connectionState === "closed"
|
|
||||||
) {
|
|
||||||
clearTimeout(statsTimeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendWebRTCInfoEvent("stats");
|
|
||||||
statsTimeout = setTimeout(sendStats, 30 * 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(sendStats, 30 * 1000);
|
|
||||||
|
|
||||||
peerConnection.addEventListener("iceconnectionstatechange", () => {
|
|
||||||
sendWebRTCInfoEvent("iceconnectionstatechange");
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener("icegatheringstatechange", () => {
|
|
||||||
sendWebRTCInfoEvent("icegatheringstatechange");
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener("negotiationneeded", () => {
|
|
||||||
sendWebRTCInfoEvent("negotiationneeded");
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener("track", () => {
|
|
||||||
sendWebRTCInfoEvent("track");
|
|
||||||
});
|
|
||||||
// NOTE: Not available on Firefox
|
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1561441
|
|
||||||
peerConnection.addEventListener(
|
|
||||||
"icecandidateerror",
|
|
||||||
({ errorCode, url, errorText }) => {
|
|
||||||
this.client.sendEvent(
|
|
||||||
this.groupCall.room.roomId,
|
|
||||||
"me.robertlong.call.ice_error",
|
|
||||||
{
|
|
||||||
call_id: call.callId,
|
|
||||||
errorCode,
|
|
||||||
url,
|
|
||||||
errorText,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
peerConnection.addEventListener("signalingstatechange", () => {
|
|
||||||
sendWebRTCInfoEvent("signalingstatechange");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,720 +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 matrix from "matrix-js-sdk/src/browser-index";
|
|
||||||
import {
|
|
||||||
GroupCallIntent,
|
|
||||||
GroupCallType,
|
|
||||||
} from "matrix-js-sdk/src/browser-index";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
export const defaultHomeserver =
|
|
||||||
import.meta.env.VITE_DEFAULT_HOMESERVER ||
|
|
||||||
`${window.location.protocol}//${window.location.host}`;
|
|
||||||
|
|
||||||
export const defaultHomeserverHost = new URL(defaultHomeserver).host;
|
|
||||||
|
|
||||||
const ClientContext = createContext();
|
|
||||||
|
|
||||||
function waitForSync(client) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const onSync = (state, _old, data) => {
|
|
||||||
if (state === "PREPARED") {
|
|
||||||
resolve();
|
|
||||||
client.removeListener("sync", onSync);
|
|
||||||
} else if (state === "ERROR") {
|
|
||||||
reject(data?.error);
|
|
||||||
client.removeListener("sync", onSync);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client.on("sync", onSync);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initClient(clientOptions, guest) {
|
|
||||||
const client = matrix.createClient(clientOptions);
|
|
||||||
|
|
||||||
if (guest) {
|
|
||||||
client.setGuest(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.startClient({
|
|
||||||
// dirty hack to reduce chance of gappy syncs
|
|
||||||
// should be fixed by spotting gaps and backpaginating
|
|
||||||
initialSyncLimit: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForSync(client);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchGroupCall(
|
|
||||||
client,
|
|
||||||
roomIdOrAlias,
|
|
||||||
viaServers = undefined,
|
|
||||||
timeout = 5000
|
|
||||||
) {
|
|
||||||
const { roomId } = await client.joinRoom(roomIdOrAlias, { viaServers });
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timeoutId;
|
|
||||||
|
|
||||||
function onGroupCallIncoming(groupCall) {
|
|
||||||
if (groupCall && groupCall.room.roomId === roomId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
client.removeListener("GroupCall.incoming", onGroupCallIncoming);
|
|
||||||
resolve(groupCall);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupCall = client.getGroupCallForRoom(roomId);
|
|
||||||
|
|
||||||
if (groupCall) {
|
|
||||||
resolve(groupCall);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on("GroupCall.incoming", onGroupCallIncoming);
|
|
||||||
|
|
||||||
if (timeout) {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
client.removeListener("GroupCall.incoming", onGroupCallIncoming);
|
|
||||||
reject(new Error("Fetching group call timed out."));
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ClientProvider({ children }) {
|
|
||||||
const history = useHistory();
|
|
||||||
const [
|
|
||||||
{ loading, isAuthenticated, isPasswordlessUser, isGuest, client, userName },
|
|
||||||
setState,
|
|
||||||
] = useState({
|
|
||||||
loading: true,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
isGuest: false,
|
|
||||||
client: undefined,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function restore() {
|
|
||||||
try {
|
|
||||||
const authStore = localStorage.getItem("matrix-auth-store");
|
|
||||||
|
|
||||||
if (authStore) {
|
|
||||||
const {
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
access_token,
|
|
||||||
guest,
|
|
||||||
passwordlessUser,
|
|
||||||
tempPassword,
|
|
||||||
} = JSON.parse(authStore);
|
|
||||||
|
|
||||||
const client = await initClient(
|
|
||||||
{
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
},
|
|
||||||
guest
|
|
||||||
);
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
access_token,
|
|
||||||
guest,
|
|
||||||
passwordlessUser,
|
|
||||||
tempPassword,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return { client, guest, passwordlessUser };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { client: undefined, guest: false };
|
|
||||||
} catch (err) {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restore()
|
|
||||||
.then(({ client, guest, passwordlessUser }) => {
|
|
||||||
setState({
|
|
||||||
client,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: !!client,
|
|
||||||
isPasswordlessUser: !!passwordlessUser,
|
|
||||||
isGuest: guest,
|
|
||||||
userName: client?.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setState({
|
|
||||||
client: undefined,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
isGuest: false,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const login = useCallback(async (homeserver, username, password) => {
|
|
||||||
try {
|
|
||||||
let loginHomeserverUrl = homeserver.trim();
|
|
||||||
|
|
||||||
if (!loginHomeserverUrl.includes("://")) {
|
|
||||||
loginHomeserverUrl = "https://" + loginHomeserverUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const wellKnownUrl = new URL(
|
|
||||||
"/.well-known/matrix/client",
|
|
||||||
window.location
|
|
||||||
);
|
|
||||||
const response = await fetch(wellKnownUrl);
|
|
||||||
const config = await response.json();
|
|
||||||
|
|
||||||
if (config["m.homeserver"]) {
|
|
||||||
loginHomeserverUrl = config["m.homeserver"];
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
|
|
||||||
const registrationClient = matrix.createClient(loginHomeserverUrl);
|
|
||||||
|
|
||||||
const { user_id, device_id, access_token } =
|
|
||||||
await registrationClient.loginWithPassword(username, password);
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: loginHomeserverUrl,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({ user_id, device_id, access_token })
|
|
||||||
);
|
|
||||||
|
|
||||||
setState({
|
|
||||||
client,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
isGuest: false,
|
|
||||||
userName: client.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
setState({
|
|
||||||
client: undefined,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
isGuest: false,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const registerGuest = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const registrationClient = matrix.createClient(defaultHomeserver);
|
|
||||||
|
|
||||||
const { user_id, device_id, access_token } =
|
|
||||||
await registrationClient.registerGuest({});
|
|
||||||
|
|
||||||
const client = await initClient(
|
|
||||||
{
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.setProfileInfo("displayname", {
|
|
||||||
displayname: `Guest ${client.getUserIdLocalpart()}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({ user_id, device_id, access_token, guest: true })
|
|
||||||
);
|
|
||||||
|
|
||||||
setState({
|
|
||||||
client,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isGuest: true,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
userName: client.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
setState({
|
|
||||||
client: undefined,
|
|
||||||
loading: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
isGuest: false,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const register = useCallback(async (username, password, passwordlessUser) => {
|
|
||||||
try {
|
|
||||||
const registrationClient = matrix.createClient(defaultHomeserver);
|
|
||||||
|
|
||||||
const { user_id, device_id, access_token } =
|
|
||||||
await registrationClient.register(username, password, null, {
|
|
||||||
type: "m.login.dummy",
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
|
||||||
|
|
||||||
if (passwordlessUser) {
|
|
||||||
session.tempPassword = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
|
||||||
|
|
||||||
setState({
|
|
||||||
client,
|
|
||||||
loading: false,
|
|
||||||
isGuest: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isPasswordlessUser: passwordlessUser,
|
|
||||||
userName: client.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return client;
|
|
||||||
} catch (err) {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
setState({
|
|
||||||
client: undefined,
|
|
||||||
loading: false,
|
|
||||||
isGuest: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
userName: null,
|
|
||||||
});
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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,
|
|
||||||
isGuest: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isPasswordlessUser: false,
|
|
||||||
userName: client.getUserIdLocalpart(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
localStorage.removeItem("matrix-auth-store");
|
|
||||||
window.location = "/";
|
|
||||||
}, [history]);
|
|
||||||
|
|
||||||
const context = useMemo(
|
|
||||||
() => ({
|
|
||||||
loading,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
isGuest,
|
|
||||||
client,
|
|
||||||
login,
|
|
||||||
registerGuest,
|
|
||||||
register,
|
|
||||||
changePassword,
|
|
||||||
logout,
|
|
||||||
userName,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
loading,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
isGuest,
|
|
||||||
client,
|
|
||||||
login,
|
|
||||||
registerGuest,
|
|
||||||
register,
|
|
||||||
changePassword,
|
|
||||||
logout,
|
|
||||||
userName,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useClient() {
|
|
||||||
return useContext(ClientContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function roomAliasFromRoomName(roomName) {
|
|
||||||
return roomName
|
|
||||||
.trim()
|
|
||||||
.replace(/\s/g, "-")
|
|
||||||
.replace(/[^\w-]/g, "")
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRoom(client, name) {
|
|
||||||
const { room_id, room_alias } = await client.createRoom({
|
|
||||||
visibility: "private",
|
|
||||||
preset: "public_chat",
|
|
||||||
name,
|
|
||||||
room_alias_name: roomAliasFromRoomName(name),
|
|
||||||
power_level_content_override: {
|
|
||||||
invite: 100,
|
|
||||||
kick: 100,
|
|
||||||
ban: 100,
|
|
||||||
redact: 50,
|
|
||||||
state_default: 0,
|
|
||||||
events_default: 0,
|
|
||||||
users_default: 0,
|
|
||||||
events: {
|
|
||||||
"m.room.power_levels": 100,
|
|
||||||
"m.room.history_visibility": 100,
|
|
||||||
"m.room.tombstone": 100,
|
|
||||||
"m.room.encryption": 100,
|
|
||||||
"m.room.name": 50,
|
|
||||||
"m.room.message": 0,
|
|
||||||
"m.room.encrypted": 50,
|
|
||||||
"m.sticker": 50,
|
|
||||||
"org.matrix.msc3401.call.member": 0,
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
[client.getUserId()]: 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.setGuestAccess(room_id, {
|
|
||||||
allowJoin: true,
|
|
||||||
allowRead: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.createGroupCall(
|
|
||||||
room_id,
|
|
||||||
GroupCallType.Video,
|
|
||||||
GroupCallIntent.Prompt
|
|
||||||
);
|
|
||||||
|
|
||||||
return room_alias || room_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLoadGroupCall(client, roomId, viaServers) {
|
|
||||||
const [state, setState] = useState({
|
|
||||||
loading: true,
|
|
||||||
error: undefined,
|
|
||||||
groupCall: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setState({ loading: true });
|
|
||||||
fetchGroupCall(client, roomId, viaServers, 30000)
|
|
||||||
.then((groupCall) => setState({ loading: false, groupCall }))
|
|
||||||
.catch((error) => setState({ loading: false, error }));
|
|
||||||
}, [client, roomId]);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tsCache = {};
|
|
||||||
|
|
||||||
function getLastTs(client, r) {
|
|
||||||
if (tsCache[r.roomId]) {
|
|
||||||
return tsCache[r.roomId];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!r || !r.timeline) {
|
|
||||||
const ts = Number.MAX_SAFE_INTEGER;
|
|
||||||
tsCache[r.roomId] = ts;
|
|
||||||
return ts;
|
|
||||||
}
|
|
||||||
|
|
||||||
const myUserId = client.getUserId();
|
|
||||||
|
|
||||||
if (r.getMyMembership() !== "join") {
|
|
||||||
const membershipEvent = r.currentState.getStateEvents(
|
|
||||||
"m.room.member",
|
|
||||||
myUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
|
||||||
const ts = membershipEvent.getTs();
|
|
||||||
tsCache[r.roomId] = ts;
|
|
||||||
return ts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
|
||||||
const ev = r.timeline[i];
|
|
||||||
const ts = ev.getTs();
|
|
||||||
|
|
||||||
if (ts) {
|
|
||||||
tsCache[r.roomId] = ts;
|
|
||||||
return ts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ts = Number.MAX_SAFE_INTEGER;
|
|
||||||
tsCache[r.roomId] = ts;
|
|
||||||
return ts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortRooms(client, rooms) {
|
|
||||||
return rooms.sort((a, b) => {
|
|
||||||
return getLastTs(client, b) - getLastTs(client, a);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGroupCallRooms(client) {
|
|
||||||
const [rooms, setRooms] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function updateRooms() {
|
|
||||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
|
||||||
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
|
||||||
const sortedRooms = sortRooms(client, rooms);
|
|
||||||
const items = sortedRooms.map((room) => {
|
|
||||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
roomId: room.getCanonicalAlias() || room.roomId,
|
|
||||||
roomName: room.name,
|
|
||||||
avatarUrl: null,
|
|
||||||
room,
|
|
||||||
groupCall,
|
|
||||||
participants: [...groupCall.participants],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setRooms(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRooms();
|
|
||||||
|
|
||||||
client.on("GroupCall.incoming", updateRooms);
|
|
||||||
client.on("GroupCall.participants", updateRooms);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
client.removeListener("GroupCall.incoming", updateRooms);
|
|
||||||
client.removeListener("GroupCall.participants", updateRooms);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return rooms;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePublicRooms(client, publicSpaceRoomId, maxRooms = 50) {
|
|
||||||
const [rooms, setRooms] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (publicSpaceRoomId) {
|
|
||||||
client.getRoomHierarchy(publicSpaceRoomId, maxRooms).then(({ rooms }) => {
|
|
||||||
const filteredRooms = rooms
|
|
||||||
.filter((room) => room.room_type !== "m.space")
|
|
||||||
.map((room) => ({
|
|
||||||
roomId: room.room_alias || room.room_id,
|
|
||||||
roomName: room.name,
|
|
||||||
avatarUrl: null,
|
|
||||||
room,
|
|
||||||
participants: [],
|
|
||||||
}));
|
|
||||||
|
|
||||||
setRooms(filteredRooms);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setRooms([]);
|
|
||||||
}
|
|
||||||
}, [publicSpaceRoomId]);
|
|
||||||
|
|
||||||
return rooms;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRoomUrl(roomId) {
|
|
||||||
if (roomId.startsWith("#")) {
|
|
||||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
|
||||||
|
|
||||||
if (host !== defaultHomeserverHost) {
|
|
||||||
return `${window.location.host}/room/${roomId}`;
|
|
||||||
} else {
|
|
||||||
return `${window.location.host}/${localPart}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return `${window.location.host}/room/${roomId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAvatarUrl(client, mxcUrl, avatarSize = 96) {
|
|
||||||
const width = Math.floor(avatarSize * window.devicePixelRatio);
|
|
||||||
const height = Math.floor(avatarSize * window.devicePixelRatio);
|
|
||||||
return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProfile(client) {
|
|
||||||
const [{ loading, displayName, avatarUrl, error, success }, setState] =
|
|
||||||
useState(() => {
|
|
||||||
const user = client?.getUser(client.getUserId());
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
loading: false,
|
|
||||||
displayName: user?.displayName,
|
|
||||||
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onChangeUser = (_event, { displayName, avatarUrl }) => {
|
|
||||||
setState({
|
|
||||||
success: false,
|
|
||||||
loading: false,
|
|
||||||
displayName,
|
|
||||||
avatarUrl: getAvatarUrl(client, avatarUrl),
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let user;
|
|
||||||
|
|
||||||
if (client) {
|
|
||||||
const userId = client.getUserId();
|
|
||||||
user = client.getUser(userId);
|
|
||||||
user.on("User.displayName", onChangeUser);
|
|
||||||
user.on("User.avatarUrl", onChangeUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (user) {
|
|
||||||
user.removeListener("User.displayName", onChangeUser);
|
|
||||||
user.removeListener("User.avatarUrl", onChangeUser);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
const saveProfile = useCallback(
|
|
||||||
async ({ displayName, avatar }) => {
|
|
||||||
if (client) {
|
|
||||||
setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
loading: true,
|
|
||||||
error: null,
|
|
||||||
success: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.setDisplayName(displayName);
|
|
||||||
|
|
||||||
let mxcAvatarUrl;
|
|
||||||
|
|
||||||
if (avatar) {
|
|
||||||
mxcAvatarUrl = await client.uploadContent(avatar);
|
|
||||||
await client.setAvatarUrl(mxcAvatarUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
displayName,
|
|
||||||
avatarUrl: mxcAvatarUrl
|
|
||||||
? getAvatarUrl(client, mxcAvatarUrl)
|
|
||||||
: prev.avatarUrl,
|
|
||||||
loading: false,
|
|
||||||
success: true,
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
loading: false,
|
|
||||||
error,
|
|
||||||
success: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("Client not initialized before calling saveProfile");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { loading, error, displayName, avatarUrl, saveProfile, success };
|
|
||||||
}
|
|
||||||
@@ -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 "./ConferenceCallManagerHooks";
|
|
||||||
|
|
||||||
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.getMxcAvatarUrl();
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
key={member.userId}
|
||||||
|
size={size}
|
||||||
|
src={avatarUrl ?? undefined}
|
||||||
|
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,43 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
|
||||||
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
|
|
||||||
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
|
|
||||||
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
|
|
||||||
import styles from "./GridLayoutMenu.module.css";
|
|
||||||
import { Menu } from "./Menu";
|
|
||||||
import { Item } from "@react-stately/collections";
|
|
||||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
|
||||||
|
|
||||||
export function GridLayoutMenu({ layout, setLayout }) {
|
|
||||||
return (
|
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button variant="icon">
|
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
|
||||||
</Button>
|
|
||||||
{(props) => (
|
|
||||||
<Tooltip position="bottom" {...props}>
|
|
||||||
Layout Type
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
|
||||||
{(props) => (
|
|
||||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
|
||||||
<Item key="freedom" textValue="Freedom">
|
|
||||||
<FreedomIcon />
|
|
||||||
<span>Freedom</span>
|
|
||||||
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
|
|
||||||
</Item>
|
|
||||||
<Item key="spotlight" textValue="Spotlight">
|
|
||||||
<SpotlightIcon />
|
|
||||||
<span>Spotlight</span>
|
|
||||||
{layout === "spotlight" && (
|
|
||||||
<CheckIcon className={styles.checkIcon} />
|
|
||||||
)}
|
|
||||||
</Item>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</PopoverMenuTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.checkIcon {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkIcon * {
|
|
||||||
stroke: var(--textColor1);
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import { Resizable } from "re-resizable";
|
|
||||||
import React, { useEffect, useState, useMemo } from "react";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import ReactJson from "react-json-view";
|
|
||||||
|
|
||||||
function getCallUserId(call) {
|
|
||||||
return call.getOpponentMember()?.userId || call.invitee || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCallState(call) {
|
|
||||||
return {
|
|
||||||
id: call.callId,
|
|
||||||
opponentMemberId: getCallUserId(call),
|
|
||||||
state: call.state,
|
|
||||||
direction: call.direction,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHangupCallState(call) {
|
|
||||||
return {
|
|
||||||
...getCallState(call),
|
|
||||||
hangupReason: call.hangupReason,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GroupCallInspector({ client, groupCall, show }) {
|
|
||||||
const [roomStateEvents, setRoomStateEvents] = useState([]);
|
|
||||||
const [toDeviceEvents, setToDeviceEvents] = useState([]);
|
|
||||||
const [state, setState] = useState({
|
|
||||||
userId: client.getUserId(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateState = useCallback(
|
|
||||||
(next) => setState((prev) => ({ ...prev, ...next })),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onUpdateRoomState(event) {
|
|
||||||
if (event) {
|
|
||||||
setRoomStateEvents((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
eventType: event.getType(),
|
|
||||||
stateKey: event.getStateKey(),
|
|
||||||
content: event.getContent(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomEvent = groupCall.room.currentState
|
|
||||||
.getStateEvents("org.matrix.msc3401.call", groupCall.groupCallId)
|
|
||||||
.getContent();
|
|
||||||
|
|
||||||
const memberEvents = Object.fromEntries(
|
|
||||||
groupCall.room.currentState
|
|
||||||
.getStateEvents("org.matrix.msc3401.call.member")
|
|
||||||
.map((event) => [event.getStateKey(), event.getContent()])
|
|
||||||
);
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
["org.matrix.msc3401.call"]: roomEvent,
|
|
||||||
["org.matrix.msc3401.call.member"]: memberEvents,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallsChanged() {
|
|
||||||
const calls = groupCall.calls.reduce((obj, call) => {
|
|
||||||
obj[
|
|
||||||
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
|
||||||
] = getCallState(call);
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
updateState({ calls });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallHangup(call) {
|
|
||||||
setState(({ hangupCalls, ...rest }) => ({
|
|
||||||
...rest,
|
|
||||||
hangupCalls: {
|
|
||||||
...hangupCalls,
|
|
||||||
[`${call.callId} (${
|
|
||||||
call.getOpponentMember()?.userId || call.sender
|
|
||||||
})`]: getHangupCallState(call),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onToDeviceEvent(event) {
|
|
||||||
const eventType = event.getType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
eventType.startsWith("m.call.") ||
|
|
||||||
eventType.startsWith("org.matrix.call.")
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = event.getContent();
|
|
||||||
|
|
||||||
if (content.conf_id && content.conf_id !== groupCall.groupCallId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setToDeviceEvents((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ eventType, content, sender: event.getSender() },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on("RoomState.events", onUpdateRoomState);
|
|
||||||
groupCall.on("calls_changed", onCallsChanged);
|
|
||||||
client.on("state", onCallsChanged);
|
|
||||||
client.on("hangup", onCallHangup);
|
|
||||||
client.on("toDeviceEvent", onToDeviceEvent);
|
|
||||||
|
|
||||||
onUpdateRoomState();
|
|
||||||
}, [client, groupCall]);
|
|
||||||
|
|
||||||
const toDeviceEventsByCall = useMemo(() => {
|
|
||||||
const result = {};
|
|
||||||
|
|
||||||
for (const event of toDeviceEvents) {
|
|
||||||
const callId = event.content.call_id;
|
|
||||||
const key = `${callId} (${event.sender})`;
|
|
||||||
result[key] = result[key] || [];
|
|
||||||
result[key].push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, [toDeviceEvents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout;
|
|
||||||
|
|
||||||
async function updateCallStats() {
|
|
||||||
const callIds = groupCall.calls.map(
|
|
||||||
(call) =>
|
|
||||||
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
|
||||||
);
|
|
||||||
const stats = await Promise.all(
|
|
||||||
groupCall.calls.map((call) =>
|
|
||||||
call.peerConn
|
|
||||||
? call.peerConn
|
|
||||||
.getStats(null)
|
|
||||||
.then((stats) =>
|
|
||||||
Object.fromEntries(
|
|
||||||
Array.from(stats).map(([_id, report], i) => [
|
|
||||||
report.type + i,
|
|
||||||
report,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: Promise.resolve(null)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const callStats = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < groupCall.calls.length; i++) {
|
|
||||||
callStats[callIds[i]] = stats[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({ callStats });
|
|
||||||
timeout = setTimeout(updateCallStats, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
updateCallStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Resizable enable={{ top: true }} defaultSize={{ height: 200 }}>
|
|
||||||
<ReactJson
|
|
||||||
theme="monokai"
|
|
||||||
src={{
|
|
||||||
...state,
|
|
||||||
roomStateEvents,
|
|
||||||
toDeviceEvents,
|
|
||||||
toDeviceEventsByCall,
|
|
||||||
}}
|
|
||||||
name={null}
|
|
||||||
indentWidth={2}
|
|
||||||
collapsed={1}
|
|
||||||
displayDataTypes={false}
|
|
||||||
displayObjectSize={false}
|
|
||||||
enableClipboard={false}
|
|
||||||
style={{ height: "100%", overflowY: "scroll" }}
|
|
||||||
/>
|
|
||||||
</Resizable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +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";
|
|
||||||
|
|
||||||
export function Header({ children, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<header className={classNames(styles.header, className)} {...rest}>
|
|
||||||
{children}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LeftNav({ children, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.nav, styles.leftNav, className)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RightNav({ children, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.nav, styles.rightNav, className)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeaderLogo() {
|
|
||||||
return (
|
|
||||||
<Link className={styles.logo} to="/">
|
|
||||||
<Logo />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RoomHeaderInfo({ roomName }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.roomAvatar}>
|
|
||||||
<VideoIcon width={16} height={16} />
|
|
||||||
</div>
|
|
||||||
<h3>{roomName}</h3>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -16,16 +16,24 @@
|
|||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.headerLogo {
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leftNav.hideMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.leftNav > * {
|
.leftNav > * {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leftNav h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.rightNav {
|
.rightNav {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
@@ -34,13 +42,17 @@
|
|||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rightNav.hideMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nav > :last-child {
|
.nav > :last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomAvatar {
|
.roomAvatar {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
@@ -58,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;
|
||||||
}
|
}
|
||||||
@@ -92,8 +104,37 @@
|
|||||||
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,
|
||||||
|
.roomAvatar,
|
||||||
|
.leftNav.hideMobile,
|
||||||
|
.rightNav.hideMobile {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftNav h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
height: 98px;
|
height: 76px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
106
src/Header.stories.jsx
Normal file
106
src/Header.stories.jsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { GridLayoutMenu } from "./room/GridLayoutMenu";
|
||||||
|
import {
|
||||||
|
Header,
|
||||||
|
HeaderLogo,
|
||||||
|
LeftNav,
|
||||||
|
RightNav,
|
||||||
|
RoomHeaderInfo,
|
||||||
|
} from "./Header";
|
||||||
|
import { UserMenu } from "./UserMenu";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Header",
|
||||||
|
component: Header,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HomeAnonymous = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<HeaderLogo />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<UserMenu />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const HomeNamedGuest = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<HeaderLogo />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const HomeLoggedIn = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<HeaderLogo />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<UserMenu isAuthenticated displayName="Yara" />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LobbyNamedGuest = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LobbyLoggedIn = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<UserMenu isAuthenticated displayName="Yara" />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InRoomNamedGuest = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<GridLayoutMenu layout="freedom" />
|
||||||
|
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InRoomLoggedIn = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav>
|
||||||
|
<GridLayoutMenu layout="freedom" />
|
||||||
|
<UserMenu isAuthenticated displayName="Yara" />
|
||||||
|
</RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CreateAccount = () => (
|
||||||
|
<Header>
|
||||||
|
<LeftNav>
|
||||||
|
<HeaderLogo />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav></RightNav>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
352
src/Home.jsx
352
src/Home.jsx
@@ -1,352 +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, useState } from "react";
|
|
||||||
import { useHistory, Link } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
useClient,
|
|
||||||
useGroupCallRooms,
|
|
||||||
usePublicRooms,
|
|
||||||
createRoom,
|
|
||||||
roomAliasFromRoomName,
|
|
||||||
} from "./ConferenceCallManagerHooks";
|
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
|
||||||
import styles from "./Home.module.css";
|
|
||||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
|
||||||
import { UserMenu } from "./UserMenu";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { CallList } from "./CallList";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { ErrorView, LoadingView } from "./FullScreenView";
|
|
||||||
import { useModalTriggerState } from "./Modal";
|
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
|
||||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
|
||||||
|
|
||||||
export function Home() {
|
|
||||||
const {
|
|
||||||
isAuthenticated,
|
|
||||||
isGuest,
|
|
||||||
isPasswordlessUser,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
client,
|
|
||||||
register,
|
|
||||||
} = useClient();
|
|
||||||
|
|
||||||
const history = useHistory();
|
|
||||||
const [creatingRoom, setCreatingRoom] = useState(false);
|
|
||||||
const [createRoomError, setCreateRoomError] = useState();
|
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
|
||||||
const [existingRoomId, setExistingRoomId] = useState();
|
|
||||||
|
|
||||||
const onCreateRoom = useCallback(
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target);
|
|
||||||
const roomName = data.get("roomName");
|
|
||||||
const userName = data.get("userName");
|
|
||||||
|
|
||||||
async function onCreateRoom() {
|
|
||||||
let _client = client;
|
|
||||||
|
|
||||||
if (!_client || isGuest) {
|
|
||||||
_client = await register(userName, randomString(16), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomIdOrAlias = await createRoom(_client, roomName);
|
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreateRoomError(undefined);
|
|
||||||
setCreatingRoom(true);
|
|
||||||
|
|
||||||
return onCreateRoom().catch((error) => {
|
|
||||||
if (error.errcode === "M_ROOM_IN_USE") {
|
|
||||||
setExistingRoomId(roomAliasFromRoomName(roomName));
|
|
||||||
setCreateRoomError(undefined);
|
|
||||||
modalState.open();
|
|
||||||
} else {
|
|
||||||
setCreateRoomError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreatingRoom(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[client, history, register, isGuest]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onJoinRoom = useCallback(
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target);
|
|
||||||
const roomId = data.get("roomId");
|
|
||||||
history.push(`/${roomId}`);
|
|
||||||
},
|
|
||||||
[history]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onJoinExistingRoom = useCallback(() => {
|
|
||||||
history.push(`/${existingRoomId}`);
|
|
||||||
}, [history, existingRoomId]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <LoadingView />;
|
|
||||||
} else if (error) {
|
|
||||||
return <ErrorView error={error} />;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!isAuthenticated || isGuest ? (
|
|
||||||
<UnregisteredView
|
|
||||||
onCreateRoom={onCreateRoom}
|
|
||||||
createRoomError={createRoomError}
|
|
||||||
creatingRoom={creatingRoom}
|
|
||||||
onJoinRoom={onJoinRoom}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<RegisteredView
|
|
||||||
client={client}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
isGuest={isGuest}
|
|
||||||
onCreateRoom={onCreateRoom}
|
|
||||||
createRoomError={createRoomError}
|
|
||||||
creatingRoom={creatingRoom}
|
|
||||||
onJoinRoom={onJoinRoom}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{modalState.isOpen && (
|
|
||||||
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function UnregisteredView({
|
|
||||||
onCreateRoom,
|
|
||||||
createRoomError,
|
|
||||||
creatingRoom,
|
|
||||||
onJoinRoom,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.home, styles.fullWidth)}>
|
|
||||||
<Header className={styles.header}>
|
|
||||||
<LeftNav>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
<div className={styles.splitContainer}>
|
|
||||||
<div className={styles.left}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.centered}>
|
|
||||||
<form onSubmit={onJoinRoom}>
|
|
||||||
<h1>Join a call</h1>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<InputField
|
|
||||||
id="roomId"
|
|
||||||
name="roomId"
|
|
||||||
label="Call ID"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Call ID"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<Button className={styles.button} type="submit">
|
|
||||||
Join call
|
|
||||||
</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</form>
|
|
||||||
<hr />
|
|
||||||
<form onSubmit={onCreateRoom}>
|
|
||||||
<h1>Create a call</h1>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<InputField
|
|
||||||
id="userName"
|
|
||||||
name="userName"
|
|
||||||
label="Username"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Username"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<InputField
|
|
||||||
id="roomName"
|
|
||||||
name="roomName"
|
|
||||||
label="Room Name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Room Name"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{createRoomError && (
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<Button
|
|
||||||
className={styles.button}
|
|
||||||
type="submit"
|
|
||||||
disabled={creatingRoom}
|
|
||||||
>
|
|
||||||
{creatingRoom ? "Creating call..." : "Create call"}
|
|
||||||
</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className={styles.authLinks}>
|
|
||||||
<p>
|
|
||||||
Not registered yet?{" "}
|
|
||||||
<Link to="/register">Create an account</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RegisteredView({
|
|
||||||
client,
|
|
||||||
isPasswordlessUser,
|
|
||||||
isGuest,
|
|
||||||
onCreateRoom,
|
|
||||||
createRoomError,
|
|
||||||
creatingRoom,
|
|
||||||
onJoinRoom,
|
|
||||||
}) {
|
|
||||||
const publicRooms = usePublicRooms(
|
|
||||||
client,
|
|
||||||
import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
|
|
||||||
);
|
|
||||||
const recentRooms = useGroupCallRooms(client);
|
|
||||||
|
|
||||||
const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.home, {
|
|
||||||
[styles.fullWidth]: hideCallList,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Header className={styles.header}>
|
|
||||||
<LeftNav className={styles.leftNav}>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
<div className={styles.splitContainer}>
|
|
||||||
<div className={styles.left}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.centered}>
|
|
||||||
<form onSubmit={onJoinRoom}>
|
|
||||||
<h1>Join a call</h1>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<InputField
|
|
||||||
id="roomId"
|
|
||||||
name="roomId"
|
|
||||||
label="Call ID"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Call ID"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<Button className={styles.button} type="submit">
|
|
||||||
Join call
|
|
||||||
</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</form>
|
|
||||||
<hr />
|
|
||||||
<form onSubmit={onCreateRoom}>
|
|
||||||
<h1>Create a call</h1>
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<InputField
|
|
||||||
id="roomName"
|
|
||||||
name="roomName"
|
|
||||||
label="Room Name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Room Name"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{createRoomError && (
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
<FieldRow className={styles.fieldRow}>
|
|
||||||
<Button
|
|
||||||
className={styles.button}
|
|
||||||
type="submit"
|
|
||||||
disabled={creatingRoom}
|
|
||||||
>
|
|
||||||
{creatingRoom ? "Creating call..." : "Create call"}
|
|
||||||
</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</form>
|
|
||||||
{(isPasswordlessUser || isGuest) && (
|
|
||||||
<div className={styles.authLinks}>
|
|
||||||
<p>
|
|
||||||
Not registered yet?{" "}
|
|
||||||
<Link to="/register">Create an account</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!hideCallList && (
|
|
||||||
<div className={styles.right}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
{publicRooms.length > 0 && (
|
|
||||||
<CallList
|
|
||||||
title="Public Calls"
|
|
||||||
rooms={publicRooms}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{recentRooms.length > 0 && (
|
|
||||||
<CallList
|
|
||||||
title="Recent Calls"
|
|
||||||
rooms={recentRooms}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
.home {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.splitContainer {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left,
|
|
||||||
.right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullWidth {
|
|
||||||
background-color: var(--bgColor1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullWidth .header {
|
|
||||||
background-color: var(--bgColor1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 512px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content form > * {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content form > :last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content hr:after {
|
|
||||||
background-color: var(--bgColor1);
|
|
||||||
content: "OR";
|
|
||||||
padding: 0 12px;
|
|
||||||
position: relative;
|
|
||||||
top: -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 92px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldRow {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
height: 40px;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content form:first-child {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .content form:last-child {
|
|
||||||
padding-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right .content {
|
|
||||||
padding: 0 40px 40px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right .content h3:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authLinks {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authLinks {
|
|
||||||
margin-bottom: 100px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authLinks a {
|
|
||||||
color: #0dbd8b;
|
|
||||||
font-weight: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.left {
|
|
||||||
background-color: var(--bgColor2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home:not(.fullWidth) .left {
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home:not(.fullWidth) .leftNav {
|
|
||||||
background-color: var(--bgColor2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.splitContainer {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullWidth .content hr:after,
|
|
||||||
.left .content hr:after,
|
|
||||||
.fullWidth .header {
|
|
||||||
background-color: var(--bgColor2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React, { forwardRef } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./Input.module.css";
|
|
||||||
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
|
|
||||||
|
|
||||||
export function FieldRow({ children, rightAlign, className, ...rest }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.fieldRow,
|
|
||||||
{ [styles.rightAlign]: rightAlign },
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Field({ children, className, ...rest }) {
|
|
||||||
return <div className={classNames(styles.field, className)}>{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InputField = forwardRef(
|
|
||||||
(
|
|
||||||
{ id, label, className, type, checked, prefix, suffix, disabled, ...rest },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
className={classNames(
|
|
||||||
type === "checkbox" ? styles.checkboxField : styles.inputField,
|
|
||||||
{
|
|
||||||
[styles.prefix]: !!prefix,
|
|
||||||
[styles.disabled]: disabled,
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{prefix && <span>{prefix}</span>}
|
|
||||||
<input
|
|
||||||
id={id}
|
|
||||||
{...rest}
|
|
||||||
ref={ref}
|
|
||||||
type={type}
|
|
||||||
checked={checked}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<label htmlFor={id}>
|
|
||||||
{type === "checkbox" && (
|
|
||||||
<div className={styles.checkbox}>
|
|
||||||
<CheckIcon />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
{suffix && <span>{suffix}</span>}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function ErrorMessage({ children }) {
|
|
||||||
return <p className={styles.errorMessage}>{children}</p>;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Modal, ModalContent } from "./Modal";
|
|
||||||
import { CopyButton } from "./button";
|
|
||||||
import { getRoomUrl } from "./ConferenceCallManagerHooks";
|
|
||||||
import styles from "./InviteModal.module.css";
|
|
||||||
|
|
||||||
export function InviteModal({ roomId, ...rest }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title="Invite People"
|
|
||||||
isDismissable
|
|
||||||
className={styles.inviteModal}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<ModalContent>
|
|
||||||
<p>Copy and share this meeting link</p>
|
|
||||||
<CopyButton className={styles.copyButton} value={getRoomUrl(roomId)} />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Modal, ModalContent } from "./Modal";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { FieldRow } from "./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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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;
|
||||||
@@ -23,15 +23,11 @@
|
|||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option.selected {
|
|
||||||
color: #0dbd8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option.focused {
|
.option.focused {
|
||||||
background-color: rgba(111, 120, 130, 0.2);
|
background-color: rgba(111, 120, 130, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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,12 +11,12 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem > * {
|
.menuItem > * {
|
||||||
margin-right: 10px;
|
margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem > :last-child {
|
.menuItem > :last-child {
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 799px) {
|
@media (max-width: 799px) {
|
||||||
|
.modalHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px 24px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal.mobileFullScreen {
|
.modal.mobileFullScreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 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,77 +0,0 @@
|
|||||||
import React, { useCallback } from "react";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { Menu } from "./Menu";
|
|
||||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
|
||||||
import { Item } from "@react-stately/collections";
|
|
||||||
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
|
|
||||||
import { ReactComponent as AddUserIcon } from "./icons/AddUser.svg";
|
|
||||||
import { ReactComponent as OverflowIcon } from "./icons/Overflow.svg";
|
|
||||||
import { useModalTriggerState } from "./Modal";
|
|
||||||
import { SettingsModal } from "./SettingsModal";
|
|
||||||
import { InviteModal } from "./InviteModal";
|
|
||||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
|
||||||
|
|
||||||
export function OverflowMenu({
|
|
||||||
roomId,
|
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
client,
|
|
||||||
}) {
|
|
||||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
|
||||||
useModalTriggerState();
|
|
||||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
|
||||||
useModalTriggerState();
|
|
||||||
|
|
||||||
// TODO: On closing modal, focus should be restored to the trigger button
|
|
||||||
// https://github.com/adobe/react-spectrum/issues/2444
|
|
||||||
const onAction = useCallback((key) => {
|
|
||||||
switch (key) {
|
|
||||||
case "invite":
|
|
||||||
inviteModalState.open();
|
|
||||||
break;
|
|
||||||
case "settings":
|
|
||||||
settingsModalState.open();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PopoverMenuTrigger disableOnState>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button variant="toolbar">
|
|
||||||
<OverflowIcon />
|
|
||||||
</Button>
|
|
||||||
{(props) => (
|
|
||||||
<Tooltip position="top" {...props}>
|
|
||||||
More
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
|
||||||
{(props) => (
|
|
||||||
<Menu {...props} label="More menu" onAction={onAction}>
|
|
||||||
<Item key="invite" textValue="Invite people">
|
|
||||||
<AddUserIcon />
|
|
||||||
<span>Invite people</span>
|
|
||||||
</Item>
|
|
||||||
<Item key="settings" textValue="Settings">
|
|
||||||
<SettingsIcon />
|
|
||||||
<span>Settings</span>
|
|
||||||
</Item>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</PopoverMenuTrigger>
|
|
||||||
{settingsModalState.isOpen && (
|
|
||||||
<SettingsModal
|
|
||||||
{...settingsModalProps}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{inviteModalState.isOpen && (
|
|
||||||
<InviteModal roomId={roomId} {...inviteModalProps} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import React, { forwardRef, useRef } from "react";
|
|
||||||
import { DismissButton, useOverlay } from "@react-aria/overlays";
|
|
||||||
import { FocusScope } from "@react-aria/focus";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./Popover.module.css";
|
|
||||||
|
|
||||||
export const Popover = forwardRef(
|
|
||||||
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
|
|
||||||
const fallbackRef = useRef();
|
|
||||||
const popoverRef = ref || fallbackRef;
|
|
||||||
|
|
||||||
const { overlayProps } = useOverlay(
|
|
||||||
{
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
shouldCloseOnBlur: true,
|
|
||||||
isDismissable: true,
|
|
||||||
},
|
|
||||||
popoverRef
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusScope restoreFocus>
|
|
||||||
<div
|
|
||||||
{...overlayProps}
|
|
||||||
{...rest}
|
|
||||||
className={classNames(styles.popover, className)}
|
|
||||||
ref={popoverRef}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<DismissButton onDismiss={onClose} />
|
|
||||||
</div>
|
|
||||||
</FocusScope>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { useProfile } from "./ConferenceCallManagerHooks";
|
|
||||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
|
||||||
import { Modal, ModalContent } from "./Modal";
|
|
||||||
|
|
||||||
export function ProfileModal({
|
|
||||||
client,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
isGuest,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const { onClose } = rest;
|
|
||||||
const {
|
|
||||||
success,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
displayName: initialDisplayName,
|
|
||||||
saveProfile,
|
|
||||||
} = useProfile(client);
|
|
||||||
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
|
||||||
|
|
||||||
const onChangeDisplayName = useCallback(
|
|
||||||
(e) => {
|
|
||||||
setDisplayName(e.target.value);
|
|
||||||
},
|
|
||||||
[setDisplayName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target);
|
|
||||||
const displayName = data.get("displayName");
|
|
||||||
const avatar = data.get("avatar");
|
|
||||||
|
|
||||||
saveProfile({
|
|
||||||
displayName,
|
|
||||||
avatar,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[saveProfile]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (success) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}, [success, onClose]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal title="Profile" isDismissable {...rest}>
|
|
||||||
<ModalContent>
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id="displayName"
|
|
||||||
name="displayName"
|
|
||||||
label="Display Name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Display Name"
|
|
||||||
value={displayName}
|
|
||||||
onChange={onChangeDisplayName}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{isAuthenticated && !isGuest && !isPasswordlessUser && (
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="file"
|
|
||||||
id="avatar"
|
|
||||||
name="avatar"
|
|
||||||
label="Avatar"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<FieldRow>
|
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
<FieldRow rightAlign>
|
|
||||||
<Button type="button" variant="secondary" onPress={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={loading}>
|
|
||||||
{loading ? "Saving..." : "Save"}
|
|
||||||
</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,183 +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, useRef, useState } from "react";
|
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
|
||||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { useClient, defaultHomeserverHost } from "./ConferenceCallManagerHooks";
|
|
||||||
import styles from "./LoginPage.module.css";
|
|
||||||
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
|
|
||||||
import { LoadingView } from "./FullScreenView";
|
|
||||||
|
|
||||||
export function RegisterPage() {
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
client,
|
|
||||||
register,
|
|
||||||
changePassword,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
} = useClient();
|
|
||||||
const confirmPasswordRef = useRef();
|
|
||||||
const history = useHistory();
|
|
||||||
const location = useLocation();
|
|
||||||
const [registering, setRegistering] = useState(false);
|
|
||||||
const [error, setError] = useState();
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
|
||||||
|
|
||||||
const onSubmitRegisterForm = useCallback(
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target);
|
|
||||||
const userName = data.get("userName");
|
|
||||||
const password = data.get("password");
|
|
||||||
const passwordConfirmation = data.get("passwordConfirmation");
|
|
||||||
|
|
||||||
if (password !== passwordConfirmation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRegistering(true);
|
|
||||||
|
|
||||||
if (isPasswordlessUser) {
|
|
||||||
changePassword(password)
|
|
||||||
.then(() => {
|
|
||||||
if (location.state && location.state.from) {
|
|
||||||
history.push(location.state.from);
|
|
||||||
} else {
|
|
||||||
history.push("/");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error);
|
|
||||||
setRegistering(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
register(userName, password)
|
|
||||||
.then(() => {
|
|
||||||
if (location.state && location.state.from) {
|
|
||||||
history.push(location.state.from);
|
|
||||||
} else {
|
|
||||||
history.push("/");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error);
|
|
||||||
setRegistering(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[register, changePassword, location, history, isPasswordlessUser]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!confirmPasswordRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
|
||||||
confirmPasswordRef.current.setCustomValidity("Passwords must match");
|
|
||||||
} else {
|
|
||||||
confirmPasswordRef.current.setCustomValidity("");
|
|
||||||
}
|
|
||||||
}, [password, passwordConfirmation]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loading && isAuthenticated && !isPasswordlessUser) {
|
|
||||||
history.push("/");
|
|
||||||
}
|
|
||||||
}, [history, isAuthenticated, isPasswordlessUser]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <LoadingView />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.formContainer}>
|
|
||||||
<Logo width="auto" height="auto" className={styles.logo} />
|
|
||||||
<h2>Create your account</h2>
|
|
||||||
<form onSubmit={onSubmitRegisterForm}>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="text"
|
|
||||||
name="userName"
|
|
||||||
placeholder="Username"
|
|
||||||
label="Username"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="none"
|
|
||||||
prefix="@"
|
|
||||||
suffix={`:${defaultHomeserverHost}`}
|
|
||||||
value={
|
|
||||||
isAuthenticated && isPasswordlessUser
|
|
||||||
? client.getUserIdLocalpart()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={isAuthenticated && isPasswordlessUser}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
required
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
value={password}
|
|
||||||
placeholder="Password"
|
|
||||||
label="Password"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
name="passwordConfirmation"
|
|
||||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
|
||||||
value={passwordConfirmation}
|
|
||||||
placeholder="Confirm Password"
|
|
||||||
label="Confirm Password"
|
|
||||||
ref={confirmPasswordRef}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{error && (
|
|
||||||
<FieldRow>
|
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
<FieldRow>
|
|
||||||
<Button type="submit" disabled={registering}>
|
|
||||||
{registering ? "Registering..." : "Register"}
|
|
||||||
</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div className={styles.authLinks}>
|
|
||||||
<p>Already have an account?</p>
|
|
||||||
<p>
|
|
||||||
<Link to="/login">Log in</Link>
|
|
||||||
{" Or "}
|
|
||||||
<Link to="/">Access as a guest</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
574
src/Room.jsx
574
src/Room.jsx
@@ -1,574 +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, useMemo, useState } from "react";
|
|
||||||
import styles from "./Room.module.css";
|
|
||||||
import { useLocation, useParams, useHistory, Link } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
CopyButton,
|
|
||||||
HangupButton,
|
|
||||||
MicButton,
|
|
||||||
VideoButton,
|
|
||||||
ScreenshareButton,
|
|
||||||
LinkButton,
|
|
||||||
} from "./button";
|
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "./Header";
|
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import VideoGrid, {
|
|
||||||
useVideoGridLayout,
|
|
||||||
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
|
||||||
import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
|
|
||||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
|
||||||
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
|
||||||
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
|
|
||||||
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
|
|
||||||
import {
|
|
||||||
getAvatarUrl,
|
|
||||||
getRoomUrl,
|
|
||||||
useClient,
|
|
||||||
useLoadGroupCall,
|
|
||||||
useProfile,
|
|
||||||
} from "./ConferenceCallManagerHooks";
|
|
||||||
import { ErrorView, LoadingView, FullScreenView } from "./FullScreenView";
|
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
|
||||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
|
||||||
import { UserMenu } from "./UserMenu";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { Avatar } from "./Avatar";
|
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
|
||||||
// or with getUsermedia and getDisplaymedia being used within the same session.
|
|
||||||
// For now we can disable screensharing in Safari.
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
export function Room() {
|
|
||||||
const [registeringGuest, setRegisteringGuest] = useState(false);
|
|
||||||
const [registrationError, setRegistrationError] = useState();
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
isAuthenticated,
|
|
||||||
error,
|
|
||||||
client,
|
|
||||||
registerGuest,
|
|
||||||
isGuest,
|
|
||||||
isPasswordlessUser,
|
|
||||||
} = useClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loading && !isAuthenticated) {
|
|
||||||
setRegisteringGuest(true);
|
|
||||||
|
|
||||||
registerGuest()
|
|
||||||
.then(() => {
|
|
||||||
setRegisteringGuest(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setRegistrationError(error);
|
|
||||||
setRegisteringGuest(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [loading, isAuthenticated]);
|
|
||||||
|
|
||||||
if (loading || registeringGuest) {
|
|
||||||
return <LoadingView />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registrationError || error) {
|
|
||||||
return <ErrorView error={registrationError || error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GroupCall
|
|
||||||
client={client}
|
|
||||||
isGuest={isGuest}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GroupCall({ client, isGuest, isPasswordlessUser }) {
|
|
||||||
const { roomId: maybeRoomId } = useParams();
|
|
||||||
const { hash, search } = useLocation();
|
|
||||||
const [simpleGrid, viaServers] = useMemo(() => {
|
|
||||||
const params = new URLSearchParams(search);
|
|
||||||
return [params.has("simple"), params.getAll("via")];
|
|
||||||
}, [search]);
|
|
||||||
const roomId = maybeRoomId || hash;
|
|
||||||
const { loading, error, groupCall } = useLoadGroupCall(
|
|
||||||
client,
|
|
||||||
roomId,
|
|
||||||
viaServers
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.groupCall = groupCall;
|
|
||||||
}, [groupCall]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <LoadingRoomView />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorView error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GroupCallView
|
|
||||||
isGuest={isGuest}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
client={client}
|
|
||||||
roomId={roomId}
|
|
||||||
groupCall={groupCall}
|
|
||||||
simpleGrid={simpleGrid}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GroupCallView({
|
|
||||||
client,
|
|
||||||
isGuest,
|
|
||||||
isPasswordlessUser,
|
|
||||||
roomId,
|
|
||||||
groupCall,
|
|
||||||
simpleGrid,
|
|
||||||
}) {
|
|
||||||
const [showInspector, setShowInspector] = useState(false);
|
|
||||||
const {
|
|
||||||
state,
|
|
||||||
error,
|
|
||||||
activeSpeaker,
|
|
||||||
userMediaFeeds,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
localCallFeed,
|
|
||||||
initLocalCallFeed,
|
|
||||||
enter,
|
|
||||||
leave,
|
|
||||||
toggleLocalVideoMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
toggleScreensharing,
|
|
||||||
isScreensharing,
|
|
||||||
localScreenshareFeed,
|
|
||||||
screenshareFeeds,
|
|
||||||
hasLocalParticipant,
|
|
||||||
} = useGroupCall(groupCall);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onHangup(call) {
|
|
||||||
if (call.hangupReason === "ice_failed") {
|
|
||||||
Sentry.captureException(new Error("Call hangup due to ICE failure."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(error) {
|
|
||||||
Sentry.captureException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupCall) {
|
|
||||||
groupCall.on("hangup", onHangup);
|
|
||||||
groupCall.on("error", onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (groupCall) {
|
|
||||||
groupCall.removeListener("hangup", onHangup);
|
|
||||||
groupCall.removeListener("error", onError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [groupCall]);
|
|
||||||
|
|
||||||
const [left, setLeft] = useState(false);
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const onLeave = useCallback(() => {
|
|
||||||
leave();
|
|
||||||
|
|
||||||
if (!isGuest && !isPasswordlessUser) {
|
|
||||||
history.push("/");
|
|
||||||
} else {
|
|
||||||
setLeft(true);
|
|
||||||
}
|
|
||||||
}, [leave, history, isGuest]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorView error={error} />;
|
|
||||||
} else if (state === GroupCallState.Entered) {
|
|
||||||
return (
|
|
||||||
<InRoomView
|
|
||||||
groupCall={groupCall}
|
|
||||||
client={client}
|
|
||||||
isGuest={isGuest}
|
|
||||||
roomName={groupCall.room.name}
|
|
||||||
microphoneMuted={microphoneMuted}
|
|
||||||
localVideoMuted={localVideoMuted}
|
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
|
||||||
userMediaFeeds={userMediaFeeds}
|
|
||||||
activeSpeaker={activeSpeaker}
|
|
||||||
onLeave={onLeave}
|
|
||||||
toggleScreensharing={toggleScreensharing}
|
|
||||||
isScreensharing={isScreensharing}
|
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
|
||||||
screenshareFeeds={screenshareFeeds}
|
|
||||||
simpleGrid={simpleGrid}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
roomId={roomId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (state === GroupCallState.Entering) {
|
|
||||||
return <EnteringRoomView />;
|
|
||||||
} else if (left) {
|
|
||||||
if (isPasswordlessUser) {
|
|
||||||
return <PasswordlessUserCallEndedScreen client={client} />;
|
|
||||||
} else {
|
|
||||||
return <GuestCallEndedScreen />;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<RoomSetupView
|
|
||||||
isGuest={isGuest}
|
|
||||||
client={client}
|
|
||||||
hasLocalParticipant={hasLocalParticipant}
|
|
||||||
roomName={groupCall.room.name}
|
|
||||||
state={state}
|
|
||||||
onInitLocalCallFeed={initLocalCallFeed}
|
|
||||||
localCallFeed={localCallFeed}
|
|
||||||
onEnter={enter}
|
|
||||||
microphoneMuted={microphoneMuted}
|
|
||||||
localVideoMuted={localVideoMuted}
|
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
roomId={roomId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoadingRoomView() {
|
|
||||||
return (
|
|
||||||
<FullScreenView>
|
|
||||||
<h1>Loading room...</h1>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnteringRoomView() {
|
|
||||||
return (
|
|
||||||
<FullScreenView>
|
|
||||||
<h1>Entering room...</h1>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomSetupView({
|
|
||||||
client,
|
|
||||||
roomName,
|
|
||||||
state,
|
|
||||||
onInitLocalCallFeed,
|
|
||||||
onEnter,
|
|
||||||
localCallFeed,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
toggleLocalVideoMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
roomId,
|
|
||||||
}) {
|
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
|
||||||
const videoRef = useMediaStream(stream, true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onInitLocalCallFeed();
|
|
||||||
}, [onInitLocalCallFeed]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.room}>
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
<div className={styles.joinRoom}>
|
|
||||||
<div className={styles.joinRoomContent}>
|
|
||||||
<div className={styles.preview}>
|
|
||||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
|
||||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
|
||||||
<p className={styles.webcamPermissions}>
|
|
||||||
Webcam/microphone permissions needed to join the call.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
|
||||||
<p className={styles.webcamPermissions}>
|
|
||||||
Accept webcam/microphone permissions to join the call.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
className={styles.joinCallButton}
|
|
||||||
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
|
||||||
onPress={onEnter}
|
|
||||||
>
|
|
||||||
Join call now
|
|
||||||
</Button>
|
|
||||||
<div className={styles.previewButtons}>
|
|
||||||
<MicButton
|
|
||||||
muted={microphoneMuted}
|
|
||||||
onPress={toggleMicrophoneMuted}
|
|
||||||
/>
|
|
||||||
<VideoButton
|
|
||||||
muted={localVideoMuted}
|
|
||||||
onPress={toggleLocalVideoMuted}
|
|
||||||
/>
|
|
||||||
<OverflowMenu
|
|
||||||
roomId={roomId}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p>Or</p>
|
|
||||||
<CopyButton
|
|
||||||
value={getRoomUrl(roomId)}
|
|
||||||
className={styles.copyButton}
|
|
||||||
copiedMessage="Call link copied"
|
|
||||||
>
|
|
||||||
Copy call link and join later
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
<div className={styles.joinRoomFooter}>
|
|
||||||
<Link className={styles.homeLink} to="/">
|
|
||||||
Take me Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InRoomView({
|
|
||||||
client,
|
|
||||||
isGuest,
|
|
||||||
groupCall,
|
|
||||||
roomName,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
toggleLocalVideoMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
userMediaFeeds,
|
|
||||||
activeSpeaker,
|
|
||||||
onLeave,
|
|
||||||
toggleScreensharing,
|
|
||||||
isScreensharing,
|
|
||||||
screenshareFeeds,
|
|
||||||
simpleGrid,
|
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
roomId,
|
|
||||||
}) {
|
|
||||||
const [layout, setLayout] = useVideoGridLayout();
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
|
||||||
const participants = [];
|
|
||||||
|
|
||||||
for (const callFeed of userMediaFeeds) {
|
|
||||||
participants.push({
|
|
||||||
id: callFeed.stream.id,
|
|
||||||
usermediaCallFeed: callFeed,
|
|
||||||
isActiveSpeaker:
|
|
||||||
screenshareFeeds.length === 0
|
|
||||||
? callFeed.userId === activeSpeaker
|
|
||||||
: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const callFeed of screenshareFeeds) {
|
|
||||||
const participant = participants.find(
|
|
||||||
(p) => p.usermediaCallFeed.userId === callFeed.userId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (participant) {
|
|
||||||
participant.screenshareCallFeed = callFeed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return participants;
|
|
||||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
|
||||||
|
|
||||||
const onFocusTile = useCallback(
|
|
||||||
(tiles, focusedTile) => {
|
|
||||||
if (layout === "freedom") {
|
|
||||||
return tiles.map((tile) => {
|
|
||||||
if (tile === focusedTile) {
|
|
||||||
return { ...tile, presenter: !tile.presenter };
|
|
||||||
}
|
|
||||||
|
|
||||||
return tile;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLayout("spotlight");
|
|
||||||
|
|
||||||
return tiles.map((tile) => {
|
|
||||||
if (tile === focusedTile) {
|
|
||||||
return { ...tile, presenter: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...tile, presenter: false };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[layout, setLayout]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderAvatar = useCallback(
|
|
||||||
(roomMember, width, height) => {
|
|
||||||
const avatarUrl = roomMember.user?.avatarUrl;
|
|
||||||
const size = Math.round(Math.min(width, height) / 2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
key={roomMember.userId}
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
borderRadius: size,
|
|
||||||
fontSize: Math.round(size / 2),
|
|
||||||
}}
|
|
||||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
|
||||||
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
|
||||||
className={styles.avatar}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.room, styles.inRoom)}>
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
|
||||||
{!isGuest && <UserMenu disableLogout />}
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
{items.length === 0 ? (
|
|
||||||
<div className={styles.centerMessage}>
|
|
||||||
<p>Waiting for other participants...</p>
|
|
||||||
</div>
|
|
||||||
) : simpleGrid ? (
|
|
||||||
<SimpleVideoGrid items={items} />
|
|
||||||
) : (
|
|
||||||
<VideoGrid
|
|
||||||
items={items}
|
|
||||||
layout={layout}
|
|
||||||
getAvatar={renderAvatar}
|
|
||||||
onFocusTile={onFocusTile}
|
|
||||||
disableAnimations={isSafari}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={styles.footer}>
|
|
||||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
|
||||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
|
||||||
{canScreenshare && !isSafari && (
|
|
||||||
<ScreenshareButton
|
|
||||||
enabled={isScreensharing}
|
|
||||||
onPress={toggleScreensharing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<OverflowMenu
|
|
||||||
roomId={roomId}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
<HangupButton onPress={onLeave} />
|
|
||||||
</div>
|
|
||||||
<GroupCallInspector
|
|
||||||
client={client}
|
|
||||||
groupCall={groupCall}
|
|
||||||
show={showInspector}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GuestCallEndedScreen() {
|
|
||||||
return (
|
|
||||||
<FullScreenView className={styles.callEndedScreen}>
|
|
||||||
<h1>Your call is now ended</h1>
|
|
||||||
<div className={styles.callEndedContent}>
|
|
||||||
<p>Why not finish by creating an account?</p>
|
|
||||||
<p>You'll be able to:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Easily access all your previous call links</li>
|
|
||||||
<li>Set a username and avatar</li>
|
|
||||||
</ul>
|
|
||||||
<LinkButton
|
|
||||||
className={styles.callEndedButton}
|
|
||||||
size="lg"
|
|
||||||
variant="default"
|
|
||||||
to="/register"
|
|
||||||
>
|
|
||||||
Create account
|
|
||||||
</LinkButton>
|
|
||||||
</div>
|
|
||||||
<Link to="/">Not now, return to home screen</Link>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasswordlessUserCallEndedScreen({ client }) {
|
|
||||||
const { displayName } = useProfile(client);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullScreenView className={styles.callEndedScreen}>
|
|
||||||
<h1>{displayName}, your call is now ended</h1>
|
|
||||||
<div className={styles.callEndedContent}>
|
|
||||||
<p>Why not finish by setting up a password to keep your account?</p>
|
|
||||||
<p>
|
|
||||||
You'll be able to keep your name and set an avatar for use on future
|
|
||||||
calls
|
|
||||||
</p>
|
|
||||||
<LinkButton
|
|
||||||
className={styles.callEndedButton}
|
|
||||||
size="lg"
|
|
||||||
variant="default"
|
|
||||||
to="/register"
|
|
||||||
>
|
|
||||||
Create account
|
|
||||||
</LinkButton>
|
|
||||||
</div>
|
|
||||||
<Link to="/">Not now, return to home screen</Link>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,217 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.room {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inRoom {
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinRoom {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinRoomContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinRoomContent h1 {
|
|
||||||
display: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinRoomFooter {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.homeLink {
|
|
||||||
margin-top: 50px;
|
|
||||||
color: #0dbd8b;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
position: relative;
|
|
||||||
min-height: 280px;
|
|
||||||
height: 50vh;
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--bgColor3);
|
|
||||||
margin: 40px 20px 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
background-color: black;
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.webcamPermissions {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 66px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(23, 25, 28, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinCallButton {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 222px;
|
|
||||||
height: 40px;
|
|
||||||
bottom: 86px;
|
|
||||||
left: 50%;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 15px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton {
|
|
||||||
width: 320px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons > * {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons > :last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centerMessage {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centerMessage p {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomContainer {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer > * {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer > :last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callEndedScreen h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callEndedScreen h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callEndedScreen p {
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callEndedScreen ul {
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
text-align: initial;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callEndedButton {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callEndedContent {
|
|
||||||
text-align: center;
|
|
||||||
max-width: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.roomContainer {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
height: 118px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinRoomContent h1 {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,95 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Modal } from "./Modal";
|
|
||||||
import styles from "./SettingsModal.module.css";
|
|
||||||
import { TabContainer, TabItem } from "./Tabs";
|
|
||||||
import { ReactComponent as AudioIcon } from "./icons/Audio.svg";
|
|
||||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
|
||||||
import { ReactComponent as DeveloperIcon } from "./icons/Developer.svg";
|
|
||||||
import { SelectInput } from "./SelectInput";
|
|
||||||
import { Item } from "@react-stately/collections";
|
|
||||||
import { useMediaHandler } from "./useMediaHandler";
|
|
||||||
import { FieldRow, InputField } from "./Input";
|
|
||||||
|
|
||||||
export function SettingsModal({
|
|
||||||
client,
|
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
audioInput,
|
|
||||||
audioInputs,
|
|
||||||
setAudioInput,
|
|
||||||
videoInput,
|
|
||||||
videoInputs,
|
|
||||||
setVideoInput,
|
|
||||||
} = useMediaHandler(client);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title="Settings"
|
|
||||||
isDismissable
|
|
||||||
mobileFullScreen
|
|
||||||
className={styles.settingsModal}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<TabContainer className={styles.tabContainer}>
|
|
||||||
<TabItem
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<AudioIcon width={16} height={16} />
|
|
||||||
<span>Audio</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectInput
|
|
||||||
label="Microphone"
|
|
||||||
selectedKey={audioInput}
|
|
||||||
onSelectionChange={setAudioInput}
|
|
||||||
>
|
|
||||||
{audioInputs.map(({ deviceId, label }) => (
|
|
||||||
<Item key={deviceId}>{label}</Item>
|
|
||||||
))}
|
|
||||||
</SelectInput>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<VideoIcon width={16} height={16} />
|
|
||||||
<span>Video</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectInput
|
|
||||||
label="Webcam"
|
|
||||||
selectedKey={videoInput}
|
|
||||||
onSelectionChange={setVideoInput}
|
|
||||||
>
|
|
||||||
{videoInputs.map(({ deviceId, label }) => (
|
|
||||||
<Item key={deviceId}>{label}</Item>
|
|
||||||
))}
|
|
||||||
</SelectInput>
|
|
||||||
</TabItem>
|
|
||||||
<TabItem
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<DeveloperIcon width={16} height={16} />
|
|
||||||
<span>Developer</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id="showInspector"
|
|
||||||
name="inspector"
|
|
||||||
label="Show Call Inspector"
|
|
||||||
type="checkbox"
|
|
||||||
checked={showInspector}
|
|
||||||
onChange={(e) => setShowInspector(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
</TabItem>
|
|
||||||
</TabContainer>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
53
src/Tabs.jsx
53
src/Tabs.jsx
@@ -1,53 +0,0 @@
|
|||||||
import React, { useRef } from "react";
|
|
||||||
import { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
|
|
||||||
import { Item } from "@react-stately/collections";
|
|
||||||
import { useTabListState } from "@react-stately/tabs";
|
|
||||||
import styles from "./Tabs.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export function TabContainer(props) {
|
|
||||||
const state = useTabListState(props);
|
|
||||||
const ref = useRef();
|
|
||||||
const { tabListProps } = useTabList(props, state, ref);
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.tabContainer, props.className)}>
|
|
||||||
<ul {...tabListProps} ref={ref} className={styles.tabList}>
|
|
||||||
{[...state.collection].map((item) => (
|
|
||||||
<Tab key={item.key} item={item} state={state} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<TabPanel key={state.selectedItem?.key} state={state} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tab({ item, state }) {
|
|
||||||
const { key, rendered } = item;
|
|
||||||
const ref = useRef();
|
|
||||||
const { tabProps } = useTab({ key }, state, ref);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
{...tabProps}
|
|
||||||
ref={ref}
|
|
||||||
className={classNames(styles.tab, {
|
|
||||||
[styles.selected]: state.selectedKey === key,
|
|
||||||
[styles.disabled]: state.disabledKeys.has(key),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{rendered}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabPanel({ state, ...props }) {
|
|
||||||
const ref = useRef();
|
|
||||||
const { tabPanelProps } = useTabPanel(props, state, ref);
|
|
||||||
return (
|
|
||||||
<div {...tabPanelProps} ref={ref} className={styles.tabPanel}>
|
|
||||||
{state.selectedItem?.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TabItem = Item;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import React, { forwardRef, useRef } from "react";
|
|
||||||
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
|
||||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
|
||||||
import { mergeProps } from "@react-aria/utils";
|
|
||||||
import styles from "./Tooltip.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export function Tooltip({ position, state, ...props }) {
|
|
||||||
let { tooltipProps } = useTooltip(props, state);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.tooltip, styles[position || "bottom"])}
|
|
||||||
{...mergeProps(props, tooltipProps)}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
|
||||||
const tooltipState = useTooltipTriggerState(rest);
|
|
||||||
const fallbackRef = useRef();
|
|
||||||
const triggerRef = ref || fallbackRef;
|
|
||||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
|
||||||
rest,
|
|
||||||
tooltipState,
|
|
||||||
triggerRef
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className={styles.tooltipContainer}>
|
|
||||||
<tooltipTrigger.type
|
|
||||||
{...mergeProps(triggerProps, tooltipTrigger.props, rest)}
|
|
||||||
ref={triggerRef}
|
|
||||||
/>
|
|
||||||
{tooltipState.isOpen && tooltip({ state: tooltipState, ...tooltipProps })}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
TooltipTrigger.defaultProps = {
|
|
||||||
delay: 250,
|
|
||||||
};
|
|
||||||
@@ -1,33 +1,14 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
position: absolute;
|
|
||||||
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;
|
||||||
z-index: 1;
|
font-size: 12px;
|
||||||
left: 50%;
|
font-weight: 500;
|
||||||
transform: translateX(-50%);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.top {
|
|
||||||
bottom: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottom {
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottomLeft {
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
left: -25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipContainer {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|||||||
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]);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user