Compare commits
986 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f20fc78bd7 | ||
|
|
741233909d | ||
|
|
4e0f4a8dc7 | ||
|
|
0d151452ba | ||
|
|
4fd76f9599 | ||
|
|
d123793deb | ||
|
|
449c1c9d79 | ||
|
|
de5b58792e | ||
|
|
7769074410 | ||
|
|
881054e265 | ||
|
|
767f9cdc4a | ||
|
|
946f564f84 | ||
|
|
468e389324 | ||
|
|
62e98f6c47 | ||
|
|
de31c099e3 | ||
|
|
49cae76387 | ||
|
|
d45ea78ddb | ||
|
|
dcbc3ed865 | ||
|
|
ff19135d4e | ||
|
|
de7343d16a | ||
|
|
e5135a41ba | ||
|
|
67d1c29d6a | ||
|
|
7ee6b02aeb | ||
|
|
6b021b9f70 | ||
|
|
faa8f95f97 | ||
|
|
c09fec5f88 | ||
|
|
b711a67893 | ||
|
|
a30bded3ee | ||
|
|
0349f5306c | ||
|
|
6980652cbc | ||
|
|
eff56d7ce4 | ||
|
|
1a66c455f6 | ||
|
|
7163db357c | ||
|
|
e5572210d3 | ||
|
|
11a262b58f | ||
|
|
62e4e80ca3 | ||
|
|
f55cc21289 | ||
|
|
c34a1f7f65 | ||
|
|
70b693ef3c | ||
|
|
d2175b40a4 | ||
|
|
1830acbddf | ||
|
|
df9c1fed2a | ||
|
|
05be247946 | ||
|
|
1f5c22e325 | ||
|
|
b9ee9583e4 | ||
|
|
fef503c65d | ||
|
|
dc37c83aac | ||
|
|
398e238bfb | ||
|
|
10819ed044 | ||
|
|
6edcdd3890 | ||
|
|
5e47a439ca | ||
|
|
d1091eda17 | ||
|
|
0488cbdd8c | ||
|
|
5823cd41e8 | ||
|
|
de0f2e65a5 | ||
|
|
891ff86b49 | ||
|
|
d7d7bee685 | ||
|
|
2c2aded094 | ||
|
|
f35b50d89f | ||
|
|
0880faf312 | ||
|
|
51be754ad8 | ||
|
|
70522ed8da | ||
|
|
3581aceb5a | ||
|
|
16e8ea3420 | ||
|
|
29f48f25f4 | ||
|
|
089234ae09 | ||
|
|
4dd823eca4 | ||
|
|
6bc5b16b02 | ||
|
|
f67eb328bf | ||
|
|
1c824da32b | ||
|
|
1442e57a23 | ||
|
|
39e92b9e2e | ||
|
|
c879090a34 | ||
|
|
6303b357ab | ||
|
|
b3d97810a8 | ||
|
|
eaf14a0562 | ||
|
|
fd3c0d9fc6 | ||
|
|
7765cfe1c2 | ||
|
|
b94562d43b | ||
|
|
19e478f2a9 | ||
|
|
d6b8ecdfd1 | ||
|
|
47ade7ee3c | ||
|
|
23387ee75f | ||
|
|
c85d1c1d9c | ||
|
|
96de515e56 | ||
|
|
e4c25bcc69 | ||
|
|
824ed06f36 | ||
|
|
aa57cf039a | ||
|
|
850d6a40cc | ||
|
|
282a4853cf | ||
|
|
7c26bdbda3 | ||
|
|
4ad5ea49c2 | ||
|
|
e3aa810230 | ||
|
|
db2e0758de | ||
|
|
39467f434e | ||
|
|
fddf344923 | ||
|
|
f806538e2c | ||
|
|
c6ad2003f0 | ||
|
|
9aed344a80 | ||
|
|
d8b4fea6fc | ||
|
|
0c55efe4b6 | ||
|
|
c93df1fd06 | ||
|
|
c30eb19021 | ||
|
|
2d8c33d66d | ||
|
|
223793a445 | ||
|
|
b60a92112f | ||
|
|
b017faac36 | ||
|
|
1615b6e559 | ||
|
|
d71542a757 | ||
|
|
eb8a1cea45 | ||
|
|
a44a716100 | ||
|
|
34a4403135 | ||
|
|
cfe5ce977e | ||
|
|
102ce87bb0 | ||
|
|
f9845617b3 | ||
|
|
1ec598453b | ||
|
|
4659e58be8 | ||
|
|
9f6cacb3b6 | ||
|
|
32168fb467 | ||
|
|
90ef5505bb | ||
|
|
acc41c532e | ||
|
|
fb4fe0e928 | ||
|
|
e6e18dd3f9 | ||
|
|
357a3be90d | ||
|
|
be6f21c7e1 | ||
|
|
6d8ae91b6c | ||
|
|
5bd0429e98 | ||
|
|
0a38395bdc | ||
|
|
29d8f03a43 | ||
|
|
c3a9760f22 | ||
|
|
086bacc10e | ||
|
|
1b78461ee8 | ||
|
|
0464515f3e | ||
|
|
978c408ab3 | ||
|
|
d6a800166a | ||
|
|
70620d8294 | ||
|
|
18606d46fa | ||
|
|
0e3a2afb4e | ||
|
|
4de0edc359 | ||
|
|
3a752b8f12 | ||
|
|
f81ef3e134 | ||
|
|
593e7a788a | ||
|
|
09f4bdef8e | ||
|
|
88d358f976 | ||
|
|
a8394ebc56 | ||
|
|
fde1712809 | ||
|
|
a4b6e4ff54 | ||
|
|
d7dc76a93d | ||
|
|
6340fb7c13 | ||
|
|
21ffcd503b | ||
|
|
63dc5744f3 | ||
|
|
e363fe8367 | ||
|
|
d6d919bce9 | ||
|
|
7e4c04d54e | ||
|
|
3c1d86214a | ||
|
|
37654bce7c | ||
|
|
33f983d254 | ||
|
|
6a116a4413 | ||
|
|
c9a9aa399c | ||
|
|
f8795931c1 | ||
|
|
bba60189e9 | ||
|
|
44e22e2684 | ||
|
|
aa828fe9f5 | ||
|
|
aec034182b | ||
|
|
5ba7267164 | ||
|
|
5c735f0936 | ||
|
|
9c08a69226 | ||
|
|
40a0958e0d | ||
|
|
098be75415 | ||
|
|
13def24f7e | ||
|
|
46e429c37b | ||
|
|
eab8b1d095 | ||
|
|
b92acd4822 | ||
|
|
93aafb1415 | ||
|
|
734d330a10 | ||
|
|
432f7ef93a | ||
|
|
5623fa415f | ||
|
|
f2746ab994 | ||
|
|
80f07a5454 | ||
|
|
18139f78d2 | ||
|
|
a2bbe61292 | ||
|
|
6855e61c47 | ||
|
|
d09c3d8374 | ||
|
|
67b97e63ca | ||
|
|
4cd49dee4b | ||
|
|
0aadb7e60c | ||
|
|
c67e7ebc2c | ||
|
|
b30ef953b4 | ||
|
|
c9330debd4 | ||
|
|
456194312b | ||
|
|
cb85733426 | ||
|
|
6ef41b924d | ||
|
|
24299c09b1 | ||
|
|
ab860b8655 | ||
|
|
f69e032a52 | ||
|
|
bd08166a50 | ||
|
|
d8688413f5 | ||
|
|
556f975552 | ||
|
|
12079ded4f | ||
|
|
62988e6b46 | ||
|
|
0e478f4c20 | ||
|
|
9bf1b6f928 | ||
|
|
3f341de78f | ||
|
|
6498d2bd19 | ||
|
|
15bb710394 | ||
|
|
57bf934bc2 | ||
|
|
7553983655 | ||
|
|
ca6d75e384 | ||
|
|
85b02a3589 | ||
|
|
ba6c3fac7d | ||
|
|
b5f7b52a63 | ||
|
|
88ec0d7c2f | ||
|
|
5f84cb5790 | ||
|
|
fb75c1536b | ||
|
|
715bec5949 | ||
|
|
c91dfdfe9d | ||
|
|
667f902278 | ||
|
|
e133625405 | ||
|
|
01244c1873 | ||
|
|
70344fd40f | ||
|
|
0a5701b9fa | ||
|
|
85959046a5 | ||
|
|
7444763008 | ||
|
|
d5a5ce9860 | ||
|
|
e5feba8c26 | ||
|
|
3cac74df24 | ||
|
|
51572b5787 | ||
|
|
bcbc20b53d | ||
|
|
5c8562088d | ||
|
|
58e505cd38 | ||
|
|
509fd65156 | ||
|
|
cafac39733 | ||
|
|
123763afec | ||
|
|
72503d0335 | ||
|
|
d5326ed9ee | ||
|
|
6a619de67b | ||
|
|
a47bd13db6 | ||
|
|
0cd2ad6242 | ||
|
|
78a313c373 | ||
|
|
806a9032e1 | ||
|
|
02e1d602d9 | ||
|
|
1e02afe1c1 | ||
|
|
2d5f413a1f | ||
|
|
093bf7c1a1 | ||
|
|
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 |
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
|
||||
|
||||
# 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_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
|
||||
24
.env.example
Normal file
24
.env.example
Normal file
@@ -0,0 +1,24 @@
|
||||
####
|
||||
# Build-time 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_FALLBACK_STUN_ALLOWED=false
|
||||
|
||||
# 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
|
||||
34
.eslintrc.cjs
Normal file
34
.eslintrc.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
32
.github/workflows/build.yaml
vendored
Normal file
32
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Build
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [main]
|
||||
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"
|
||||
env:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
- 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"
|
||||
88
.github/workflows/netlify-main.yaml
vendored
Normal file
88
.github/workflows/netlify-main.yaml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: Netlify Main
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
deployments: write
|
||||
# Important: the 'branches' filter above will match the 'main' branch on forks,
|
||||
# so we need to check the head repo too in order to not run on PRs from forks
|
||||
# We check the branch name again too just for completeness
|
||||
# (Is there a nicer way to see if a PR is from a fork?)
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == 'vector-im/element-call' && github.event.workflow_run.head_branch == 'main'
|
||||
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: Add config file
|
||||
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/main/config/element_io_develop.json > dist/config.json
|
||||
|
||||
- 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 }}
|
||||
81
.github/workflows/netlify-pr.yaml
vendored
Normal file
81
.github/workflows/netlify-pr.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Netlify PR Preview
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build"]
|
||||
types:
|
||||
- completed
|
||||
branches-ignore:
|
||||
- "main"
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
deployments: write
|
||||
environment: Netlify
|
||||
steps:
|
||||
- name: 📝 Create Deployment
|
||||
uses: bobheadxi/deployments@v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: Netlify
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
|
||||
- id: prdetails
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: build
|
||||
path: webapp
|
||||
|
||||
- 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 > webapp/_redirects
|
||||
|
||||
- name: Add config file
|
||||
env:
|
||||
HEADBRACH: ${{ github.event.workflow_run.head_branch }}
|
||||
run: curl -s "https://raw.githubusercontent.com/vector-im/element-call/${HEADBRACH}/config/element_io_preview.json" > webapp/config.json
|
||||
|
||||
- name: ☁️ Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2
|
||||
with:
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
enable-commit-comment: false
|
||||
alias: pr${{ steps.prdetails.outputs.pr_id }}
|
||||
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 }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
33
.github/workflows/publish.yaml
vendored
33
.github/workflows/publish.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Build & publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # required to upload release asset
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check it out
|
||||
@@ -26,16 +26,47 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
- name: Build
|
||||
run: "yarn run build"
|
||||
env:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
VITE_APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Create Tarball
|
||||
env:
|
||||
GITHUB_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
tar --numeric-owner --transform "s/dist/element-call-${GITHUB_TAG_NAME}/" -cvzf element-call-${GITHUB_TAG_NAME}.tar.gz dist
|
||||
- name: Upload
|
||||
uses: alexellis/upload-assets@0.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["element-call-${{ github.event.release.tag_name }}.tar.gz"]'
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
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 }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.env
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.idea/
|
||||
public/config.json
|
||||
|
||||
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 @@
|
||||
{}
|
||||
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
@@ -1,5 +1,22 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
Contributing code to Element
|
||||
============================
|
||||
# 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).
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,15 +1,7 @@
|
||||
FROM node:16-buster as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . /src/element-call
|
||||
RUN element-call/scripts/dockerbuild.sh
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
|
||||
COPY --from=builder /src/element-call/dist /app
|
||||
COPY scripts/default.conf /etc/nginx/conf.d/
|
||||
COPY ./dist /app
|
||||
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
USER root
|
||||
|
||||
|
||||
78
README.md
78
README.md
@@ -1,53 +1,77 @@
|
||||
# Element Call
|
||||
|
||||
Showcase for full mesh video chat powered by Matrix, implementing [MSC3401](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md).
|
||||
[](https://matrix.to/#/#webrtc:matrix.org)
|
||||
[](https://translate.element.io/engage/element-call/)
|
||||
|
||||
Discussion in [#webrtc:matrix.org: ](https://matrix.to/#/#webrtc:matrix.org)
|
||||
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).
|
||||
|
||||
## Getting Started
|
||||
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).
|
||||
|
||||
`element-call` is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902). Because of how this package is configured and Vite's requirements, you will need to clone it locally and use `yarn link` to stich things together.
|
||||
## Host it yourself
|
||||
|
||||
First clone, install, and link `matrix-js-sdk`
|
||||
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
|
||||
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.
|
||||
|
||||
You may also wish to add a configuration file (Element Call uses the domain it's hosted on as a Homeserver URL by default,
|
||||
but you can change this in the config file). This goes in `public/config.json` - you can use the sample as a starting point:
|
||||
|
||||
```
|
||||
cp config/config.sample.json public/config.json
|
||||
# edit public/config.json
|
||||
```
|
||||
|
||||
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 [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
|
||||
cd matrix-js-sdk
|
||||
git checkout robertlong/group-call
|
||||
yarn
|
||||
yarn link
|
||||
```
|
||||
|
||||
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
||||
|
||||
Finally we can set up this project.
|
||||
Next, we can set up this project:
|
||||
|
||||
```
|
||||
git clone https://github.com/vector-im/element-call.git
|
||||
cd element-call
|
||||
yarn
|
||||
yarn link matrix-js-sdk
|
||||
```
|
||||
|
||||
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 add a config file as above.
|
||||
|
||||
You're now ready to launch the development server:
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Config
|
||||
## Configuration
|
||||
|
||||
Configuration options are documented in the `.env` file.
|
||||
There are currently two different config files. `.env` holds variables that are used at build time, while `public/config.json` holds variables that are used at runtime. Documentation and default values for `public/config.json` can be found in [ConfigOptions.ts](src/config/ConfigOptions.ts).
|
||||
|
||||
## License
|
||||
## Translation
|
||||
|
||||
All files in this project are:
|
||||
|
||||
Copyright 2021-2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
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"],
|
||||
};
|
||||
8
config/config.sample.json
Normal file
8
config/config.sample.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://call.ems.host",
|
||||
"server_name": "call.ems.host"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
config/element_io_develop.json
Normal file
19
config/element_io_develop.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://call.ems.host",
|
||||
"server_name": "call.ems.host"
|
||||
}
|
||||
},
|
||||
"posthog": {
|
||||
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
|
||||
"api_host": "https://posthog-element-call.element.io"
|
||||
},
|
||||
"sentry": {
|
||||
"environment": "main-branch-cd",
|
||||
"DSN": "https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41"
|
||||
},
|
||||
"rageshake": {
|
||||
"submit_url": "https://element.io/bugreports/submit"
|
||||
}
|
||||
}
|
||||
11
config/element_io_preview.json
Normal file
11
config/element_io_preview.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://call.ems.host",
|
||||
"server_name": "call.ems.host"
|
||||
}
|
||||
},
|
||||
"rageshake": {
|
||||
"submit_url": "https://element.io/bugreports/submit"
|
||||
}
|
||||
}
|
||||
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
|
||||
22
i18next-parser.config.js
Normal file
22
i18next-parser.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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,
|
||||
};
|
||||
67
package.json
67
package.json
@@ -1,14 +1,24 @@
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
"build-storybook": "build-storybook",
|
||||
"prettier:check": "prettier -c .",
|
||||
"prettier:format": "prettier -w .",
|
||||
"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": {
|
||||
"@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.14.tgz",
|
||||
"@react-aria/button": "^3.3.4",
|
||||
"@react-aria/dialog": "^3.1.4",
|
||||
"@react-aria/focus": "^3.5.0",
|
||||
@@ -26,34 +36,79 @@
|
||||
"@react-stately/tree": "^3.2.0",
|
||||
"@sentry/react": "^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",
|
||||
"color-hash": "^2.0.1",
|
||||
"events": "^3.3.0",
|
||||
"lodash-move": "^1.1.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
||||
"i18next": "^21.10.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"i18next-http-backend": "^1.4.4",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#79575ef3760bb6085b7b770f241b3d32756d5b96",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pako": "^2.0.4",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss-preset-env": "^7",
|
||||
"posthog-js": "^1.29.0",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react": "18",
|
||||
"react-dom": "18",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use-clipboard": "^1.0.7",
|
||||
"react-use-measure": "^2.1.1",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unique-names-generator": "^4.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@sentry/vite-plugin": "^0.3.0",
|
||||
"@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.3.1",
|
||||
"prettier": "^2.6.2",
|
||||
"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-plugin-html-template": "^1.1.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<!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>
|
||||
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
129
public/locales/bg/app.json
Normal file
129
public/locales/bg/app.json
Normal file
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"<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>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 логове",
|
||||
"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}} - уоки-токи разговор"
|
||||
}
|
||||
140
public/locales/cs/app.json
Normal file
140
public/locales/cs/app.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"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",
|
||||
"{{name}} is talking…": "{{name}} mluví…",
|
||||
"{{name}} is presenting": "{{name}} prezentuje",
|
||||
"{{name}} (Connecting...)": "{{name}} (Připojení...)",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, váš hovor je nyní ukončen",
|
||||
"{{count}} people connected|other": "{{count}} lidí připojeno",
|
||||
"{{count}} people connected|one": "{{count}} lidí připojeno",
|
||||
"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.)": "Tato možnost způsobí, že zvuk účastníků hovoru se bude tvářit jako by přicházel z místa, kde jsou umístěni na obrazovce.(Experimentální možnost: může způsobit nestabilitu audia.)",
|
||||
"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>": "Tato stárnka je chráněna pomocí ReCAPTCHA a Google <2>zásad ochrany osobních údajů</2> a <6>podmínky služby</6> platí.<9></9>Kliknutím na \"Registrovat\", souhlasíte s <12>Pravidly a podmínkami</12>",
|
||||
"Walkie-talkie call name": "Jméno vysílačkového hovoru",
|
||||
"Walkie-talkie call": "Vysílačkový hovor",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Vysílačkový hovor",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Zapnout jedno-klávesové zkratky, např. 'm' pro vypnutí/zapnutí mikrofonu.",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Toto bude odesílat anonymizovaná data (jako délku a počet účastníků hovoru) týmu Element Call, aby nám pomohly zlepšovat aplikaci podle toho, jak je používaná.",
|
||||
"Talking…": "Mluvení…",
|
||||
"Talk over speaker": "Mluvit přes mluvčího",
|
||||
"Spotlight": "Soustředěný mód",
|
||||
"Single-key keyboard shortcuts": "Jedno-klávesová klávesnice",
|
||||
"Release to stop": "Pusťte pro ukončení",
|
||||
"Release spacebar key to stop": "Pusťte mezerník pro ukončení",
|
||||
"Recaptcha not loaded": "Recaptcha se nenačetla",
|
||||
"Recaptcha dismissed": "Recaptcha byla zamítnuta",
|
||||
"Press and hold to talk over {{name}}": "Zmáčkněte a držte, abyste mluvili přes {{name}}",
|
||||
"Press and hold spacebar to talk over {{name}}": "Zmáčkněte a držte mezerník, abyste mluvili přes {{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>": "Ostatní uživatelé se pokoušejí připojit k tomuto hovoru s nekompatibilních verzí. Tito uživatelé by se měli ujistit, že stránku načetli znovu:<1>{userLis}</1>",
|
||||
"Not registered yet? <2>Create an account</2>": "Nejste registrovaní? <2>Vytvořit účet</2>",
|
||||
"More menu": "Další možnosti",
|
||||
"Join existing call?": "Připojit se k existujícimu hovoru?",
|
||||
"Include debug logs": "Zahrnout ladící záznamy",
|
||||
"Home": "Domov",
|
||||
"Having trouble? Help us fix it.": "Máte problémy? Pomozte nám je spravit.",
|
||||
"Grid layout menu": "Menu rozložení",
|
||||
"Go": "Pokračovat",
|
||||
"Full screen": "Zvětšit na celou obrazovku",
|
||||
"Freedom": "Volný",
|
||||
"Fetching group call timed out.": "Vypršel časový limit načítání skupinového hovoru.",
|
||||
"Exit full screen": "Ukončit režim celé obrazovky",
|
||||
"Element Call Home": "Domov Element Call",
|
||||
"Download debug logs": "Stáhnout ladící záznamy",
|
||||
"Display name": "Zobrazované jméno",
|
||||
"Developer": "Vývojář",
|
||||
"Details": "Detaily",
|
||||
"Description (optional)": "Popis (nepovinný)",
|
||||
"Debug log request": "Žádost o protokoly ladění",
|
||||
"Debug log": "Protokoly ladění",
|
||||
"Create account": "Vytvořit účet",
|
||||
"Copy": "Kopírovat",
|
||||
"Call type menu": "Menu typu hovoru",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions</2>",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions</2>",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.",
|
||||
"Allow analytics": "Povolit analytiku",
|
||||
"Advanced": "Pokročilé",
|
||||
"<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>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?</0><1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory </1>",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Připojit se</0><1>Or</1><2>Zkopírovat odkaz a připojit se později</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Už máte účet?</0><1><0>Přihlásit se</0> Or <2>Jako host</2></1>",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Čekání na video...)"
|
||||
}
|
||||
143
public/locales/de/app.json
Normal file
143
public/locales/de/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"<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>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.": "Für die Teilnahme am Anruf sind Kamera- und Mikrofonberechtigungen 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",
|
||||
"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 …)",
|
||||
"Allow analytics": "Analysedaten senden",
|
||||
"Advanced": "Erweitert",
|
||||
"Copy": "Kopieren",
|
||||
"Element Call Home": "Element Call-Startseite",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Dies wird anonymisierte Daten (wie z. B. die Dauer eines Anrufs und die Zahl der Teilnehmenden) dem Element Call-Team senden, um uns bei der Optimierung der Anwendung basierend auf dem Nutzungsverhalten zu helfen.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Ob Tastenkürzel mit nur einer Taste aktiviert sein sollen, z. B. „m“ um das Mikrofon stumm/aktiv zu schalten.",
|
||||
"Single-key keyboard shortcuts": "Ein-Tasten-Tastenkürzel",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Warte auf Video …)",
|
||||
"This feature is only supported on Firefox.": "Diese Funktion wird nur in Firefox unterstützt.",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.</0>",
|
||||
"<0>Oops, something's gone wrong.</0>": "<0>Hoppla, etwas ist schiefgelaufen.</0>"
|
||||
}
|
||||
143
public/locales/en-GB/app.json
Normal file
143
public/locales/en-GB/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"{{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}} (Waiting for video...)": "{{name}} (Waiting for video...)",
|
||||
"{{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>": "<0>Oops, something's gone wrong.</0>",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Submitting debug logs will help us track down the problem.</0>",
|
||||
"<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.",
|
||||
"Advanced": "Advanced",
|
||||
"Allow analytics": "Allow analytics",
|
||||
"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": "Copy",
|
||||
"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",
|
||||
"Element Call Home": "Element Call Home",
|
||||
"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",
|
||||
"Single-key keyboard shortcuts": "Single-key keyboard shortcuts",
|
||||
"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 feature is only supported on Firefox.": "This feature is only supported on Firefox.",
|
||||
"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.)",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.",
|
||||
"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.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.",
|
||||
"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"
|
||||
}
|
||||
140
public/locales/es/app.json
Normal file
140
public/locales/es/app.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"<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": "Comenzar",
|
||||
"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",
|
||||
"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>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",
|
||||
"Allow analytics": "Permitir analíticas",
|
||||
"Advanced": "Avanzado",
|
||||
"Element Call Home": "Inicio de Element Call",
|
||||
"Copy": "Copiar",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Esto enviará datos anónimos (como la duración de la llamada y el número de participantes) al equipo de Element Call para ayudarnos a optimizar la aplicación dependiendo de cómo se use.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Habilita los atajos de teclado de una sola tecla, por ejemplo 'm' para silenciar/desilenciar el micrófono.",
|
||||
"Single-key keyboard shortcuts": "Atajos de teclado de una sola tecla",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)"
|
||||
}
|
||||
143
public/locales/et/app.json
Normal file
143
public/locales/et/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"Accept camera/microphone permissions to join the call.": "Kõnega liitumiseks 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 soovid salasõna seadistada ja sellega oma kasutajakonto alles jätta?</0><1>Nii saad säilitada oma nime ja määrata profiilipildi, mida saad kasutada tulevastes kõnedes</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",
|
||||
"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>",
|
||||
"Allow analytics": "Luba analüütika",
|
||||
"Advanced": "Lisaseadistused",
|
||||
"Element Call Home": "Element Call Home",
|
||||
"Copy": "Kopeeri",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Me saadame kõne anonüümsed andmed (nagu kõne kestus ja osalejate arv) meie arendustiimile ja see võimaldab levinud kasutusmustrite alusel arendust optimeerida.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Kas kasutame üheklahvilisi kiirklahve, näiteks „m“ mikrofoni sisse/välja lülitamiseks.",
|
||||
"Single-key keyboard shortcuts": "Üheklahvilised kiirklahvid",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Ootame videovoo algust...)",
|
||||
"This feature is only supported on Firefox.": "See funktsionaalsus on toetatud vaid Firefoxis.",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.</0>",
|
||||
"<0>Oops, something's gone wrong.</0>": "<0>Ohoo, midagi on nüüd katki.</0>"
|
||||
}
|
||||
140
public/locales/fa/app.json
Normal file
140
public/locales/fa/app.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"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": "خروج از حالت تمام صفحه",
|
||||
"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>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": "صدای جهتدار",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "این که میانبرهای صفحهکلید تککلیده مثل m برای خموشی و ناخموشی میکروفون به کار بیفتند یا نه.",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "دادههای ناشناس شده (از اطَلاعاتی مثل طول تماس و شمارهٔ طرفها) را به گروه تماس المنت فرستاده تا در بهینهسازی برنامه بر پایهٔ چگونگی استفادهاش یاریمان کنند.",
|
||||
"Single-key keyboard shortcuts": "میانبرهای صفحهکلید تککلیده",
|
||||
"Element Call Home": "خانهٔ تماس المنت",
|
||||
"Copy": "رونوشت",
|
||||
"Allow analytics": "نمایش تجزیهها",
|
||||
"Advanced": "پیش رفته",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>اکنون به تماس پیوسته</0><1>یا</1><2>پیوند تماس را رونوشت کرده و بعداً بپیوندید</2>",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (منتظر تصویر…)",
|
||||
"{{name}} (Connecting...)": "{{name}} (وصل شدن…)"
|
||||
}
|
||||
143
public/locales/fr/app.json
Normal file
143
public/locales/fr/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"<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>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",
|
||||
"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…)",
|
||||
"Allow analytics": "Autoriser les statistiques",
|
||||
"Advanced": "Avancé",
|
||||
"Element Call Home": "Accueil Element Call",
|
||||
"Copy": "Copier",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Cela enverra des données anonymisées (telles que la durée d’un appel et le nombre de participants) à l’équipe de Element Call pour aider à optimiser l’application en fonction de l’utilisation.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Bascule sur les raccourcis clavier à touche unique, par exemple « m » pour désactiver / activer le micro.",
|
||||
"Single-key keyboard shortcuts": "Raccourcis clavier en une touche",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (En attente de vidéo…)",
|
||||
"This feature is only supported on Firefox.": "Cette fonctionnalité est prise en charge dans Firefox uniquement.",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.</0>",
|
||||
"<0>Oops, something's gone wrong.</0>": "<0>Oups, quelque chose s’est mal passé.</0>"
|
||||
}
|
||||
143
public/locales/id/app.json
Normal file
143
public/locales/id/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"<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>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",
|
||||
"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...)",
|
||||
"Allow analytics": "Perbolehkan analitik",
|
||||
"Advanced": "Tingkat lanjut",
|
||||
"Element Call Home": "Beranda Element Call",
|
||||
"Copy": "Salin",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Ini akan mengirimkan data anonim (seperti durasi dan jumlah peserta panggilan) ke tim Element Call untuk membantu kami mengoptimalkan aplikasi berdasarkan bagaimana penggunaannya.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Apakah pintasan papan ketik seharusnya diaktifkan, mis. 'm' untuk membisukan/menyuarakan mikrofon.",
|
||||
"Single-key keyboard shortcuts": "Pintasan papan ketik satu tombol",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Menunggu video...)",
|
||||
"This feature is only supported on Firefox.": "Fitur ini hanya didukung di Firefox.",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.</0>",
|
||||
"<0>Oops, something's gone wrong.</0>": "<0>Aduh, ada yang salah.</0>"
|
||||
}
|
||||
1
public/locales/ja/app.json
Normal file
1
public/locales/ja/app.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
11
public/locales/ko/app.json
Normal file
11
public/locales/ko/app.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"<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>": "",
|
||||
"{{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}} - 워키토키 전화"
|
||||
}
|
||||
128
public/locales/pl/app.json
Normal file
128
public/locales/pl/app.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"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",
|
||||
"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>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"
|
||||
}
|
||||
137
public/locales/ru/app.json
Normal file
137
public/locales/ru/app.json
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"Register": "Зарегистрироваться",
|
||||
"Saving…": "Сохранение…",
|
||||
"Registering…": "Регистрация…",
|
||||
"Logging in…": "Вход…",
|
||||
"{{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.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.",
|
||||
"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}} подключился",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Это даст разрешение на отправку анонимизированных данных (таких, как продолжительность звонка и количество участников) команде Element Call, чтобы помочь нам оптимизировать работу приложения на основании того как оно используется.",
|
||||
"Element Call Home": "Главная Element Call",
|
||||
"Copy": "Копировать",
|
||||
"Allow analytics": "Разрешить аналитику",
|
||||
"Advanced": "Расширенные",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Присоединиться сейчас</0><1>или<1><2>Скопировать ссылку на звонок и присоединиться позже</2>",
|
||||
"{{name}} (Connecting...)": "{{name}} (Соединение...)"
|
||||
}
|
||||
143
public/locales/sk/app.json
Normal file
143
public/locales/sk/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"Spotlight": "Stredobod",
|
||||
"Local volume": "Lokálna hlasitosť",
|
||||
"Include debug logs": "Zahrnúť záznamy o ladení",
|
||||
"Fetching group call timed out.": "Vypršal čas načítania skupinového volania.",
|
||||
"Element Call Home": "Domov Element Call",
|
||||
"You can't talk at the same time": "Nemôžete hovoriť naraz",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Či chcete povoliť jednotlačidlové klávesové skratky, napr. \"m\" na stlmenie/zapnutie mikrofónu.",
|
||||
"Waiting for other participants…": "Čaká sa na ďalších účastníkov…",
|
||||
"Waiting for network": "Čakanie na sieť",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Týmto spôsobom sa budú posielať anonymizované údaje (napríklad trvanie hovoru a počet účastníkov) tímu Element Call, aby nám pomohli optimalizovať aplikáciu na základe toho, ako sa používa.",
|
||||
"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.)": "Zvuk reproduktora tak bude vyzerať, akoby vychádzal z miesta, kde je na obrazovke umiestnená jeho ikona. (Experimentálna funkcia: môže to mať vplyv na stabilitu zvuku.)",
|
||||
"Thanks! We'll get right on it.": "Vďaka! Hneď sa do toho pustíme.",
|
||||
"Talking…": "Rozprávanie…",
|
||||
"Talk over speaker": "Hovor cez reproduktor",
|
||||
"Take me Home": "Zober ma domov",
|
||||
"Submitting feedback…": "Odosielanie spätnej väzby…",
|
||||
"Submit feedback": "Odoslať spätnú väzbu",
|
||||
"Stop sharing screen": "Zastaviť zdieľanie obrazovky",
|
||||
"Single-key keyboard shortcuts": "Jednotlačidlové klávesové skratky",
|
||||
"Show call inspector": "Zobraziť inšpektora hovorov",
|
||||
"Share screen": "Zdieľať obrazovku",
|
||||
"Sending…": "Odosielanie…",
|
||||
"Sending debug logs…": "Odosielanie záznamov o ladení…",
|
||||
"Send debug logs": "Odoslať záznamy o ladení",
|
||||
"Select an option": "Vyberte možnosť",
|
||||
"Saving…": "Ukladanie…",
|
||||
"Save": "Uložiť",
|
||||
"Return to home screen": "Návrat na domovskú obrazovku",
|
||||
"Remove": "Odstrániť",
|
||||
"Release spacebar key to stop": "Pustite medzerník pre ukončenie",
|
||||
"Release to stop": "Pustite pre ukončenie",
|
||||
"Registering…": "Registrácia…",
|
||||
"Register": "Registrovať sa",
|
||||
"Recaptcha not loaded": "Recaptcha sa nenačítala",
|
||||
"Recaptcha dismissed": "Recaptcha zamietnutá",
|
||||
"Profile": "Profil",
|
||||
"Press and hold to talk over {{name}}": "Stlačte a podržte pre hovor cez {{name}}",
|
||||
"Press and hold to talk": "Stlačte a podržte pre hovor",
|
||||
"Press and hold spacebar to talk over {{name}}": "Stlačte a podržte medzerník, ak chcete hovoriť cez {{name}}",
|
||||
"Press and hold spacebar to talk": "Stlačte a podržte medzerník, ak chcete hovoriť",
|
||||
"Passwords must match": "Heslá sa musia zhodovať",
|
||||
"Password": "Heslo",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Ostatní používatelia sa pokúšajú pripojiť k tomuto hovoru z nekompatibilných verzií. Títo používatelia by sa mali uistiť, že si obnovili svoje prehliadače:<1>{userLis}</1>",
|
||||
"Not registered yet? <2>Create an account</2>": "Ešte nie ste zaregistrovaný? <2>Vytvorte si účet</2>",
|
||||
"Not now, return to home screen": "Teraz nie, vrátiť sa na domovskú obrazovku",
|
||||
"No": "Nie",
|
||||
"Mute microphone": "Stlmiť mikrofón",
|
||||
"More menu": "Ponuka viac",
|
||||
"More": "Viac",
|
||||
"Microphone permissions needed to join the call.": "Povolenie mikrofónu je potrebné na pripojenie k hovoru.",
|
||||
"Microphone {{n}}": "Mikrofón {{n}}",
|
||||
"Microphone": "Mikrofón",
|
||||
"Login to your account": "Prihláste sa do svojho konta",
|
||||
"Login": "Prihlásiť sa",
|
||||
"Logging in…": "Prihlasovanie…",
|
||||
"Loading…": "Načítanie…",
|
||||
"Loading room…": "Načítanie miestnosti…",
|
||||
"Leave": "Opustiť",
|
||||
"Join existing call?": "Pripojiť sa k existujúcemu hovoru?",
|
||||
"Join call now": "Pripojiť sa k hovoru teraz",
|
||||
"Join call": "Pripojiť sa k hovoru",
|
||||
"Invite people": "Pozvať ľudí",
|
||||
"Invite": "Pozvať",
|
||||
"Inspector": "Inšpektor",
|
||||
"Incompatible versions!": "Nekompatibilné verzie!",
|
||||
"Incompatible versions": "Nekompatibilné verzie",
|
||||
"Home": "Domov",
|
||||
"Having trouble? Help us fix it.": "Máte problém? Pomôžte nám ho opraviť.",
|
||||
"Grid layout menu": "Ponuka rozloženia mriežky",
|
||||
"Go": "Prejsť",
|
||||
"Full screen": "Zobrazenie na celú obrazovku",
|
||||
"Freedom": "Sloboda",
|
||||
"Exit full screen": "Ukončiť zobrazenie na celú obrazovku",
|
||||
"Download debug logs": "Stiahnuť záznamy ladenia",
|
||||
"Your recent calls": "Vaše nedávne hovory",
|
||||
"Yes, join call": "Áno, pripojiť sa k hovoru",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC nie je podporované alebo je v tomto prehliadači blokované.",
|
||||
"Walkie-talkie call name": "Názov vysielačkového hovoru",
|
||||
"Walkie-talkie call": "Vysielačkový hovor",
|
||||
"Video call name": "Názov video hovoru",
|
||||
"Video call": "Video hovor",
|
||||
"Video": "Video",
|
||||
"Version: {{version}}": "Verzia: {{version}}",
|
||||
"Username": "Meno používateľa",
|
||||
"User menu": "Používateľské menu",
|
||||
"User ID": "ID používateľa",
|
||||
"Unmute microphone": "Zrušiť stlmenie mikrofónu",
|
||||
"Turn on camera": "Zapnúť kameru",
|
||||
"Turn off camera": "Vypnúť kameru",
|
||||
"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>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov</2> a <6>Podmienky poskytovania služieb</6> spoločnosti Google.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" vyjadrujete súhlas s našimi <12>Podmienkami poskytovania služieb</12>",
|
||||
"This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?",
|
||||
"Speaker {{n}}": "Reproduktor {{n}}",
|
||||
"Speaker": "Reproduktor",
|
||||
"Spatial audio": "Priestorový zvuk",
|
||||
"Sign out": "Odhlásiť sa",
|
||||
"Sign in": "Prihlásiť sa",
|
||||
"Settings": "Nastavenia",
|
||||
"Display name": "Zobrazované meno",
|
||||
"Developer": "Vývojár",
|
||||
"Details": "Podrobnosti",
|
||||
"Description (optional)": "Popis (voliteľné)",
|
||||
"Debug log request": "Žiadosť o záznam ladenia",
|
||||
"Debug log": "Záznam o ladení",
|
||||
"Create account": "Vytvoriť účet",
|
||||
"Copy and share this call link": "Skopírovať a zdieľať tento odkaz na hovor",
|
||||
"Copy": "Kopírovať",
|
||||
"Copied!": "Skopírované!",
|
||||
"Connection lost": "Strata spojenia",
|
||||
"Confirm password": "Potvrdiť heslo",
|
||||
"Close": "Zatvoriť",
|
||||
"Change layout": "Zmeniť rozloženie",
|
||||
"Camera/microphone permissions needed to join the call.": "Povolenie kamery/mikrofónu je potrebné na pripojenie k hovoru.",
|
||||
"Camera {{n}}": "Kamera {{n}}",
|
||||
"Camera": "Kamera",
|
||||
"Call type menu": "Ponuka typu hovoru",
|
||||
"Call link copied": "Odkaz na hovor skopírovaný",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pripojiť sa k hovoru\" súhlasíte s našimi <2>Podmienkami</2>",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na tlačidlo \"Prejsť\" súhlasíte s našimi <2>Podmienkami</2>",
|
||||
"Avatar": "Obrázok",
|
||||
"Audio": "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.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.",
|
||||
"Allow analytics": "Povoliť analytiku",
|
||||
"Advanced": "Pokročilé",
|
||||
"Accept camera/microphone permissions to join the call.": "Prijmite povolenia kamery/mikrofónu, aby ste sa mohli pripojiť k hovoru.",
|
||||
"Accept microphone permissions to join the call.": "Prijmite povolenia mikrofónu, aby ste sa mohli pripojiť k hovoru.",
|
||||
"<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>Prečo neskončiť nastavením hesla, aby ste si zachovali svoj účet? </0><1>Budete si môcť ponechať svoje meno a nastaviť obrázok, ktorý sa bude používať pri budúcich hovoroch</1>",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Pripojiť sa k hovoru teraz</0><1>alebo</1><2>Kopírovať odkaz na hovor a pripojiť sa neskôr</2>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Vytvoriť konto</0> Alebo <2>Prihlásiť sa ako hosť</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Už máte konto?</0><1><0>Prihláste sa</0> Alebo <2>Prihlásiť sa ako hosť</2></1>",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Vysielačkový hovor",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is talking…": "{{name}} rozpráva…",
|
||||
"{{name}} is presenting": "{{name}} prezentuje",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Čaká sa na video...)",
|
||||
"{{name}} (Connecting...)": "{{name}} (Pripájanie...)",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, váš hovor je teraz ukončený",
|
||||
"{{count}} people connected|other": "{{count}} osôb pripojených",
|
||||
"{{count}} people connected|one": "{{count}} osoba pripojená",
|
||||
"This feature is only supported on Firefox.": "Táto funkcia je podporovaná len v prehliadači Firefox.",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.</0>",
|
||||
"<0>Oops, something's gone wrong.</0>": "<0>Hups, niečo sa pokazilo.</0>"
|
||||
}
|
||||
100
public/locales/tr/app.json
Normal file
100
public/locales/tr/app.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"<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",
|
||||
"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>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>"
|
||||
}
|
||||
143
public/locales/uk/app.json
Normal file
143
public/locales/uk/app.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"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": "Вийти з повноекранного режиму",
|
||||
"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>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}} (З'єднання...)",
|
||||
"Allow analytics": "Дозволити аналітику",
|
||||
"Advanced": "Розширені",
|
||||
"Element Call Home": "Домівка Element Call",
|
||||
"Copy": "Копіювати",
|
||||
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Це дозволить надсилати анонімні дані (такі як тривалість виклику та кількість учасників) команді Element Call, щоб допомогти нам оптимізувати роботу застосунку на основі того, як він використовується.",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Чи вмикати/вимикати мікрофон однією клавішею, наприклад, «m» для ввімкнення/вимкнення мікрофона.",
|
||||
"Single-key keyboard shortcuts": "Одноклавішні комбінації клавіш",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (Очікування на відео...)",
|
||||
"This feature is only supported on Firefox.": "Ця функція підтримується лише в браузері Firefox.",
|
||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Надсилання журналів зневадження допоможе нам виявити проблему.</0>",
|
||||
"<0>Oops, something's gone wrong.</0>": "<0>Йой, щось пішло не за планом.</0>"
|
||||
}
|
||||
140
public/locales/zh-Hans/app.json
Normal file
140
public/locales/zh-Hans/app.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"Your recent calls": "最近通话",
|
||||
"You can't talk at the same time": "你不能在同一时间发言",
|
||||
"Yes, join call": "是,加入通话",
|
||||
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "是否启用单键键盘快捷键,例如,'m'可使麦克风静音/取消静音。",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "此浏览器不支持WebRTC或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 send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "这将向Element Call团队发送匿名数据(如通话的持续时间和参与者的数量),以帮助我们根据使用方式优化应用程序。",
|
||||
"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…": "正在发言……",
|
||||
"Talk over speaker": "通过扬声器发言",
|
||||
"Take me Home": "返回主页",
|
||||
"Submitting feedback…": "正在提交反馈……",
|
||||
"Submit feedback": "提交反馈",
|
||||
"Stop sharing screen": "停止屏幕共享",
|
||||
"Spotlight": "聚焦模式",
|
||||
"Speaker {{n}}": "发言人 {{n}}",
|
||||
"Speaker": "发言人",
|
||||
"Spatial audio": "空间音频",
|
||||
"Single-key keyboard shortcuts": "单键键盘快捷方式",
|
||||
"Sign out": "注销登录",
|
||||
"Sign in": "登录",
|
||||
"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.": "这个通话中的另一个用户出现了问题。为了更好地诊断这些问题,我们想收集调试日志。",
|
||||
"Allow analytics": "允许进行分析",
|
||||
"Advanced": "偏好",
|
||||
"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>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>现在加入通话</0><1>或</1><2>复制通话链接并稍后加入</2>",
|
||||
"<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}} - 对讲机通话",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is talking…": "{{name}}正在发言……",
|
||||
"{{name}} is presenting": "{{name}}正在展示",
|
||||
"{{name}} (Waiting for video...)": "{{name}}(等待视频……)",
|
||||
"{{name}} (Connecting...)": "{{name}} (正在连接……)",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}},您的通话已结束",
|
||||
"{{count}} people connected|other": "{{count}}人已连接",
|
||||
"{{count}} people connected|one": "{{count}}人已连接",
|
||||
"Inspector": "检查器",
|
||||
"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…": "加载中……",
|
||||
"Loading room…": "加载房间中……",
|
||||
"Leave": "离开",
|
||||
"Join existing call?": "加入现有的通话?",
|
||||
"Join call now": "现在加入通话",
|
||||
"Join call": "加入通话",
|
||||
"Invite people": "邀请他人",
|
||||
"Invite": "邀请",
|
||||
"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": "退出全屏",
|
||||
"Element Call Home": "Element Call 主页",
|
||||
"Download debug logs": "下载调试日志",
|
||||
"Display name": "显示名称",
|
||||
"Developer": "开发者",
|
||||
"Details": "详情",
|
||||
"Description (optional)": "描述(可选)",
|
||||
"Debug log request": "调试日志请求",
|
||||
"Debug log": "调试日志",
|
||||
"Create account": "创建账户",
|
||||
"Copy and share this call link": "复制并分享该链接",
|
||||
"Copy": "复制",
|
||||
"Copied!": "已复制!",
|
||||
"Connection lost": "连接丢失",
|
||||
"Confirm password": "确认密码",
|
||||
"Close": "关闭",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "点击开始则代表同意我们的<2>条款和条件<2>",
|
||||
"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>",
|
||||
"Avatar": "头像"
|
||||
}
|
||||
14
public/locales/zh-Hant/app.json
Normal file
14
public/locales/zh-Hant/app.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>加入通話</0><1>或</1><2>複製通話連結並稍候加入</2>",
|
||||
"<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}} 正在報告",
|
||||
"{{name}} (Waiting for video...)": "{{name}} (等待視訊畫面 ...)",
|
||||
"{{name}} (Connecting...)": "{{name}} (連線中 ...)",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}},您的通話現在已結束",
|
||||
"{{count}} people connected|other": "{{count}} 人已連線",
|
||||
"{{count}} people connected|one": "{{count}} 人已連線"
|
||||
}
|
||||
@@ -2,22 +2,7 @@
|
||||
|
||||
set -ex
|
||||
|
||||
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
||||
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
|
||||
cd matrix-js-sdk
|
||||
git checkout robertlong/group-call
|
||||
yarn install
|
||||
yarn run build
|
||||
yarn link
|
||||
|
||||
cd ../element-call
|
||||
|
||||
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||
|
||||
yarn link matrix-js-sdk
|
||||
yarn install
|
||||
yarn run build
|
||||
|
||||
41
src/@types/global.d.ts
vendored
Normal file
41
src/@types/global.d.ts
vendored
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 "matrix-js-sdk/src/@types/global";
|
||||
|
||||
declare global {
|
||||
interface Document {
|
||||
// Safari only supports this prefixed, so tell the type system about it
|
||||
webkitExitFullscreen: () => void;
|
||||
webkitFullscreenElement: HTMLElement | null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface HTMLElement {
|
||||
// Safari only supports this prefixed, so tell the type system about it
|
||||
webkitRequestFullscreen: () => void;
|
||||
}
|
||||
}
|
||||
18
src/@types/modules.d.ts
vendored
Normal file
18
src/@types/modules.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
63
src/App.jsx
63
src/App.jsx
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
import { HomePage } from "./home/HomePage";
|
||||
import { LoginPage } from "./auth/LoginPage";
|
||||
import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { RoomRedirect } from "./room/RoomRedirect";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
export default function App({ history }) {
|
||||
usePageFocusStyle();
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<ClientProvider>
|
||||
<OverlayProvider>
|
||||
<Switch>
|
||||
<SentryRoute exact path="/">
|
||||
<HomePage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
</OverlayProvider>
|
||||
</ClientProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
91
src/App.tsx
Normal file
91
src/App.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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, useEffect, useState } 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, LoadingView } from "./FullScreenView";
|
||||
import { Initializer } from "./initializer";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
interface AppProps {
|
||||
history: History;
|
||||
}
|
||||
|
||||
export default function App({ history }: AppProps) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Initializer.init()?.then(() => {
|
||||
setLoaded(true);
|
||||
});
|
||||
});
|
||||
|
||||
usePageFocusStyle();
|
||||
|
||||
const errorPage = <CrashView />;
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
{loaded ? (
|
||||
<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>
|
||||
) : (
|
||||
<LoadingView />
|
||||
)}
|
||||
</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,22 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
color: var(--primary-content);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -17,7 +33,7 @@
|
||||
}
|
||||
|
||||
.avatar svg * {
|
||||
fill: #ffffff;
|
||||
fill: var(--primary-content);
|
||||
}
|
||||
|
||||
.avatar span {
|
||||
|
||||
131
src/Avatar.tsx
Normal file
131
src/Avatar.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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, { 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,265 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
createContext,
|
||||
useMemo,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import { initClient, defaultHomeserver } from "./matrix-utils";
|
||||
|
||||
const ClientContext = createContext();
|
||||
|
||||
export function ClientProvider({ children }) {
|
||||
const history = useHistory();
|
||||
const [
|
||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||
setState,
|
||||
] = useState({
|
||||
loading: true,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
client: undefined,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function restore() {
|
||||
try {
|
||||
const authStore = localStorage.getItem("matrix-auth-store");
|
||||
|
||||
if (authStore) {
|
||||
const {
|
||||
user_id,
|
||||
device_id,
|
||||
access_token,
|
||||
passwordlessUser,
|
||||
tempPassword,
|
||||
} = JSON.parse(authStore);
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({
|
||||
user_id,
|
||||
device_id,
|
||||
access_token,
|
||||
|
||||
passwordlessUser,
|
||||
tempPassword,
|
||||
})
|
||||
);
|
||||
|
||||
return { client, passwordlessUser };
|
||||
}
|
||||
|
||||
return { client: undefined };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
restore()
|
||||
.then(({ client, passwordlessUser }) => {
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: !!client,
|
||||
isPasswordlessUser: !!passwordlessUser,
|
||||
userName: client?.getUserIdLocalpart(),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const changePassword = useCallback(
|
||||
async (password) => {
|
||||
const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
|
||||
localStorage.getItem("matrix-auth-store")
|
||||
);
|
||||
|
||||
await client.setPassword(
|
||||
{
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: existingSession.user_id,
|
||||
},
|
||||
user: existingSession.user_id,
|
||||
password: tempPassword,
|
||||
},
|
||||
password
|
||||
);
|
||||
|
||||
localStorage.setItem(
|
||||
"matrix-auth-store",
|
||||
JSON.stringify({
|
||||
...existingSession,
|
||||
passwordlessUser: false,
|
||||
})
|
||||
);
|
||||
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const setClient = useCallback(
|
||||
(newClient, session) => {
|
||||
if (client && client !== newClient) {
|
||||
client.stopClient();
|
||||
}
|
||||
|
||||
if (newClient) {
|
||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
||||
|
||||
setState({
|
||||
client: newClient,
|
||||
loading: false,
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: !!session.passwordlessUser,
|
||||
userName: newClient.getUserIdLocalpart(),
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem("matrix-auth-store");
|
||||
window.location = "/";
|
||||
}, [history]);
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
const loadTime = Date.now();
|
||||
|
||||
const onToDeviceEvent = (event) => {
|
||||
if (event.getType() !== "org.matrix.call_duplicate_session") {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.getContent();
|
||||
|
||||
if (content.session_id === client.getSessionId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.timestamp > loadTime) {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: new Error(
|
||||
"This application has been opened in another tab."
|
||||
),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
client.on("toDeviceEvent", onToDeviceEvent);
|
||||
|
||||
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||
[client.getUserId()]: {
|
||||
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||
};
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
client,
|
||||
changePassword,
|
||||
logout,
|
||||
userName,
|
||||
setClient,
|
||||
}),
|
||||
[
|
||||
loading,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
client,
|
||||
changePassword,
|
||||
logout,
|
||||
userName,
|
||||
setClient,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.matrixclient = client;
|
||||
}, [client]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useClient() {
|
||||
return useContext(ClientContext);
|
||||
}
|
||||
353
src/ClientContext.tsx
Normal file
353
src/ClientContext.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
Copyright 2021-2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
createContext,
|
||||
useMemo,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import {
|
||||
initClient,
|
||||
CryptoStoreIntegrityError,
|
||||
fallbackICEServerAllowed,
|
||||
} from "./matrix-utils";
|
||||
import { widget } from "./widget";
|
||||
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
|
||||
import { translatedError } from "./TranslatedError";
|
||||
import { useEventTarget } from "./useEvents";
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
matrixclient: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
access_token: string;
|
||||
passwordlessUser: boolean;
|
||||
tempPassword?: string;
|
||||
}
|
||||
|
||||
const loadChannel =
|
||||
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
|
||||
|
||||
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: Config.defaultHomeserverUrl(),
|
||||
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: Config.defaultHomeserverUrl(),
|
||||
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("/");
|
||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
||||
}, [history, client]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// To protect against multiple sessions writing to the same storage
|
||||
// simultaneously, we send a broadcast 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.
|
||||
useEffect(() => {
|
||||
if (!widget) loadChannel?.postMessage({});
|
||||
}, []);
|
||||
|
||||
useEventTarget(
|
||||
loadChannel,
|
||||
"message",
|
||||
useCallback(() => {
|
||||
client?.stopClient();
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: translatedError(
|
||||
"This application has been opened in another tab.",
|
||||
t
|
||||
),
|
||||
}));
|
||||
}, [client, setState, 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;
|
||||
window.isPasswordlessUser = isPasswordlessUser;
|
||||
}, [client, isPasswordlessUser]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useClient = () => useContext(ClientContext);
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from "react";
|
||||
import styles from "./Facepile.module.css";
|
||||
import classNames from "classnames";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { getAvatarUrl } from "./matrix-utils";
|
||||
|
||||
export function Facepile({ className, client, participants, ...rest }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.facepile, className)}
|
||||
title={participants.map((member) => member.name).join(", ")}
|
||||
{...rest}
|
||||
>
|
||||
{participants.slice(0, 3).map((member, i) => {
|
||||
const avatarUrl = member.user?.avatarUrl;
|
||||
return (
|
||||
<Avatar
|
||||
key={member.userId}
|
||||
size="xs"
|
||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 22)}
|
||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
style={{ left: i * 22 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{participants.length > 3 && (
|
||||
<Avatar
|
||||
key="additional"
|
||||
size="xs"
|
||||
fallback={`+${participants.length - 3}`}
|
||||
className={styles.avatar}
|
||||
style={{ left: 3 * 22 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,42 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.facepile {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.facepile.xs {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.facepile.sm {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.facepile.md {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.facepile .avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border: 1px solid var(--bgColor2);
|
||||
border: 1px solid var(--system);
|
||||
}
|
||||
|
||||
.facepile.md .avatar {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
97
src/Facepile.tsx
Normal file
97
src/Facepile.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
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;
|
||||
members: RoomMember[];
|
||||
max?: number;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export function Facepile({
|
||||
className,
|
||||
client,
|
||||
members,
|
||||
max = 3,
|
||||
size = Size.XS,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _size = sizes.get(size);
|
||||
const _overlap = overlapMap[size];
|
||||
|
||||
const title = useMemo(() => {
|
||||
return members.reduce<string | null>(
|
||||
(prev, curr) =>
|
||||
prev === null
|
||||
? curr.name
|
||||
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
||||
null
|
||||
) as string;
|
||||
}, [members, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.facepile, styles[size], className)}
|
||||
title={title}
|
||||
style={{
|
||||
width:
|
||||
Math.min(members.length, max + 1) * (_size - _overlap) + _overlap,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{members.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) }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{members.length > max && (
|
||||
<Avatar
|
||||
key="additional"
|
||||
size={size}
|
||||
fallback={`+${members.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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -36,6 +52,12 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.homeLink {
|
||||
/* Make the buttons the same width */
|
||||
.wideButton {
|
||||
width: 291px;
|
||||
}
|
||||
|
||||
/* Fixed height to avoid content jumping around*/
|
||||
.sendLogsSection {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
166
src/FullScreenView.tsx
Normal file
166
src/FullScreenView.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
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, { 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";
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
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 if (Config.get().rageshake?.submit_url) {
|
||||
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>
|
||||
</Trans>
|
||||
{Config.get().rageshake?.submit_url && (
|
||||
<Trans>
|
||||
<p>Submitting debug logs will help us track down the problem.</p>
|
||||
</Trans>
|
||||
)}
|
||||
|
||||
<div className={styles.sendLogsSection}>{logsComponent}</div>
|
||||
{error && (
|
||||
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
|
||||
)}
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.wideButton}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("Return to home screen")}
|
||||
</Button>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingView() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Loading…")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import React, { useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styles from "./Header.module.css";
|
||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { Subtitle } from "./typography/Typography";
|
||||
import { Avatar } from "./Avatar";
|
||||
|
||||
export function Header({ children, className, ...rest }) {
|
||||
return (
|
||||
<header className={classNames(styles.header, className)} {...rest}>
|
||||
{children}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function LeftNav({ children, className, hideMobile, ...rest }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.nav,
|
||||
styles.leftNav,
|
||||
{ [styles.hideMobile]: hideMobile },
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RightNav({ children, className, hideMobile, ...rest }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.nav,
|
||||
styles.rightNav,
|
||||
{ [styles.hideMobile]: hideMobile },
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeaderLogo({ className }) {
|
||||
return (
|
||||
<Link className={classNames(styles.headerLogo, className)} to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomHeaderInfo({ roomName }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.roomAvatar}>
|
||||
<Avatar
|
||||
size="md"
|
||||
bgKey={roomName}
|
||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<VideoIcon width={16} height={16} />
|
||||
</div>
|
||||
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
|
||||
const ref = useRef();
|
||||
const { buttonProps } = useButton(rest, ref);
|
||||
return (
|
||||
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
||||
<ArrowLeftIcon width={16} height={16} />
|
||||
<RoomHeaderInfo roomName={roomName} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -70,7 +86,7 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
color: var(--textColor1);
|
||||
color: var(--primary-content);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -104,6 +120,24 @@
|
||||
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) {
|
||||
.headerLogo,
|
||||
.roomAvatar,
|
||||
@@ -113,7 +147,7 @@
|
||||
}
|
||||
|
||||
.leftNav h3 {
|
||||
font-size: 18px;
|
||||
font-size: var(--font-size-subtitle);
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
||||
202
src/Header.tsx
Normal file
202
src/Header.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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 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) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(styles.headerLogo, className)}
|
||||
to="/"
|
||||
aria-label={t("Element Call Home")}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
60
src/IncompatibleVersionModal.tsx
Normal file
60
src/IncompatibleVersionModal.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const IncompatibleVersionModal: React.FC<Props> = ({
|
||||
userIds,
|
||||
room,
|
||||
onClose,
|
||||
...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
|
||||
onClose={onClose}
|
||||
{...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>
|
||||
);
|
||||
};
|
||||
22
src/IndexedDBWorker.ts
Normal file
22
src/IndexedDBWorker.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const remoteWorker = new IndexedDBStoreWorker((self as any).postMessage);
|
||||
|
||||
self.onmessage = remoteWorker.onMessage;
|
||||
90
src/LazyEventEmitter.ts
Normal file
90
src/LazyEventEmitter.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2022 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";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.listBox {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -5,8 +21,8 @@
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
background-color: var(--bgColor1);
|
||||
border: 1px solid var(--quinary-content);
|
||||
background-color: var(--background);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -15,11 +31,11 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: transparent;
|
||||
color: var(--textColor1);
|
||||
color: var(--primary-content);
|
||||
padding: 8px 16px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-size: var(--font-size-body);
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
@@ -28,6 +44,6 @@
|
||||
}
|
||||
|
||||
.option.disabled {
|
||||
color: var(--textColor2);
|
||||
color: var(--quaternary-content);
|
||||
background-color: var(--bgColor3);
|
||||
}
|
||||
|
||||
105
src/ListBox.tsx
Normal file
105
src/ListBox.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
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, 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
|
||||
);
|
||||
|
||||
// Hack: remove the onPointerUp event handler and re-wire it to
|
||||
// onClick. Chrome Android triggers a click event after the onpointerup
|
||||
// event which leaks through to elements underneath the z-indexed select
|
||||
// popover. preventDefault / stopPropagation don't have any effect, even
|
||||
// adding just a dummy onClick handler still doesn't work, but it's fine
|
||||
// if we handle just onClick.
|
||||
// https://github.com/vector-im/element-call/issues/762
|
||||
const origPointerUp = optionProps.onPointerUp;
|
||||
delete optionProps.onPointerUp;
|
||||
optionProps.onClick = useCallback(
|
||||
(e) => {
|
||||
origPointerUp(e as unknown as React.PointerEvent<HTMLElement>);
|
||||
},
|
||||
[origPointerUp]
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...optionProps}
|
||||
ref={ref}
|
||||
className={classNames(styles.option, className, {
|
||||
[styles.selected]: isSelected,
|
||||
[styles.focused]: isFocused,
|
||||
[styles.disables]: isDisabled,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
60
src/Menu.jsx
60
src/Menu.jsx
@@ -1,60 +0,0 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import styles from "./Menu.module.css";
|
||||
import { useMenu, useMenuItem } from "@react-aria/menu";
|
||||
import { useTreeState } from "@react-stately/tree";
|
||||
import { mergeProps } from "@react-aria/utils";
|
||||
import { useFocus } from "@react-aria/interactions";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Menu({ className, onAction, ...rest }) {
|
||||
const state = useTreeState({ ...rest, selectionMode: "none" });
|
||||
const menuRef = useRef();
|
||||
const { menuProps } = useMenu(rest, state, menuRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...mergeProps(menuProps, rest)}
|
||||
ref={menuRef}
|
||||
className={classNames(styles.menu, className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
onAction={onAction}
|
||||
onClose={rest.onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ item, state, onAction, onClose }) {
|
||||
const ref = useRef();
|
||||
const { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
isDisabled: item.isDisabled,
|
||||
onAction,
|
||||
onClose,
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const { focusProps } = useFocus({ onFocusChange: setFocused });
|
||||
|
||||
return (
|
||||
<li
|
||||
{...mergeProps(menuItemProps, focusProps)}
|
||||
ref={ref}
|
||||
className={classNames(styles.menuItem, {
|
||||
[styles.focused]: isFocused,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
@@ -11,8 +27,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: var(--textColor1);
|
||||
font-size: 14px;
|
||||
color: var(--primary-content);
|
||||
font-size: var(--font-size-body);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menuItem > * {
|
||||
@@ -25,7 +44,7 @@
|
||||
|
||||
.menuItem.focused,
|
||||
.menuItem:hover {
|
||||
background-color: var(--bgColor4);
|
||||
background-color: var(--quinary-content);
|
||||
}
|
||||
|
||||
.menuItem.focused:first-child,
|
||||
@@ -39,3 +58,12 @@
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.checkIcon * {
|
||||
stroke: var(--primary-content);
|
||||
}
|
||||
|
||||
97
src/Menu.tsx
Normal file
97
src/Menu.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
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, { Key, useRef, useState } from "react";
|
||||
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
|
||||
import { TreeState, useTreeState } from "@react-stately/tree";
|
||||
import { mergeProps } from "@react-aria/utils";
|
||||
import { useFocus } from "@react-aria/interactions";
|
||||
import classNames from "classnames";
|
||||
import { Node } from "@react-types/shared";
|
||||
|
||||
import styles from "./Menu.module.css";
|
||||
|
||||
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 { menuProps } = useMenu<T>(rest, state, menuRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...mergeProps(menuProps, rest)}
|
||||
ref={menuRef}
|
||||
className={classNames(styles.menu, className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
onAction={onAction}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
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 { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
onAction,
|
||||
onClose,
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const { focusProps } = useFocus({ onFocusChange: setFocused });
|
||||
|
||||
return (
|
||||
<li
|
||||
{...mergeProps(menuItemProps, focusProps)}
|
||||
ref={ref}
|
||||
className={classNames(styles.menuItem, {
|
||||
[styles.focused]: isFocused,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
@@ -28,7 +44,8 @@
|
||||
}
|
||||
|
||||
.modalHeader h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-title);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
204
src/Modal.tsx
Normal file
204
src/Modal.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
|
||||
import React, { useRef, useMemo, ReactNode } from "react";
|
||||
import {
|
||||
useOverlay,
|
||||
usePreventScroll,
|
||||
useModal,
|
||||
OverlayContainer,
|
||||
OverlayProps,
|
||||
} from "@react-aria/overlays";
|
||||
import {
|
||||
OverlayTriggerState,
|
||||
useOverlayTriggerState,
|
||||
} from "@react-stately/overlays";
|
||||
import { useDialog } from "@react-aria/dialog";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { ButtonAria, useButton } from "@react-aria/button";
|
||||
import classNames from "classnames";
|
||||
import { AriaDialogProps } from "@react-types/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||
import styles from "./Modal.module.css";
|
||||
|
||||
export interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
mobileFullScreen?: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
mobileFullScreen,
|
||||
onClose,
|
||||
...rest
|
||||
}: ModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const modalRef = useRef();
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ ...rest, onClose },
|
||||
modalRef
|
||||
);
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||
const closeButtonRef = useRef();
|
||||
const { buttonProps: closeButtonProps } = useButton(
|
||||
{
|
||||
onPress: () => onClose(),
|
||||
},
|
||||
closeButtonRef
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<div className={styles.modalOverlay} {...underlayProps}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={classNames(
|
||||
styles.modal,
|
||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
<button
|
||||
{...closeButtonProps}
|
||||
ref={closeButtonRef}
|
||||
className={styles.closeButton}
|
||||
title={t("Close")}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalContentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ModalContent({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: ModalContentProps) {
|
||||
return (
|
||||
<div className={classNames(styles.content, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState(): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: { isOpen: boolean; onClose: () => void };
|
||||
} {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
}
|
||||
|
||||
export function useToggleModalButton(
|
||||
modalState: OverlayTriggerState,
|
||||
ref: React.RefObject<HTMLButtonElement>
|
||||
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.toggle(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenModalButton(
|
||||
modalState: OverlayTriggerState,
|
||||
ref: React.RefObject<HTMLButtonElement>
|
||||
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.open(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useCloseModalButton(
|
||||
modalState: OverlayTriggerState,
|
||||
ref: React.RefObject<HTMLButtonElement>
|
||||
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.close(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalTriggerProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ModalTrigger({ children }: ModalTriggerProps) {
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const buttonRef = useRef();
|
||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [modalTrigger, modal] = children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<modalTrigger.type
|
||||
{...modalTrigger.props}
|
||||
{...buttonProps}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{modalState.isOpen && modal(modalProps)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
418
src/PosthogAnalytics.ts
Normal file
418
src/PosthogAnalytics.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
Copyright 2022 The 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 posthog, { CaptureOptions, PostHog, Properties } from "posthog-js";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixClient } from "matrix-js-sdk";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import { widget } from "./widget";
|
||||
import { getSetting, setSetting, settingsBus } from "./settings/useSetting";
|
||||
import {
|
||||
CallEndedTracker,
|
||||
CallStartedTracker,
|
||||
LoginTracker,
|
||||
SignupTracker,
|
||||
MuteCameraTracker,
|
||||
MuteMicrophoneTracker,
|
||||
} from "./PosthogEvents";
|
||||
import { Config } from "./config/Config";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
|
||||
/* Posthog analytics tracking.
|
||||
*
|
||||
* Anonymity behaviour is as follows:
|
||||
*
|
||||
* - If Posthog isn't configured in `config.json`, events are not sent.
|
||||
* - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
|
||||
* enabled, events are not sent (this detection is built into posthog and turned on via the
|
||||
* `respect_dnt` flag being passed to `posthog.init`).
|
||||
* - If the posthog analytics are explicitly activated by the user in the element call settings,
|
||||
* a randomised analytics ID is created and stored in account_data for that user (shared between devices)
|
||||
* so that the user can be identified in posthog.
|
||||
*/
|
||||
|
||||
export interface IPosthogEvent {
|
||||
// The event name that will be used by PostHog. Event names should use camelCase.
|
||||
eventName: string;
|
||||
|
||||
// do not allow these to be sent manually, we enqueue them all for caching purposes
|
||||
$set?: void;
|
||||
$set_once?: void;
|
||||
}
|
||||
|
||||
export enum Anonymity {
|
||||
Disabled,
|
||||
Anonymous,
|
||||
Pseudonymous,
|
||||
}
|
||||
|
||||
export enum RegistrationType {
|
||||
Guest,
|
||||
Registered,
|
||||
}
|
||||
|
||||
interface PlatformProperties {
|
||||
appVersion: string;
|
||||
matrixBackend: "embedded" | "jssdk";
|
||||
}
|
||||
|
||||
interface PosthogSettings {
|
||||
project_api_key?: string;
|
||||
api_host?: string;
|
||||
}
|
||||
|
||||
export class PosthogAnalytics {
|
||||
/* Wrapper for Posthog analytics.
|
||||
* 3 modes of anonymity are supported, governed by this.anonymity
|
||||
* - Anonymity.Disabled means *no data* is passed to posthog
|
||||
* - Anonymity.Anonymous means no identifier is passed to posthog
|
||||
* - Anonymity.Pseudonymous means an analytics ID stored in account_data and shared between devices
|
||||
* is passed to posthog.
|
||||
*
|
||||
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
||||
*
|
||||
* To pass an event to Posthog:
|
||||
*
|
||||
* 1. Declare a type for the event, extending IPosthogEvent.
|
||||
*/
|
||||
|
||||
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics";
|
||||
|
||||
// set true during the constructor if posthog config is present, otherwise false
|
||||
private static internalInstance = null;
|
||||
|
||||
private identificationPromise: Promise<void>;
|
||||
private readonly enabled: boolean = false;
|
||||
private anonymity = Anonymity.Disabled;
|
||||
private platformSuperProperties = {};
|
||||
private registrationType: RegistrationType = RegistrationType.Guest;
|
||||
|
||||
public static get instance(): PosthogAnalytics {
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new PosthogAnalytics(posthog);
|
||||
}
|
||||
return this.internalInstance;
|
||||
}
|
||||
|
||||
constructor(private readonly posthog: PostHog) {
|
||||
const posthogConfig: PosthogSettings = {
|
||||
project_api_key: Config.get().posthog?.api_key,
|
||||
api_host: Config.get().posthog?.api_host,
|
||||
};
|
||||
|
||||
if (posthogConfig.project_api_key && posthogConfig.api_host) {
|
||||
if (
|
||||
PosthogAnalytics.getPlatformProperties().matrixBackend === "embedded"
|
||||
) {
|
||||
const { analyticsID } = getUrlParams();
|
||||
// if the embedding platform (element web) already got approval to communicating with posthog
|
||||
// element call can also send events to posthog
|
||||
setSetting("opt-in-analytics", Boolean(analyticsID));
|
||||
}
|
||||
|
||||
this.posthog.init(posthogConfig.project_api_key, {
|
||||
api_host: posthogConfig.api_host,
|
||||
autocapture: false,
|
||||
mask_all_text: true,
|
||||
mask_all_element_attributes: true,
|
||||
capture_pageview: false,
|
||||
sanitize_properties: this.sanitizeProperties,
|
||||
respect_dnt: true,
|
||||
advanced_disable_decide: true,
|
||||
});
|
||||
this.enabled = true;
|
||||
} else {
|
||||
this.enabled = false;
|
||||
}
|
||||
this.startListeningToSettingsChanges();
|
||||
const optInAnalytics = getSetting("opt-in-analytics", false);
|
||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||
}
|
||||
|
||||
private sanitizeProperties = (
|
||||
properties: Properties,
|
||||
_eventName: string
|
||||
): Properties => {
|
||||
// Callback from posthog to sanitize properties before sending them to the server.
|
||||
// Here we sanitize posthog's built in properties which leak PII e.g. url reporting.
|
||||
// See utils.js _.info.properties in posthog-js.
|
||||
|
||||
if (this.anonymity == Anonymity.Anonymous) {
|
||||
// drop referrer information for anonymous users
|
||||
properties["$referrer"] = null;
|
||||
properties["$referring_domain"] = null;
|
||||
properties["$initial_referrer"] = null;
|
||||
properties["$initial_referring_domain"] = null;
|
||||
|
||||
// drop device ID, which is a UUID persisted in local storage
|
||||
properties["$device_id"] = null;
|
||||
}
|
||||
// the url leaks a lot of private data like the call name or the user.
|
||||
// Its stripped down to the bare minimum to only give insights about the host (develop, main or sfu)
|
||||
properties["$current_url"] = (properties["$current_url"] as string)
|
||||
.split("/")
|
||||
.slice(0, 3)
|
||||
.join("");
|
||||
|
||||
return properties;
|
||||
};
|
||||
|
||||
private registerSuperProperties(properties: Properties) {
|
||||
if (this.enabled) {
|
||||
this.posthog.register(properties);
|
||||
}
|
||||
}
|
||||
|
||||
private static getPlatformProperties(): PlatformProperties {
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION || "dev";
|
||||
return {
|
||||
appVersion,
|
||||
matrixBackend: widget ? "embedded" : "jssdk",
|
||||
};
|
||||
}
|
||||
|
||||
private capture(
|
||||
eventName: string,
|
||||
properties: Properties,
|
||||
options?: CaptureOptions
|
||||
) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
this.posthog.capture(eventName, { ...properties }, options);
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
setAnonymity(anonymity: Anonymity): void {
|
||||
// Update this.anonymity.
|
||||
// To update the anonymity typically you want to call updateAnonymityFromSettings
|
||||
// to ensure this value is in step with the user's settings.
|
||||
if (
|
||||
this.enabled &&
|
||||
(anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)
|
||||
) {
|
||||
// when transitioning to Disabled or Anonymous ensure we clear out any prior state
|
||||
// set in posthog e.g. distinct ID
|
||||
this.posthog.reset();
|
||||
// Restore any previously set platform super properties
|
||||
this.updateSuperProperties();
|
||||
}
|
||||
this.anonymity = anonymity;
|
||||
}
|
||||
|
||||
private static getRandomAnalyticsId(): string {
|
||||
return [...crypto.getRandomValues(new Uint8Array(16))]
|
||||
.map((c) => c.toString(16))
|
||||
.join("");
|
||||
}
|
||||
|
||||
public async identifyUser(analyticsIdGenerator: () => string) {
|
||||
// There might be a better way to get the client here.
|
||||
|
||||
if (this.anonymity == Anonymity.Pseudonymous) {
|
||||
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
|
||||
// different devices to send the same ID.
|
||||
let analyticsID = await this.getAnalyticsId();
|
||||
try {
|
||||
if (!analyticsID && !widget) {
|
||||
// only try setting up a new analytics ID in the standalone app.
|
||||
|
||||
// Couldn't retrieve an analytics ID from user settings, so create one and set it on the server.
|
||||
// Note there's a race condition here - if two devices do these steps at the same time, last write
|
||||
// wins, and the first writer will send tracking with an ID that doesn't match the one on the server
|
||||
// until the next time account data is refreshed and this function is called (most likely on next
|
||||
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
|
||||
const accountDataAnalyticsId = analyticsIdGenerator();
|
||||
await this.setAccountAnalyticsId(accountDataAnalyticsId);
|
||||
analyticsID = await this.hashedEcAnalyticsId(accountDataAnalyticsId);
|
||||
}
|
||||
} catch (e) {
|
||||
// The above could fail due to network requests, but not essential to starting the application,
|
||||
// so swallow it.
|
||||
logger.log("Unable to identify user for tracking" + e.toString());
|
||||
}
|
||||
if (analyticsID) {
|
||||
this.posthog.identify(analyticsID);
|
||||
} else {
|
||||
logger.info(
|
||||
"No analyticsID is availble. Should not try to setup posthog"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAnalyticsId() {
|
||||
const client: MatrixClient = window.matrixclient;
|
||||
let accountAnalyticsId;
|
||||
if (widget) {
|
||||
accountAnalyticsId = getUrlParams().analyticsID;
|
||||
} else {
|
||||
const accountData = await client.getAccountDataFromServer(
|
||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE
|
||||
);
|
||||
accountAnalyticsId = accountData?.id;
|
||||
}
|
||||
if (accountAnalyticsId) {
|
||||
// we dont just use the element web analytics ID because that would allow to associate
|
||||
// users between the two posthog instances. By using a hash from the username and the element web analytics id
|
||||
// it is not possible to conclude the element web posthog user id from the element call user id and vice versa.
|
||||
return await this.hashedEcAnalyticsId(accountAnalyticsId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async hashedEcAnalyticsId(accountAnalyticsId: string): Promise<string> {
|
||||
const client: MatrixClient = window.matrixclient;
|
||||
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
|
||||
const bufferForPosthogId = await crypto.subtle.digest(
|
||||
"sha-256",
|
||||
Buffer.from(posthogIdMaterial, "utf-8")
|
||||
);
|
||||
const view = new Int32Array(bufferForPosthogId);
|
||||
return Array.from(view)
|
||||
.map((b) => Math.abs(b).toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
async setAccountAnalyticsId(analyticsID: string) {
|
||||
if (!widget) {
|
||||
const client = window.matrixclient;
|
||||
|
||||
// the analytics ID only needs to be set in the standalone version.
|
||||
const accountData = await client.getAccountDataFromServer(
|
||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE
|
||||
);
|
||||
await client.setAccountData(
|
||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||
Object.assign({ id: analyticsID }, accountData)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getAnonymity(): Anonymity {
|
||||
return this.anonymity;
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
if (this.enabled) {
|
||||
this.posthog.reset();
|
||||
}
|
||||
this.setAnonymity(Anonymity.Disabled);
|
||||
}
|
||||
|
||||
public updateSuperProperties() {
|
||||
// Update super properties in posthog with our platform (app version, platform).
|
||||
// These properties will be subsequently passed in every event.
|
||||
//
|
||||
// This only needs to be done once per page lifetime. Note that getPlatformProperties
|
||||
this.platformSuperProperties = PosthogAnalytics.getPlatformProperties();
|
||||
this.registerSuperProperties({
|
||||
...this.platformSuperProperties,
|
||||
registrationType:
|
||||
this.registrationType == RegistrationType.Guest
|
||||
? "Guest"
|
||||
: "Registered",
|
||||
});
|
||||
}
|
||||
|
||||
private userRegisteredInThisSession(): boolean {
|
||||
// only if the signup end got tracked the end time is set. Otherwise its default value is Date(0).
|
||||
return this.eventSignup.getSignupEndTime() > new Date(0);
|
||||
}
|
||||
|
||||
public async updateAnonymityAndIdentifyUser(
|
||||
pseudonymousOptIn: boolean
|
||||
): Promise<void> {
|
||||
// Update this.anonymity based on the user's analytics opt-in settings
|
||||
const anonymity = pseudonymousOptIn
|
||||
? Anonymity.Pseudonymous
|
||||
: Anonymity.Disabled;
|
||||
this.setAnonymity(anonymity);
|
||||
|
||||
if (anonymity === Anonymity.Pseudonymous) {
|
||||
this.setRegistrationType(
|
||||
window.matrixclient.isGuest() || window.isPasswordlessUser
|
||||
? RegistrationType.Guest
|
||||
: RegistrationType.Registered
|
||||
);
|
||||
// store the promise to await posthog-tracking-events until the identification is done.
|
||||
this.identificationPromise = this.identifyUser(
|
||||
PosthogAnalytics.getRandomAnalyticsId
|
||||
);
|
||||
await this.identificationPromise;
|
||||
if (this.userRegisteredInThisSession()) {
|
||||
this.eventSignup.track();
|
||||
}
|
||||
}
|
||||
|
||||
if (anonymity !== Anonymity.Disabled) {
|
||||
this.updateSuperProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public async trackEvent<E extends IPosthogEvent>(
|
||||
{ eventName, ...properties }: E,
|
||||
options?: CaptureOptions
|
||||
): Promise<void> {
|
||||
if (this.identificationPromise) {
|
||||
// only make calls to posthog after the identificaion is done
|
||||
await this.identificationPromise;
|
||||
}
|
||||
if (
|
||||
this.anonymity == Anonymity.Disabled ||
|
||||
this.anonymity == Anonymity.Anonymous
|
||||
)
|
||||
return;
|
||||
this.capture(eventName, properties, options);
|
||||
}
|
||||
|
||||
public startListeningToSettingsChanges(): void {
|
||||
// Listen to account data changes from sync so we can observe changes to relevant flags and update.
|
||||
// This is called -
|
||||
// * On page load, when the account data is first received by sync
|
||||
// * On login
|
||||
// * When another device changes account data
|
||||
// * When the user changes their preferences on this device
|
||||
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
||||
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
||||
settingsBus.on("opt-in-analytics", (optInAnalytics) => {
|
||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||
});
|
||||
}
|
||||
|
||||
public setRegistrationType(registrationType: RegistrationType): void {
|
||||
this.registrationType = registrationType;
|
||||
if (
|
||||
this.anonymity == Anonymity.Disabled ||
|
||||
this.anonymity == Anonymity.Anonymous
|
||||
)
|
||||
return;
|
||||
this.updateSuperProperties();
|
||||
}
|
||||
|
||||
// ----- Events
|
||||
|
||||
public eventCallEnded = new CallEndedTracker();
|
||||
public eventSignup = new SignupTracker();
|
||||
public eventCallStarted = new CallStartedTracker();
|
||||
public eventLogin = new LoginTracker();
|
||||
public eventMuteMicrophone = new MuteMicrophoneTracker();
|
||||
public eventMuteCamera = new MuteCameraTracker();
|
||||
}
|
||||
151
src/PosthogEvents.ts
Normal file
151
src/PosthogEvents.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
Copyright 2022 The 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 {
|
||||
IPosthogEvent,
|
||||
PosthogAnalytics,
|
||||
RegistrationType,
|
||||
} from "./PosthogAnalytics";
|
||||
|
||||
interface CallEnded extends IPosthogEvent {
|
||||
eventName: "CallEnded";
|
||||
callId: string;
|
||||
callParticipantsOnLeave: number;
|
||||
callParticipantsMax: number;
|
||||
callDuration: number;
|
||||
}
|
||||
|
||||
export class CallEndedTracker {
|
||||
private cache: { startTime: Date; maxParticipantsCount: number } = {
|
||||
startTime: new Date(0),
|
||||
maxParticipantsCount: 0,
|
||||
};
|
||||
|
||||
cacheStartCall(time: Date) {
|
||||
this.cache.startTime = time;
|
||||
}
|
||||
|
||||
cacheParticipantCountChanged(count: number) {
|
||||
this.cache.maxParticipantsCount = Math.max(
|
||||
count,
|
||||
this.cache.maxParticipantsCount
|
||||
);
|
||||
}
|
||||
|
||||
track(callId: string, callParticipantsNow: number, sendInstantly: boolean) {
|
||||
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
||||
{
|
||||
eventName: "CallEnded",
|
||||
callId: callId,
|
||||
callParticipantsMax: this.cache.maxParticipantsCount,
|
||||
callParticipantsOnLeave: callParticipantsNow,
|
||||
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
|
||||
},
|
||||
{ send_instantly: sendInstantly }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface CallStarted extends IPosthogEvent {
|
||||
eventName: "CallStarted";
|
||||
callId: string;
|
||||
}
|
||||
|
||||
export class CallStartedTracker {
|
||||
track(callId: string) {
|
||||
PosthogAnalytics.instance.trackEvent<CallStarted>({
|
||||
eventName: "CallStarted",
|
||||
callId: callId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface Signup extends IPosthogEvent {
|
||||
eventName: "Signup";
|
||||
signupDuration: number;
|
||||
}
|
||||
|
||||
export class SignupTracker {
|
||||
private cache: { signupStart: Date; signupEnd: Date } = {
|
||||
signupStart: new Date(0),
|
||||
signupEnd: new Date(0),
|
||||
};
|
||||
|
||||
cacheSignupStart(time: Date) {
|
||||
this.cache.signupStart = time;
|
||||
}
|
||||
|
||||
getSignupEndTime() {
|
||||
return this.cache.signupEnd;
|
||||
}
|
||||
|
||||
cacheSignupEnd(time: Date) {
|
||||
this.cache.signupEnd = time;
|
||||
}
|
||||
|
||||
track() {
|
||||
PosthogAnalytics.instance.trackEvent<Signup>({
|
||||
eventName: "Signup",
|
||||
signupDuration: Date.now() - this.cache.signupStart.getTime(),
|
||||
});
|
||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Registered);
|
||||
}
|
||||
}
|
||||
|
||||
interface Login extends IPosthogEvent {
|
||||
eventName: "Login";
|
||||
}
|
||||
|
||||
export class LoginTracker {
|
||||
track() {
|
||||
PosthogAnalytics.instance.trackEvent<Login>({
|
||||
eventName: "Login",
|
||||
});
|
||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Registered);
|
||||
}
|
||||
}
|
||||
|
||||
interface MuteMicrophone {
|
||||
eventName: "MuteMicrophone";
|
||||
targetMuteState: "mute" | "unmute";
|
||||
callId: string;
|
||||
}
|
||||
|
||||
export class MuteMicrophoneTracker {
|
||||
track(targetIsMute: boolean, callId: string) {
|
||||
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
|
||||
eventName: "MuteMicrophone",
|
||||
targetMuteState: targetIsMute ? "mute" : "unmute",
|
||||
callId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface MuteCamera {
|
||||
eventName: "MuteCamera";
|
||||
targetMuteState: "mute" | "unmute";
|
||||
callId: string;
|
||||
}
|
||||
|
||||
export class MuteCameraTracker {
|
||||
track(targetIsMute: boolean, callId: string) {
|
||||
PosthogAnalytics.instance.trackEvent<MuteCamera>({
|
||||
eventName: "MuteCamera",
|
||||
targetMuteState: targetIsMute ? "mute" : "unmute",
|
||||
callId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
||||
import { FieldRow, InputField } from "./input/Input";
|
||||
import { usePageTitle } from "./usePageTitle";
|
||||
|
||||
export function SequenceDiagramViewerPage() {
|
||||
usePageTitle("Inspector");
|
||||
|
||||
const [debugLog, setDebugLog] = useState();
|
||||
const [selectedUserId, setSelectedUserId] = useState();
|
||||
const onChangeDebugLog = useCallback((e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
e.target.files[0].text().then((text) => {
|
||||
setDebugLog(JSON.parse(text));
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
id="debugLog"
|
||||
name="debugLog"
|
||||
label="Debug Log"
|
||||
onChange={onChangeDebugLog}
|
||||
/>
|
||||
</FieldRow>
|
||||
{debugLog && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={debugLog.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={debugLog.remoteUserIds}
|
||||
events={debugLog.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
src/SequenceDiagramViewerPage.tsx
Normal file
69
src/SequenceDiagramViewerPage.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
SequenceDiagramViewer,
|
||||
SequenceDiagramMatrixEvent,
|
||||
} from "./room/GroupCallInspector";
|
||||
import { FieldRow, InputField } from "./input/Input";
|
||||
import { usePageTitle } from "./usePageTitle";
|
||||
|
||||
interface DebugLog {
|
||||
localUserId: string;
|
||||
eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
||||
remoteUserIds: string[];
|
||||
}
|
||||
|
||||
export function SequenceDiagramViewerPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Inspector"));
|
||||
|
||||
const [debugLog, setDebugLog] = useState<DebugLog>();
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||
const onChangeDebugLog = useCallback((e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
e.target.files[0].text().then((text: string) => {
|
||||
setDebugLog(JSON.parse(text));
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
id="debugLog"
|
||||
name="debugLog"
|
||||
label={t("Debug log")}
|
||||
onChange={onChangeDebugLog}
|
||||
/>
|
||||
</FieldRow>
|
||||
{debugLog && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={debugLog.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={debugLog.remoteUserIds}
|
||||
events={debugLog.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
||||
import { FocusableProvider } from "@react-aria/focus";
|
||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import styles from "./Tooltip.module.css";
|
||||
import classNames from "classnames";
|
||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||
|
||||
export const Tooltip = forwardRef(
|
||||
({ position, state, className, ...props }, ref) => {
|
||||
let { tooltipProps } = useTooltip(props, state);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.tooltip, className)}
|
||||
{...mergeProps(props, tooltipProps)}
|
||||
ref={ref}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
||||
const tooltipState = useTooltipTriggerState(rest);
|
||||
const triggerRef = useObjectRef(ref);
|
||||
const overlayRef = useRef();
|
||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||
rest,
|
||||
tooltipState,
|
||||
triggerRef
|
||||
);
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
placement: rest.placement || "top",
|
||||
targetRef: triggerRef,
|
||||
overlayRef,
|
||||
isOpen: tooltipState.isOpen,
|
||||
offset: 5,
|
||||
});
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"TooltipTrigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [tooltipTrigger, tooltip] = children;
|
||||
|
||||
return (
|
||||
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
||||
{<tooltipTrigger.type {...mergeProps(tooltipTrigger.props, rest)} />}
|
||||
{tooltipState.isOpen && (
|
||||
<OverlayContainer>
|
||||
<Tooltip
|
||||
state={tooltipState}
|
||||
{...mergeProps(tooltipProps, overlayProps)}
|
||||
ref={overlayRef}
|
||||
>
|
||||
{tooltip()}
|
||||
</Tooltip>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</FocusableProvider>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipTrigger.defaultProps = {
|
||||
delay: 250,
|
||||
};
|
||||
@@ -1,12 +1,30 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.tooltip {
|
||||
background-color: var(--bgColor2);
|
||||
background-color: var(--system);
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
color: var(--textColor1);
|
||||
padding: 10px;
|
||||
color: var(--primary-content);
|
||||
border-radius: 8px;
|
||||
max-width: 135px;
|
||||
width: max-content;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
114
src/Tooltip.tsx
Normal file
114
src/Tooltip.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
TooltipTriggerState,
|
||||
useTooltipTriggerState,
|
||||
} from "@react-stately/tooltip";
|
||||
import { FocusableProvider } from "@react-aria/focus";
|
||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import classNames from "classnames";
|
||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||
import { Placement } from "@react-types/overlays";
|
||||
|
||||
import styles from "./Tooltip.module.css";
|
||||
|
||||
interface TooltipProps {
|
||||
className?: string;
|
||||
state: TooltipTriggerState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
||||
(
|
||||
{ state, className, children, ...rest }: TooltipProps,
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const { tooltipProps } = useTooltip(rest, state);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.tooltip, className)}
|
||||
{...mergeProps(rest, tooltipProps)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface TooltipTriggerProps {
|
||||
children: ReactElement;
|
||||
placement?: Placement;
|
||||
delay?: number;
|
||||
tooltip: () => string;
|
||||
}
|
||||
|
||||
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
||||
(
|
||||
{ children, placement, tooltip, ...rest }: TooltipTriggerProps,
|
||||
ref: ForwardedRef<HTMLElement>
|
||||
) => {
|
||||
const tooltipTriggerProps = { delay: 250, ...rest };
|
||||
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
|
||||
const triggerRef = useObjectRef<HTMLElement>(ref);
|
||||
const overlayRef = useRef();
|
||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||
tooltipTriggerProps,
|
||||
tooltipState,
|
||||
triggerRef
|
||||
);
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
placement: placement || "top",
|
||||
targetRef: triggerRef,
|
||||
overlayRef,
|
||||
isOpen: tooltipState.isOpen,
|
||||
offset: 12,
|
||||
});
|
||||
|
||||
return (
|
||||
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
||||
<children.type
|
||||
{...mergeProps<typeof children.props | typeof rest>(
|
||||
children.props,
|
||||
rest
|
||||
)}
|
||||
/>
|
||||
{tooltipState.isOpen && (
|
||||
<OverlayContainer>
|
||||
<Tooltip
|
||||
state={tooltipState}
|
||||
ref={overlayRef}
|
||||
{...mergeProps(tooltipProps, overlayProps)}
|
||||
>
|
||||
{tooltip()}
|
||||
</Tooltip>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</FocusableProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
41
src/TranslatedError.ts
Normal file
41
src/TranslatedError.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import i18n from "i18next";
|
||||
|
||||
/**
|
||||
* An error with messages in both English and the user's preferred language.
|
||||
*/
|
||||
// Abstract to force consumers to use the function below rather than calling the
|
||||
// constructor directly
|
||||
export abstract class TranslatedError extends Error {
|
||||
/**
|
||||
* The error message in the user's preferred language.
|
||||
*/
|
||||
public readonly translatedMessage: string;
|
||||
|
||||
public constructor(messageKey: string, translationFn: typeof i18n.t) {
|
||||
super(translationFn(messageKey, { lng: "en-GB" }));
|
||||
this.translatedMessage = translationFn(messageKey);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslatedErrorImpl extends TranslatedError {}
|
||||
|
||||
// i18next-parser can't detect calls to a constructor, so we expose a bare
|
||||
// function instead
|
||||
export const translatedError = (messageKey: string, t: typeof i18n.t) =>
|
||||
new TranslatedErrorImpl(messageKey, t);
|
||||
148
src/UrlParams.ts
Normal file
148
src/UrlParams.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
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;
|
||||
/**
|
||||
* The fonts which the interface should use, if not empty.
|
||||
*/
|
||||
fonts: string[];
|
||||
/**
|
||||
* The factor by which to scale the interface's font size.
|
||||
*/
|
||||
fontScale: number | null;
|
||||
/**
|
||||
* The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web.
|
||||
*/
|
||||
analyticsID: 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);
|
||||
|
||||
const fontScale = parseFloat(getParam("fontScale") ?? "");
|
||||
|
||||
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"),
|
||||
fonts: getAllParams("font"),
|
||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||
analyticsID: getParam("analyticsID"),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
};
|
||||
@@ -1,22 +1,38 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.menuIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.userButton svg * {
|
||||
fill: var(--textColor1);
|
||||
fill: var(--primary-content);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-caption);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 15px;
|
||||
font-size: var(--font-size-body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
import React, { useMemo } from "react";
|
||||
/*
|
||||
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, useMemo } from "react";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, LinkButton } from "./button";
|
||||
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
||||
import { Menu } from "./Menu";
|
||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { TooltipTrigger } from "./Tooltip";
|
||||
import { Avatar, Size } from "./Avatar";
|
||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
||||
import styles from "./UserMenu.module.css";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Body } from "./typography/Typography";
|
||||
import styles from "./UserMenu.module.css";
|
||||
|
||||
interface UserMenuProps {
|
||||
preventNavigation: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isPasswordlessUser: boolean;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
onAction: (value: string) => void;
|
||||
}
|
||||
|
||||
export function UserMenu({
|
||||
preventNavigation,
|
||||
@@ -19,7 +46,8 @@ export function UserMenu({
|
||||
displayName,
|
||||
avatarUrl,
|
||||
onAction,
|
||||
}) {
|
||||
}: UserMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const items = useMemo(() => {
|
||||
@@ -35,7 +63,7 @@ export function UserMenu({
|
||||
if (isPasswordlessUser && !preventNavigation) {
|
||||
arr.push({
|
||||
key: "login",
|
||||
label: "Sign In",
|
||||
label: t("Sign in"),
|
||||
icon: LoginIcon,
|
||||
});
|
||||
}
|
||||
@@ -43,14 +71,16 @@ export function UserMenu({
|
||||
if (!isPasswordlessUser && !preventNavigation) {
|
||||
arr.push({
|
||||
key: "logout",
|
||||
label: "Sign Out",
|
||||
label: t("Sign out"),
|
||||
icon: LogoutIcon,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation]);
|
||||
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation, t]);
|
||||
|
||||
const tooltip = useCallback(() => t("Profile"), [t]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
@@ -62,11 +92,11 @@ export function UserMenu({
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger placement="bottom left">
|
||||
<TooltipTrigger tooltip={tooltip} placement="bottom left">
|
||||
<Button variant="icon" className={styles.userButton}>
|
||||
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||
<Avatar
|
||||
size="sm"
|
||||
size={Size.SM}
|
||||
className={styles.avatar}
|
||||
src={avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
@@ -75,12 +105,11 @@ export function UserMenu({
|
||||
<UserIcon />
|
||||
)}
|
||||
</Button>
|
||||
{() => "Profile"}
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
<Menu {...props} label="User menu" onAction={onAction}>
|
||||
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label }) => (
|
||||
<Item key={key} textValue={label} className={styles.menuItem}>
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon width={24} height={24} className={styles.menuIcon} />
|
||||
<Body overflowEllipsis>{label}</Body>
|
||||
</Item>
|
||||
@@ -1,12 +1,33 @@
|
||||
/*
|
||||
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 } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
import { useClient } from "./ClientContext";
|
||||
import { useProfile } from "./profile/useProfile";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { ProfileModal } from "./profile/ProfileModal";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
export function UserMenuContainer({ preventNavigation }) {
|
||||
interface Props {
|
||||
preventNavigation?: boolean;
|
||||
}
|
||||
|
||||
export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||
@@ -15,7 +36,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onAction = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
modalState.open();
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.logo {
|
||||
max-width: 300px;
|
||||
margin: 80px 0;
|
||||
@@ -36,7 +52,7 @@
|
||||
|
||||
.formContainer h4 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
font-size: var(--font-size-subtitle);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -48,7 +64,7 @@
|
||||
.formContainer button {
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-size: var(--font-size-body);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -61,11 +77,11 @@
|
||||
|
||||
.authLinks {
|
||||
margin-bottom: 100px;
|
||||
font-size: 15px;
|
||||
font-size: var(--font-size-body);
|
||||
}
|
||||
|
||||
.authLinks a {
|
||||
color: #0dbd8b;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
Copyright 2021-2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,59 +14,60 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useState, useMemo } from "react";
|
||||
import React, { FC, FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||
import { Config } from "../config/Config";
|
||||
|
||||
export function LoginPage() {
|
||||
usePageTitle("Login");
|
||||
export const LoginPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Login"));
|
||||
|
||||
const [_, login] = useInteractiveLogin();
|
||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
||||
const usernameRef = useRef();
|
||||
const passwordRef = useRef();
|
||||
const { setClient } = useClient();
|
||||
const login = useInteractiveLogin();
|
||||
const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable
|
||||
const usernameRef = useRef<HTMLInputElement>();
|
||||
const passwordRef = useRef<HTMLInputElement>();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
// TODO: Handle hitting login page with authenticated client
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
(e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
.then(() => {
|
||||
.then(([client, session]) => {
|
||||
setClient(client, session);
|
||||
|
||||
if (location.state && location.state.from) {
|
||||
history.push(location.state.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
PosthogAnalytics.instance.eventLogin.track();
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[login, location, history, homeserver]
|
||||
[login, location, history, homeserver, setClient]
|
||||
);
|
||||
|
||||
const homeserverHost = useMemo(() => {
|
||||
try {
|
||||
return new URL(homeserver).host;
|
||||
} catch (_error) {
|
||||
return defaultHomeserverHost;
|
||||
}
|
||||
}, [homeserver]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
@@ -81,30 +82,30 @@ export function LoginPage() {
|
||||
<InputField
|
||||
type="text"
|
||||
ref={usernameRef}
|
||||
placeholder="Username"
|
||||
label="Username"
|
||||
placeholder={t("Username")}
|
||||
label={t("Username")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
prefix="@"
|
||||
suffix={`:${homeserverHost}`}
|
||||
suffix={`:${Config.defaultServerName()}`}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="password"
|
||||
ref={passwordRef}
|
||||
placeholder="Password"
|
||||
label="Password"
|
||||
placeholder={t("Password")}
|
||||
label={t("Password")}
|
||||
/>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
<ErrorMessage error={error} />
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Logging in..." : "Login"}
|
||||
{loading ? t("Logging in…") : t("Login")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
@@ -112,13 +113,15 @@ export function LoginPage() {
|
||||
<div className={styles.authLinks}>
|
||||
<p>Not registered yet?</p>
|
||||
<p>
|
||||
<Link to="/register">Create an account</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
<Trans>
|
||||
<Link to="/register">Create an account</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
Copyright 2021-2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,12 +14,23 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { captureException } from "@sentry/react";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { defaultHomeserverHost } from "../matrix-utils";
|
||||
import { useInteractiveRegistration } from "./useInteractiveRegistration";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||
@@ -27,69 +38,74 @@ import { LoadingView } from "../FullScreenView";
|
||||
import { useRecaptcha } from "./useRecaptcha";
|
||||
import { Caption, Link } from "../typography/Typography";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||
import { Config } from "../config/Config";
|
||||
|
||||
export function RegisterPage() {
|
||||
usePageTitle("Register");
|
||||
export const RegisterPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Register"));
|
||||
|
||||
const { loading, isAuthenticated, isPasswordlessUser, client } = useClient();
|
||||
const confirmPasswordRef = useRef();
|
||||
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
||||
useClient();
|
||||
const confirmPasswordRef = useRef<HTMLInputElement>();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [registering, setRegistering] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [error, setError] = useState<Error>();
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
||||
const [privacyPolicyUrl, recaptchaKey, register] =
|
||||
useInteractiveRegistration();
|
||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||
|
||||
const onSubmitRegisterForm = useCallback(
|
||||
(e) => {
|
||||
(e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const userName = data.get("userName");
|
||||
const password = data.get("password");
|
||||
const passwordConfirmation = data.get("passwordConfirmation");
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
const userName = data.get("userName") as string;
|
||||
const password = data.get("password") as string;
|
||||
const passwordConfirmation = data.get("passwordConfirmation") as string;
|
||||
|
||||
if (password !== passwordConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (password !== passwordConfirmation) return;
|
||||
|
||||
async function submit() {
|
||||
const submit = async () => {
|
||||
setRegistering(true);
|
||||
|
||||
let roomIds;
|
||||
|
||||
if (client && isPasswordlessUser) {
|
||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||
roomIds = Array.from(groupCalls).map(
|
||||
(groupCall) => groupCall.room.roomId
|
||||
);
|
||||
}
|
||||
|
||||
const recaptchaResponse = await execute();
|
||||
const newClient = await register(
|
||||
const [newClient, session] = await register(
|
||||
userName,
|
||||
password,
|
||||
userName,
|
||||
recaptchaResponse
|
||||
);
|
||||
|
||||
if (roomIds) {
|
||||
for (const roomId of roomIds) {
|
||||
if (client && isPasswordlessUser) {
|
||||
// Migrate the user's rooms
|
||||
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
|
||||
const roomId = groupCall.room.roomId;
|
||||
|
||||
try {
|
||||
await newClient.joinRoom(roomId);
|
||||
} catch (error) {
|
||||
console.warn(`Couldn't join room ${roomId}`, error);
|
||||
if (error.errcode === "M_LIMIT_EXCEEDED") {
|
||||
await sleep(error.data.retry_after_ms);
|
||||
await newClient.joinRoom(roomId);
|
||||
} else {
|
||||
captureException(error);
|
||||
console.error(`Couldn't join room ${roomId}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setClient(newClient, session);
|
||||
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
|
||||
};
|
||||
|
||||
submit()
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.push(location.state.from);
|
||||
if (location.state?.from) {
|
||||
history.push(location.state?.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
@@ -100,29 +116,36 @@ export function RegisterPage() {
|
||||
reset();
|
||||
});
|
||||
},
|
||||
[register, location, history, isPasswordlessUser, reset, execute, client]
|
||||
[
|
||||
register,
|
||||
location,
|
||||
history,
|
||||
isPasswordlessUser,
|
||||
reset,
|
||||
execute,
|
||||
client,
|
||||
setClient,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!confirmPasswordRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
||||
confirmPasswordRef.current.setCustomValidity("Passwords must match");
|
||||
confirmPasswordRef.current?.setCustomValidity(t("Passwords must match"));
|
||||
} else {
|
||||
confirmPasswordRef.current.setCustomValidity("");
|
||||
confirmPasswordRef.current?.setCustomValidity("");
|
||||
}
|
||||
}, [password, passwordConfirmation]);
|
||||
}, [password, passwordConfirmation, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||
history.push("/");
|
||||
}
|
||||
}, [history, isAuthenticated, isPasswordlessUser, registering]);
|
||||
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
} else {
|
||||
PosthogAnalytics.instance.eventSignup.cacheSignupStart(new Date());
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -137,12 +160,12 @@ export function RegisterPage() {
|
||||
<InputField
|
||||
type="text"
|
||||
name="userName"
|
||||
placeholder="Username"
|
||||
label="Username"
|
||||
placeholder={t("Username")}
|
||||
label={t("Username")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
prefix="@"
|
||||
suffix={`:${defaultHomeserverHost}`}
|
||||
suffix={`:${Config.defaultServerName()}`}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
@@ -150,10 +173,12 @@ export function RegisterPage() {
|
||||
required
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(e.target.value)
|
||||
}
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
label="Password"
|
||||
placeholder={t("Password")}
|
||||
label={t("Password")}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
@@ -161,50 +186,56 @@ export function RegisterPage() {
|
||||
required
|
||||
type="password"
|
||||
name="passwordConfirmation"
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setPasswordConfirmation(e.target.value)
|
||||
}
|
||||
value={passwordConfirmation}
|
||||
placeholder="Confirm Password"
|
||||
label="Confirm Password"
|
||||
placeholder={t("Confirm password")}
|
||||
label={t("Confirm password")}
|
||||
ref={confirmPasswordRef}
|
||||
/>
|
||||
</FieldRow>
|
||||
<Caption>
|
||||
This site is protected by ReCAPTCHA and the Google{" "}
|
||||
<Link href="https://www.google.com/policies/privacy/">
|
||||
Privacy Policy
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://policies.google.com/terms">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
apply.
|
||||
<br />
|
||||
By clicking "Register", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
<Trans>
|
||||
This site is protected by ReCAPTCHA and the Google{" "}
|
||||
<Link href="https://www.google.com/policies/privacy/">
|
||||
Privacy Policy
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://policies.google.com/terms">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
apply.
|
||||
<br />
|
||||
By clicking "Register", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
</Trans>
|
||||
</Caption>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
<ErrorMessage error={error} />
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow>
|
||||
<Button type="submit" disabled={registering}>
|
||||
{registering ? "Registering..." : "Register"}
|
||||
{registering ? t("Registering…") : t("Register")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
<div id={recaptchaId} />
|
||||
</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>
|
||||
<Trans>
|
||||
<p>Already have an account?</p>
|
||||
<p>
|
||||
<Link to="/login">Log in</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
</p>
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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 {
|
||||
uniqueNamesGenerator,
|
||||
adjectives,
|
||||
@@ -126,12 +142,11 @@ const elements = [
|
||||
"oganesson",
|
||||
];
|
||||
|
||||
export function generateRandomName(config) {
|
||||
export function generateRandomName(): string {
|
||||
return uniqueNamesGenerator({
|
||||
dictionaries: [colors, adjectives, animals, elements],
|
||||
style: "lowerCase",
|
||||
length: 3,
|
||||
separator: "-",
|
||||
...config,
|
||||
});
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { initClient, defaultHomeserver } from "../matrix-utils";
|
||||
|
||||
export function useInteractiveLogin() {
|
||||
const { setClient } = useClient();
|
||||
const [state, setState] = useState({ loading: false });
|
||||
|
||||
const auth = useCallback(async (homeserver, username, password) => {
|
||||
const authClient = matrix.createClient(homeserver);
|
||||
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient,
|
||||
busyChanged(loading) {
|
||||
setState((prev) => ({ ...prev, loading }));
|
||||
},
|
||||
async doRequest(_auth, _background) {
|
||||
return authClient.login("m.login.password", {
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { user_id, access_token, device_id } =
|
||||
await interactiveAuth.attemptAuth();
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
setClient(client, { user_id, access_token, device_id });
|
||||
|
||||
return client;
|
||||
}, []);
|
||||
|
||||
return [state, auth];
|
||||
}
|
||||
72
src/auth/useInteractiveLogin.ts
Normal file
72
src/auth/useInteractiveLogin.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2022 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 { useCallback } from "react";
|
||||
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { initClient } from "../matrix-utils";
|
||||
import { Session } from "../ClientContext";
|
||||
|
||||
export const useInteractiveLogin = () =>
|
||||
useCallback<
|
||||
(
|
||||
homeserver: string,
|
||||
username: string,
|
||||
password: string
|
||||
) => Promise<[MatrixClient, Session]>
|
||||
>(async (homeserver: string, username: string, password: string) => {
|
||||
const authClient = createClient({ baseUrl: homeserver });
|
||||
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient,
|
||||
doRequest: () =>
|
||||
authClient.login("m.login.password", {
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
password,
|
||||
}),
|
||||
stateUpdated: null,
|
||||
requestEmailToken: null,
|
||||
});
|
||||
|
||||
// XXX: This claims to return an IAuthData which contains none of these
|
||||
// things - the js-sdk types may be wrong?
|
||||
/* eslint-disable camelcase,@typescript-eslint/no-explicit-any */
|
||||
const { user_id, access_token, device_id } =
|
||||
(await interactiveAuth.attemptAuth()) as any;
|
||||
const session = {
|
||||
user_id,
|
||||
access_token,
|
||||
device_id,
|
||||
passwordlessUser: false,
|
||||
};
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: homeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
false
|
||||
);
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
return [client, session];
|
||||
}, []);
|
||||
@@ -1,96 +0,0 @@
|
||||
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { initClient, defaultHomeserver } from "../matrix-utils";
|
||||
|
||||
export function useInteractiveRegistration() {
|
||||
const { setClient } = useClient();
|
||||
const [state, setState] = useState({ privacyPolicyUrl: "#", loading: false });
|
||||
|
||||
const authClientRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
authClientRef.current = matrix.createClient(defaultHomeserver);
|
||||
|
||||
authClientRef.current.registerRequest({}).catch((error) => {
|
||||
const privacyPolicyUrl =
|
||||
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url;
|
||||
|
||||
const recaptchaKey = error.data?.params["m.login.recaptcha"]?.public_key;
|
||||
|
||||
if (privacyPolicyUrl || recaptchaKey) {
|
||||
setState((prev) => ({ ...prev, privacyPolicyUrl, recaptchaKey }));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const register = useCallback(
|
||||
async (
|
||||
username,
|
||||
password,
|
||||
displayName,
|
||||
recaptchaResponse,
|
||||
passwordlessUser
|
||||
) => {
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClientRef.current,
|
||||
busyChanged(loading) {
|
||||
setState((prev) => ({ ...prev, loading }));
|
||||
},
|
||||
async doRequest(auth, _background) {
|
||||
return authClientRef.current.registerRequest({
|
||||
username,
|
||||
password,
|
||||
auth: auth || undefined,
|
||||
});
|
||||
},
|
||||
stateUpdated(nextStage, status) {
|
||||
if (status.error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (nextStage === "m.login.terms") {
|
||||
interactiveAuth.submitAuthDict({
|
||||
type: "m.login.terms",
|
||||
});
|
||||
} else if (nextStage === "m.login.recaptcha") {
|
||||
interactiveAuth.submitAuthDict({
|
||||
type: "m.login.recaptcha",
|
||||
response: recaptchaResponse,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { user_id, access_token, device_id } =
|
||||
await interactiveAuth.attemptAuth();
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
await client.setDisplayName(displayName);
|
||||
|
||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
||||
|
||||
if (passwordlessUser) {
|
||||
session.tempPassword = password;
|
||||
}
|
||||
|
||||
setClient(client, session);
|
||||
|
||||
const user = client.getUser(client.getUserId());
|
||||
|
||||
user.setRawDisplayName(displayName);
|
||||
user.setDisplayName(displayName);
|
||||
|
||||
return client;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return [state, register];
|
||||
}
|
||||
130
src/auth/useInteractiveRegistration.ts
Normal file
130
src/auth/useInteractiveRegistration.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
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 { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { initClient } from "../matrix-utils";
|
||||
import { Session } from "../ClientContext";
|
||||
import { Config } from "../config/Config";
|
||||
|
||||
export const useInteractiveRegistration = (): [
|
||||
string,
|
||||
string,
|
||||
(
|
||||
username: string,
|
||||
password: string,
|
||||
displayName: string,
|
||||
recaptchaResponse: string,
|
||||
passwordlessUser?: boolean
|
||||
) => Promise<[MatrixClient, Session]>
|
||||
] => {
|
||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>();
|
||||
const [recaptchaKey, setRecaptchaKey] = useState<string>();
|
||||
|
||||
const authClient = useRef<MatrixClient>();
|
||||
if (!authClient.current) {
|
||||
authClient.current = createClient({
|
||||
baseUrl: Config.defaultHomeserverUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
authClient.current.registerRequest({}).catch((error) => {
|
||||
setPrivacyPolicyUrl(
|
||||
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
|
||||
);
|
||||
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const register = useCallback(
|
||||
async (
|
||||
username: string,
|
||||
password: string,
|
||||
displayName: string,
|
||||
recaptchaResponse: string,
|
||||
passwordlessUser?: boolean
|
||||
): Promise<[MatrixClient, Session]> => {
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient.current,
|
||||
doRequest: (auth) =>
|
||||
authClient.current.registerRequest({
|
||||
username,
|
||||
password,
|
||||
auth: auth || undefined,
|
||||
}),
|
||||
stateUpdated: (nextStage, status) => {
|
||||
if (status.error) {
|
||||
throw new Error(status.error);
|
||||
}
|
||||
|
||||
if (nextStage === "m.login.terms") {
|
||||
interactiveAuth.submitAuthDict({
|
||||
type: "m.login.terms",
|
||||
});
|
||||
} else if (nextStage === "m.login.recaptcha") {
|
||||
interactiveAuth.submitAuthDict({
|
||||
type: "m.login.recaptcha",
|
||||
response: recaptchaResponse,
|
||||
});
|
||||
}
|
||||
},
|
||||
requestEmailToken: null,
|
||||
});
|
||||
|
||||
// XXX: This claims to return an IAuthData which contains none of these
|
||||
// things - the js-sdk types may be wrong?
|
||||
/* eslint-disable camelcase,@typescript-eslint/no-explicit-any */
|
||||
const { user_id, access_token, device_id } =
|
||||
(await interactiveAuth.attemptAuth()) as any;
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: Config.defaultHomeserverUrl(),
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
await client.setDisplayName(displayName);
|
||||
|
||||
const session: Session = {
|
||||
user_id,
|
||||
device_id,
|
||||
access_token,
|
||||
passwordlessUser,
|
||||
};
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
if (passwordlessUser) {
|
||||
session.tempPassword = password;
|
||||
}
|
||||
|
||||
const user = client.getUser(client.getUserId());
|
||||
user.setRawDisplayName(displayName);
|
||||
user.setDisplayName(displayName);
|
||||
|
||||
return [client, session];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return [privacyPolicyUrl, recaptchaKey, register];
|
||||
};
|
||||
@@ -1,49 +1,66 @@
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
/*
|
||||
Copyright 2022 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 { useEffect, useCallback, useRef, useState } from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { translatedError } from "../TranslatedError";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mxOnRecaptchaLoaded: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
const RECAPTCHA_SCRIPT_URL =
|
||||
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
||||
|
||||
export function useRecaptcha(sitekey) {
|
||||
interface RecaptchaPromiseRef {
|
||||
resolve: (response: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
export const useRecaptcha = (sitekey: string) => {
|
||||
const { t } = useTranslation();
|
||||
const [recaptchaId] = useState(() => randomString(16));
|
||||
const promiseRef = useRef();
|
||||
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!sitekey) {
|
||||
return;
|
||||
}
|
||||
if (!sitekey) return;
|
||||
|
||||
const onRecaptchaLoaded = () => {
|
||||
if (!document.getElementById(recaptchaId)) {
|
||||
return;
|
||||
}
|
||||
if (!document.getElementById(recaptchaId)) return;
|
||||
|
||||
window.grecaptcha.render(recaptchaId, {
|
||||
sitekey,
|
||||
size: "invisible",
|
||||
callback: (response) => {
|
||||
if (promiseRef.current) {
|
||||
promiseRef.current.resolve(response);
|
||||
}
|
||||
},
|
||||
"error-callback": (error) => {
|
||||
if (promiseRef.current) {
|
||||
promiseRef.current.reject(error);
|
||||
}
|
||||
},
|
||||
callback: (response: string) => promiseRef.current?.resolve(response),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"error-callback": () => promiseRef.current?.reject(new Error()),
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
typeof window.grecaptcha !== "undefined" &&
|
||||
typeof window.grecaptcha.render === "function"
|
||||
) {
|
||||
if (typeof window.grecaptcha?.render === "function") {
|
||||
onRecaptchaLoaded();
|
||||
} else {
|
||||
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
||||
|
||||
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
||||
const scriptTag = document.createElement("script");
|
||||
const scriptTag = document.createElement("script") as HTMLScriptElement;
|
||||
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
||||
scriptTag.async = true;
|
||||
document.body.appendChild(scriptTag);
|
||||
@@ -58,14 +75,14 @@ export function useRecaptcha(sitekey) {
|
||||
|
||||
if (!window.grecaptcha) {
|
||||
console.log("Recaptcha not loaded");
|
||||
return Promise.reject(new Error("Recaptcha not loaded"));
|
||||
return Promise.reject(translatedError("Recaptcha not loaded", t));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const item of mutationsList) {
|
||||
if (item.target?.style?.visibility !== "visible") {
|
||||
reject(new Error("Recaptcha dismissed"));
|
||||
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
|
||||
reject(translatedError("Recaptcha dismissed", t));
|
||||
observer.disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +102,7 @@ export function useRecaptcha(sitekey) {
|
||||
|
||||
window.grecaptcha.execute();
|
||||
|
||||
const iframe = document.querySelector(
|
||||
const iframe = document.querySelector<HTMLIFrameElement>(
|
||||
'iframe[src*="recaptcha/api2/bframe"]'
|
||||
);
|
||||
|
||||
@@ -95,13 +112,11 @@ export function useRecaptcha(sitekey) {
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [recaptchaId, sitekey]);
|
||||
}, [sitekey, t]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.reset();
|
||||
}
|
||||
}, [recaptchaId]);
|
||||
window.grecaptcha?.reset();
|
||||
}, []);
|
||||
|
||||
return { execute, reset, recaptchaId };
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user