Compare commits
547 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33980511c | ||
|
|
42aeecd964 | ||
|
|
45dbaa968a | ||
|
|
db66700595 | ||
|
|
f93c022c27 | ||
|
|
84a92845c3 | ||
|
|
8731f83fb5 | ||
|
|
7b71e9b20f | ||
|
|
8a245533bb | ||
|
|
43e80dd9e7 | ||
|
|
9b93d45ea0 | ||
|
|
f1c050c327 | ||
|
|
99dbcffcaf | ||
|
|
57da08e6ff | ||
|
|
e1d6f99f25 | ||
|
|
956388807f | ||
|
|
9bd6f346e9 | ||
|
|
b1083baacf | ||
|
|
5b5c649b49 | ||
|
|
2346ad9b7e | ||
|
|
feeb9c4e7c | ||
|
|
0767a6f9dd | ||
|
|
77b139226b | ||
|
|
658185a119 | ||
|
|
37d9e48c0a | ||
|
|
a1c61fc1fd | ||
|
|
68a50146b1 | ||
|
|
effe892757 | ||
|
|
f5bc29a226 | ||
|
|
4189fc7e84 | ||
|
|
45dee7906d | ||
|
|
f7506ae9b7 | ||
|
|
7bfe3f9c47 | ||
|
|
2954290a48 | ||
|
|
1dbd0e76ad | ||
|
|
afc4c4ac4e | ||
|
|
368f69f52d | ||
|
|
de0a68fbcd | ||
|
|
d72b9a8d82 | ||
|
|
f34733bfb2 | ||
|
|
6d7b7de76e | ||
|
|
9b3835c166 | ||
|
|
54d903933b | ||
|
|
dc1e086ea2 | ||
|
|
a3b8dfcdf2 | ||
|
|
e6eb2e093c | ||
|
|
d4caa1585b | ||
|
|
363ea2e669 | ||
|
|
c25874ced5 | ||
|
|
7932d7a471 | ||
|
|
e42a83bbc4 | ||
|
|
cb5f7a3f84 | ||
|
|
99b3880afc | ||
|
|
18b5ae9d4a | ||
|
|
14a1ff7fe4 | ||
|
|
0d22ef2104 | ||
|
|
5ceda993d4 | ||
|
|
7038a76fc1 | ||
|
|
1a329966ba | ||
|
|
31decc6577 | ||
|
|
878c183548 | ||
|
|
4e295f7708 | ||
|
|
b2b233ae00 | ||
|
|
86ec677675 | ||
|
|
1104aa2412 | ||
|
|
d9575a0dcf | ||
|
|
7b4650f5f4 | ||
|
|
595eb85431 | ||
|
|
4e7e707a92 | ||
|
|
6186d1d7d2 | ||
|
|
9b493da519 | ||
|
|
b81889bf15 | ||
|
|
610e320031 | ||
|
|
983f8eb737 | ||
|
|
a2040bf4c2 | ||
|
|
d8d72d023c | ||
|
|
faf4d1b49b | ||
|
|
9045ba925a | ||
|
|
ff77fa2543 | ||
|
|
66d867b5c7 | ||
|
|
66ecb7c4e9 | ||
|
|
e01136e6bb | ||
|
|
ee6438a4bd | ||
|
|
c4c99c4bcb | ||
|
|
cef88e2894 | ||
|
|
88f4b889a1 | ||
|
|
e909ff5ad0 | ||
|
|
fcaa126147 | ||
|
|
b8af9a0733 | ||
|
|
97c294687e | ||
|
|
50e9e33922 | ||
|
|
2b74c2d9ce | ||
|
|
736aa95133 | ||
|
|
b39b3c072d | ||
|
|
d2a5baf403 | ||
|
|
e1090377f9 | ||
|
|
669b1403fc | ||
|
|
a4076cd528 | ||
|
|
877726dc3c | ||
|
|
c7a2c7110a | ||
|
|
efe9e6c2b3 | ||
|
|
9bdd5b0e58 | ||
|
|
fbf7b5d022 | ||
|
|
7ad84de9c2 | ||
|
|
bf94a5dcaf | ||
|
|
537341da3a | ||
|
|
247ed95976 | ||
|
|
4e9abfa3b0 | ||
|
|
d26de7d27f | ||
|
|
3891b042e3 | ||
|
|
821622f71c | ||
|
|
71dcc94166 | ||
|
|
1ea9432769 | ||
|
|
fa4b4eabdf | ||
|
|
39c30ebf56 | ||
|
|
3ef84c069c | ||
|
|
4ee6e450b7 | ||
|
|
50c373e091 | ||
|
|
5fe92ee30b | ||
|
|
c0d338a504 | ||
|
|
7859f3813e | ||
|
|
b4d2b8159b | ||
|
|
9d0e77adf7 | ||
|
|
afa7ae69d2 | ||
|
|
79506653eb | ||
|
|
f5fea48ccd | ||
|
|
54fe2aa7a3 | ||
|
|
3ff201562b | ||
|
|
e139ac6584 | ||
|
|
85210df28e | ||
|
|
0af116ce76 | ||
|
|
a09bb109fd | ||
|
|
c97185a50e | ||
|
|
50f7fedfa0 | ||
|
|
178c6496bd | ||
|
|
c5eb9f0b99 | ||
|
|
af4c1280f5 | ||
|
|
97ae11f656 | ||
|
|
e182dd50f2 | ||
|
|
43f98e6be6 | ||
|
|
70ba6c3c6b | ||
|
|
29a7376bc7 | ||
|
|
db02178fce | ||
|
|
1d69bef7f9 | ||
|
|
0a83a8804f | ||
|
|
5795e20865 | ||
|
|
4aba1c8b74 | ||
|
|
dc694d4ffe | ||
|
|
fafc56bb90 | ||
|
|
a83611c287 | ||
|
|
2cca320291 | ||
|
|
834582a870 | ||
|
|
2390b990c5 | ||
|
|
166046a4b1 | ||
|
|
f2dbe8abbe | ||
|
|
1a814713df | ||
|
|
fceb10e2df | ||
|
|
94323b3597 | ||
|
|
a8c5cb4821 | ||
|
|
6e32aad729 | ||
|
|
49f6249144 | ||
|
|
ab08b58ef5 | ||
|
|
ba9efc64c3 | ||
|
|
e986ef914f | ||
|
|
68117cd9e4 | ||
|
|
ccb4f8c0e4 | ||
|
|
1367a50b75 | ||
|
|
aec21e661d | ||
|
|
ae7697b33c | ||
|
|
37f72fe0b6 | ||
|
|
5660938f47 | ||
|
|
1c76385d79 | ||
|
|
208a3d9045 | ||
|
|
16c9483f37 | ||
|
|
70939fa8f0 | ||
|
|
ec1f846c92 | ||
|
|
1570657176 | ||
|
|
7e78f7a670 | ||
|
|
d556fe188a | ||
|
|
c07aeb3ba8 | ||
|
|
a6c6aed61c | ||
|
|
28a20d9b1e | ||
|
|
077e361a26 | ||
|
|
6180f2e1b9 | ||
|
|
5e57a56d21 | ||
|
|
402f62e09a | ||
|
|
6ec2e9c822 | ||
|
|
684defdc19 | ||
|
|
5ed2dc6e0e | ||
|
|
ce86a6f120 | ||
|
|
96b1a5f296 | ||
|
|
e9ebccf0df | ||
|
|
02b2aef958 | ||
|
|
c6d60cff64 | ||
|
|
81771f511c | ||
|
|
004160b664 | ||
|
|
2d25d3c2bc | ||
|
|
4728804a33 | ||
|
|
8524b9ecd6 | ||
|
|
eca598e28f | ||
|
|
f808c56121 | ||
|
|
77da0c912f | ||
|
|
e8a875eb32 | ||
|
|
e7a94426c2 | ||
|
|
17613837b6 | ||
|
|
4b4c98066c | ||
|
|
4a5b69800c | ||
|
|
70d6c3e9bf | ||
|
|
90e32af220 | ||
|
|
fdc0272940 | ||
|
|
d90a837714 | ||
|
|
47f7e0e5a0 | ||
|
|
25388a77aa | ||
|
|
2155d9bb80 | ||
|
|
46ab10f733 | ||
|
|
6e91ec3a0e | ||
|
|
b55aa12100 | ||
|
|
ded6a80b58 | ||
|
|
7435f1101a | ||
|
|
7720770c67 | ||
|
|
d9fc9e82ab | ||
|
|
ae66e4b3f8 | ||
|
|
1e65f10d3f | ||
|
|
a76f27152b | ||
|
|
de0df4b534 | ||
|
|
f78cf6e79a | ||
|
|
b84c36eb2e | ||
|
|
6355aa863c | ||
|
|
80cc10e8b9 | ||
|
|
10c37d205a | ||
|
|
a9e94c341c | ||
|
|
3b181224fd | ||
|
|
89fa9dfd64 | ||
|
|
4a08ae75b3 | ||
|
|
d9b0f45c6a | ||
|
|
c5a3fb72e1 | ||
|
|
f0d7d8fac6 | ||
|
|
1f485bfd55 | ||
|
|
9e367db324 | ||
|
|
a2fdab8eb9 | ||
|
|
2c052c162f | ||
|
|
b1c9e8c07a | ||
|
|
f71817b0a2 | ||
|
|
73d09bc99c | ||
|
|
5ebb54a857 | ||
|
|
8725b2c230 | ||
|
|
fd18f2acdf | ||
|
|
3bffe58549 | ||
|
|
e8bc22370b | ||
|
|
b7be3011da | ||
|
|
f0045c9406 | ||
|
|
3186b5f24b | ||
|
|
ca5ce7d468 | ||
|
|
a05f6a64a8 | ||
|
|
70dffe95ff | ||
|
|
0360889fd6 | ||
|
|
7304411c5d | ||
|
|
22dd095ea9 | ||
|
|
30a270193f | ||
|
|
ee1dd2293e | ||
|
|
34d5e88def | ||
|
|
30c9dfce02 | ||
|
|
48ad4d040d | ||
|
|
1b4f097b1c | ||
|
|
7b6193ab62 | ||
|
|
10a2733fd5 | ||
|
|
e7353e184f | ||
|
|
a479863f88 | ||
|
|
c550545116 | ||
|
|
1d7da9c455 | ||
|
|
5be0fdea0b | ||
|
|
a2a6eaf695 | ||
|
|
d08573b6b8 | ||
|
|
af7daee3e7 | ||
|
|
3406b46db5 | ||
|
|
2b45cf1f67 | ||
|
|
ba4258aa89 | ||
|
|
fc0a3f38ac | ||
|
|
ad96da59c3 | ||
|
|
c7ce689739 | ||
|
|
fa0a8d30e7 | ||
|
|
b57ef84e66 | ||
|
|
e5432ef260 | ||
|
|
719156aadf | ||
|
|
0720005c93 | ||
|
|
897f127fbd | ||
|
|
fd8ade1bf1 | ||
|
|
7f6b0f572b | ||
|
|
a4d982ea62 | ||
|
|
317f27e5f9 | ||
|
|
b2427bd810 | ||
|
|
4ac5c2c677 | ||
|
|
2234962acc | ||
|
|
8f95da4b07 | ||
|
|
102bde65ba | ||
|
|
3d5421819f | ||
|
|
5167cacee8 | ||
|
|
882eed0737 | ||
|
|
e82ed2cbcb | ||
|
|
05466fbd7f | ||
|
|
2bfd26b2b5 | ||
|
|
a17b62b14c | ||
|
|
88cffdb70e | ||
|
|
51ae1c819a | ||
|
|
2608f9558c | ||
|
|
8176d60d96 | ||
|
|
2ce99b969d | ||
|
|
8b97904144 | ||
|
|
0e34f9a464 | ||
|
|
c09380644b | ||
|
|
1dfffce606 | ||
|
|
7e98b19587 | ||
|
|
2a1689009a | ||
|
|
5ef3b055ff | ||
|
|
f554afd6b1 | ||
|
|
5474693711 | ||
|
|
f9a41be530 | ||
|
|
c61bc46673 | ||
|
|
dd304d3569 | ||
|
|
2eff251e0c | ||
|
|
531db48c25 | ||
|
|
9c0ce6526c | ||
|
|
96123ccf63 | ||
|
|
305c2cb806 | ||
|
|
9af122b96e | ||
|
|
7ca08f2f30 | ||
|
|
c7dbfca53d | ||
|
|
8aa66dddfd | ||
|
|
eb43b96a1b | ||
|
|
a2963adbee | ||
|
|
baebfdb0bb | ||
|
|
c3c2f409e7 | ||
|
|
89312ceb58 | ||
|
|
9b915d289b | ||
|
|
3de8f9077d | ||
|
|
90b4e44bbe | ||
|
|
bd25b7f3b7 | ||
|
|
85dfb3c1e5 | ||
|
|
d16e42374f | ||
|
|
d56b802786 | ||
|
|
93db217239 | ||
|
|
33ef680c41 | ||
|
|
a150619d08 | ||
|
|
7d5fb5f041 | ||
|
|
e824b3cfe2 | ||
|
|
cd885e3b3a | ||
|
|
005622800d | ||
|
|
aef4fd39b9 | ||
|
|
2e57eaad1d | ||
|
|
a5d5f75f52 | ||
|
|
130073689d | ||
|
|
2d99acabe2 | ||
|
|
0e5231ba43 | ||
|
|
e62d76a6f2 | ||
|
|
ce55ed8221 | ||
|
|
c5e7fe7bdc | ||
|
|
c723fae0e2 | ||
|
|
68172d12b0 | ||
|
|
44ce76bcb1 | ||
|
|
44b9bd0046 | ||
|
|
2e38558a9d | ||
|
|
a679bfcd95 | ||
|
|
44315f327b | ||
|
|
4f7724dbaf | ||
|
|
dc3cc33893 | ||
|
|
2537088099 | ||
|
|
02aaa06cb3 | ||
|
|
abf5121b74 | ||
|
|
cc7584a223 | ||
|
|
43b6351237 | ||
|
|
3b74920ece | ||
|
|
005762a1a2 | ||
|
|
5841c4f38d | ||
|
|
6acc84fd9e | ||
|
|
afc072da2c | ||
|
|
8634c16a47 | ||
|
|
0aa3359f96 | ||
|
|
077e5b2998 | ||
|
|
4b01000d4c | ||
|
|
949d28a88f | ||
|
|
57cde41983 | ||
|
|
cb5b3e9468 | ||
|
|
69f19d24a3 | ||
|
|
549c54e311 | ||
|
|
ec7f9effd8 | ||
|
|
1f4cc7bb19 | ||
|
|
1d78e2bc20 | ||
|
|
942800a2a6 | ||
|
|
414996c3f5 | ||
|
|
0c3dab8dd2 | ||
|
|
c48f9a69cc | ||
|
|
3277887089 | ||
|
|
304339f589 | ||
|
|
45cfdef45d | ||
|
|
f440c3f2c8 | ||
|
|
db74a486c5 | ||
|
|
4f36d149d7 | ||
|
|
3727bfb67f | ||
|
|
f26ab2f941 | ||
|
|
cf56b24dda | ||
|
|
2a8cb3c4e2 | ||
|
|
5478e648a7 | ||
|
|
b47d633727 | ||
|
|
810cdeeab4 | ||
|
|
075049abc4 | ||
|
|
56afbe6eb1 | ||
|
|
cf309102a2 | ||
|
|
32b37ed8f0 | ||
|
|
ce8ac0a81c | ||
|
|
4d8e0d7b85 | ||
|
|
6d7f52d2d6 | ||
|
|
e63b3d1b3e | ||
|
|
d77d953f84 | ||
|
|
689835cc17 | ||
|
|
6456a6b0c0 | ||
|
|
996c5f86c1 | ||
|
|
3fc8fe505b | ||
|
|
daeecc9b68 | ||
|
|
982398b32f | ||
|
|
fae4c504c9 | ||
|
|
b4a56f6dd7 | ||
|
|
fc26bef80a | ||
|
|
034552a063 | ||
|
|
bb505273f4 | ||
|
|
f876df6acc | ||
|
|
d097223d41 | ||
|
|
d01f7be58a | ||
|
|
d5375ca9ed | ||
|
|
eda8404144 | ||
|
|
e17a7cedb6 | ||
|
|
4ad4cff23f | ||
|
|
cc7a44dc17 | ||
|
|
873e68e1e1 | ||
|
|
4f44a68198 | ||
|
|
1eab957d85 | ||
|
|
4c145af7a3 | ||
|
|
7fab4ca1ba | ||
|
|
c1e45c4a30 | ||
|
|
5784a005dc | ||
|
|
a3e4d6998f | ||
|
|
32907764b3 | ||
|
|
cb34b1634d | ||
|
|
5199fd2566 | ||
|
|
b31c6c6780 | ||
|
|
aeec2c076e | ||
|
|
8bbce188ef | ||
|
|
dbdc010764 | ||
|
|
a81c48cc22 | ||
|
|
6eb77b7c2f | ||
|
|
92a50fe51d | ||
|
|
572caf6826 | ||
|
|
b0c8ceb302 | ||
|
|
c9ae6532a0 | ||
|
|
619e3c4852 | ||
|
|
e5cfcb601b | ||
|
|
2b92bf3694 | ||
|
|
cd42d09ea9 | ||
|
|
c56b1c8a86 | ||
|
|
e8d99e15f7 | ||
|
|
4dcec504ca | ||
|
|
1308e52e42 | ||
|
|
f6d356c5ce | ||
|
|
eb2de869b8 | ||
|
|
c6030d33ca | ||
|
|
655058a7e6 | ||
|
|
16d4ffbe3a | ||
|
|
775125c8a7 | ||
|
|
631e63a0b5 | ||
|
|
4cb2306de0 | ||
|
|
f15ee439a9 | ||
|
|
b9a2473d19 | ||
|
|
5b58223f9d | ||
|
|
f34fd0bd00 | ||
|
|
984b02700e | ||
|
|
e310392800 | ||
|
|
2cc291dccd | ||
|
|
2dcf043787 | ||
|
|
6b03ae0dc3 | ||
|
|
5dd5668389 | ||
|
|
8380894692 | ||
|
|
94f16b986a | ||
|
|
2928df8b8c | ||
|
|
71a819fcf0 | ||
|
|
713136672a | ||
|
|
f1bd47be8c | ||
|
|
2e82960ae6 | ||
|
|
a31fcd7346 | ||
|
|
4a1a53d3ab | ||
|
|
be173a838d | ||
|
|
623bd52e1f | ||
|
|
5ebdf3e878 | ||
|
|
761eee2cdc | ||
|
|
831e49919b | ||
|
|
6d90586aee | ||
|
|
a7f0ade83a | ||
|
|
c49e300247 | ||
|
|
6d8e34762e | ||
|
|
33461f5ac2 | ||
|
|
4e3345482f | ||
|
|
7dc6fb27ea | ||
|
|
5ced94755b | ||
|
|
0ffd860fdb | ||
|
|
05e786e3d6 | ||
|
|
d5e638c8f7 | ||
|
|
122ffeeab5 | ||
|
|
1448eac7c1 | ||
|
|
f2dbd5ff96 | ||
|
|
dcae5ad5f2 | ||
|
|
9bd3ade93d | ||
|
|
22dcb883b3 | ||
|
|
2e945780de | ||
|
|
9033b688ab | ||
|
|
1d4ed6609d | ||
|
|
b0269e310f | ||
|
|
74ccf7d820 | ||
|
|
2eae6243bb | ||
|
|
276532e2e1 | ||
|
|
fc07dd2af9 | ||
|
|
989712c2d5 | ||
|
|
ee43fcc91f | ||
|
|
18ca92cec4 | ||
|
|
dc11814695 | ||
|
|
17a31e0904 | ||
|
|
f990530031 | ||
|
|
46f1f0f8e9 | ||
|
|
885e933948 | ||
|
|
9b2e99c559 | ||
|
|
60ed54d6d3 | ||
|
|
939398b277 | ||
|
|
d2c820f080 | ||
|
|
375578177b | ||
|
|
eb9f2ccbaa | ||
|
|
d4b211e678 | ||
|
|
9fc4fbc3e7 | ||
|
|
1f5ac411f6 | ||
|
|
a7748a8492 | ||
|
|
edbcf95ead | ||
|
|
0aa29f775c | ||
|
|
a4a6105bc9 | ||
|
|
23098131b8 | ||
|
|
fdcedb5592 | ||
|
|
17098cf2ab | ||
|
|
7ef3dcc56c | ||
|
|
8a38276f5d | ||
|
|
190c57e853 | ||
|
|
785eca7289 | ||
|
|
93baa19ba1 |
@@ -6,6 +6,7 @@
|
||||
|
||||
# Used for determining the homeserver to use for short urls etc.
|
||||
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
|
||||
# VITE_FALLBACK_STUN_ALLOWED=false
|
||||
|
||||
# Used for submitting debug logs to an external rageshake server
|
||||
# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit
|
||||
@@ -22,7 +23,9 @@
|
||||
# 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
|
||||
@@ -28,6 +28,10 @@ module.exports = {
|
||||
"plugin:matrix-org/react",
|
||||
"prettier",
|
||||
],
|
||||
rules: {
|
||||
// We're aiming to convert this code to strict mode
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
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
@@ -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
@@ -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
|
||||
2
.github/workflows/lint.yaml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
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
|
||||
|
||||
4
.github/workflows/publish.yaml
vendored
@@ -32,10 +32,14 @@ jobs:
|
||||
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
@@ -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
@@ -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 }}
|
||||
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.env
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
18
.vscode/settings.json
vendored
@@ -2,5 +2,21 @@
|
||||
"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,4 @@
|
||||
FROM node:16-buster as builder
|
||||
FROM --platform=$BUILDPLATFORM node:16-buster as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
|
||||
76
README.md
@@ -1,32 +1,72 @@
|
||||
# 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
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
You can now edit the configuration in `.env` to your liking. The most important thing is to set `VITE_DEFAULT_HOMESERVER` to the homeserver that the app should use, such as `https://call.ems.host`.
|
||||
|
||||
Next, build the project:
|
||||
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
If all went well, you can now find the build output under `dist` as a series of static files. These can be hosted using any web server of your choice.
|
||||
|
||||
Because Element Call uses client-side routing, your server must be able to route any requests to non-existing paths back to `/index.html`. For example, in Nginx you can achieve this with the `try_files` directive:
|
||||
|
||||
```
|
||||
server {
|
||||
...
|
||||
location / {
|
||||
...
|
||||
try_files $uri /$uri /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Element Call is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/2553). To get started, clone, install, and link the package:
|
||||
|
||||
```
|
||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||
cd matrix-js-sdk
|
||||
git checkout robertlong/group-call
|
||||
git switch 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
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
By default, the app expects you to have [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008. If you wish to use another homeserver, you can set it in your `.env` file.
|
||||
|
||||
You're now ready to launch the development server:
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
@@ -34,20 +74,6 @@ yarn dev
|
||||
|
||||
Configuration options are documented in the `.env` file.
|
||||
|
||||
## 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
@@ -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"],
|
||||
};
|
||||
20
i18next-parser.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
keySeparator: false,
|
||||
namespaceSeparator: false,
|
||||
contextSeparator: "|",
|
||||
pluralSeparator: "|",
|
||||
createOldCatalogs: false,
|
||||
defaultNamespace: "app",
|
||||
lexers: {
|
||||
ts: [{
|
||||
lexer: "JavascriptLexer",
|
||||
functions: ["t", "translatedError"],
|
||||
functionsNamespace: ["useTranslation", "withTranslation"],
|
||||
}],
|
||||
},
|
||||
locales: ["en-GB"],
|
||||
output: "public/locales/$LOCALE/$NAMESPACE.json",
|
||||
input: ["src/**/*.{ts,tsx}"],
|
||||
sort: true,
|
||||
useKeysAsDefaultValue: true,
|
||||
};
|
||||
44
package.json
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
@@ -10,7 +11,10 @@
|
||||
"prettier:format": "prettier -w src",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src",
|
||||
"lint:types": "tsc"
|
||||
"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",
|
||||
@@ -33,33 +37,43 @@
|
||||
"@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",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960",
|
||||
"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#7ec726e10be835588d4b188fcd3d137b4690d79a",
|
||||
"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",
|
||||
"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",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@storybook/react": "^6.5.0-alpha.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/request": "^2.48.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
||||
"@typescript-eslint/parser": "^5.22.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -68,12 +82,32 @@
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"i18next-parser": "^6.6.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.2.2",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.42.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
131
public/locales/bg/app.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Вече имате акаунт?</0><1><0>Влезте с него</0> или <2>Влезте като гост</2></1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Създайте акаунт</0> или <2>Влезте като гост</2>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Възникна грешка.</0><1>Изпращнето на debug логове ще ни помогне да открием проблема.</1>",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Защо не настройте парола за да запазите акаунта си?</0><1>Ще можете да запазите името и аватара си за бъдещи разговори</1>",
|
||||
"Accept camera/microphone permissions to join the call.": "Приемете разрешенията за камера/микрофон за да се присъедините в разговора.",
|
||||
"Accept microphone permissions to join the call.": "Приемете разрешението за микрофона за да се присъедините в разговора.",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.",
|
||||
"Audio": "Звук",
|
||||
"Avatar": "Аватар",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия</2>",
|
||||
"Call link copied": "Връзка към разговора бе копирана",
|
||||
"Call type menu": "Меню \"тип на разговора\"",
|
||||
"Camera": "Камера",
|
||||
"Camera {{n}}": "Камера {{n}}",
|
||||
"Camera/microphone permissions needed to join the call.": "Необходими са разрешения за камера/микрофон за да се присъедините в разговора.",
|
||||
"Change layout": "Промени изгледа",
|
||||
"Close": "Затвори",
|
||||
"Confirm password": "Потвърди паролата",
|
||||
"Connection lost": "Връзката се изгуби",
|
||||
"Copied!": "Копирано!",
|
||||
"Copy and share this call link": "Копирай и сподели връзка към разговора",
|
||||
"Create account": "Създай акаунт",
|
||||
"Debug log": "Debug логове",
|
||||
"Debug log request": "Заявка за debug логове",
|
||||
"Description (optional)": "Описание (незадължително)",
|
||||
"Details": "Детайли",
|
||||
"Developer": "Разработчик",
|
||||
"Display name": "Име/псевдоним",
|
||||
"Download debug logs": "Изтеглете debug логове",
|
||||
"Entering room…": "Влизане в стаята…",
|
||||
"Exit full screen": "Излез от цял екран",
|
||||
"Fetching group call timed out.": "Изтече времето за взимане на груповия разговор.",
|
||||
"Freedom": "Свобода",
|
||||
"Full screen": "Цял екран",
|
||||
"Go": "Напред",
|
||||
"Grid layout menu": "Меню \"решетков изглед\"",
|
||||
"Having trouble? Help us fix it.": "Имате проблем? Помогнете да го поправим.",
|
||||
"Home": "Начало",
|
||||
"Include debug logs": "Включи debug логове",
|
||||
"Incompatible versions": "Несъвместими версии",
|
||||
"Incompatible versions!": "Несъвместими версии!",
|
||||
"Inspector": "Инспектор",
|
||||
"Invite": "Покани",
|
||||
"Invite people": "Покани хора",
|
||||
"Join call": "Влез в разговора",
|
||||
"Join call now": "Влез в разговора сега",
|
||||
"Join existing call?": "Присъединяване към съществуващ разговор?",
|
||||
"Leave": "Напусни",
|
||||
"Loading room…": "Напускане на стаята…",
|
||||
"Loading…": "Зареждане…",
|
||||
"Local volume": "Локална сила на звука",
|
||||
"Logging in…": "Влизане…",
|
||||
"Login": "Влез",
|
||||
"Login to your account": "Влезте в акаунта си",
|
||||
"Microphone": "Микрофон",
|
||||
"Microphone permissions needed to join the call.": "Необходими са разрешения за микрофона за да можете да се присъедините в разговора.",
|
||||
"Microphone {{n}}": "Микрофон {{n}}",
|
||||
"More": "Още",
|
||||
"More menu": "Мено \"още\"",
|
||||
"Mute microphone": "Заглуши микрофона",
|
||||
"No": "Не",
|
||||
"Not now, return to home screen": "Не сега, върни се на началния екран",
|
||||
"Not registered yet? <2>Create an account</2>": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Други потребители се опитват да се присъединят в разговора от несъвместими версии. Следните потребители трябва да проверят дали са презаредили браузърите си<1>{userLis}</1>",
|
||||
"Password": "Парола",
|
||||
"Passwords must match": "Паролите не съвпадат",
|
||||
"Press and hold spacebar to talk": "Натиснете и задръжте Space за да говорите",
|
||||
"Press and hold spacebar to talk over {{name}}": "Натиснете и задръжте Space за да говорите заедно с {{name}}",
|
||||
"Press and hold to talk": "Натиснете и задръжте за да говорите",
|
||||
"Press and hold to talk over {{name}}": "Натиснете и задръжте за да говорите заедно с {{name}}",
|
||||
"Profile": "Профил",
|
||||
"Recaptcha dismissed": "Recaptcha отхвърлена",
|
||||
"Recaptcha not loaded": "Recaptcha не е заредена",
|
||||
"Register": "Регистрация",
|
||||
"Registering…": "Регистриране…",
|
||||
"Release spacebar key to stop": "Отпуснете Space за да спрете",
|
||||
"Release to stop": "Отпуснете за да спрете",
|
||||
"Remove": "Премахни",
|
||||
"Return to home screen": "Връщане на началния екран",
|
||||
"Save": "Запази",
|
||||
"Saving…": "Запазване…",
|
||||
"Select an option": "Изберете опция",
|
||||
"Send debug logs": "Изпратете debug логове",
|
||||
"Sending…": "Изпращане…",
|
||||
"Settings": "Настройки",
|
||||
"Share screen": "Сподели екрана",
|
||||
"Show call inspector": "Покажи инспектора на разговора",
|
||||
"Sign in": "Влез",
|
||||
"Sign out": "Излез",
|
||||
"Spatial audio": "Пространствен звук",
|
||||
"Speaker": "Говорител",
|
||||
"Speaker {{n}}": "Говорител {{n}}",
|
||||
"Spotlight": "Прожектор",
|
||||
"Stop sharing screen": "Спри споделянето на екрана",
|
||||
"Submit feedback": "Изпрати обратна връзка",
|
||||
"Submitting feedback…": "Изпращане на обратна връзка…",
|
||||
"Take me Home": "Отиди в Начало",
|
||||
"Talk over speaker": "Говорете заедно с говорителя",
|
||||
"Talking…": "Говорене…",
|
||||
"Thanks! We'll get right on it.": "Благодарим! Веднага ще се заемем.",
|
||||
"This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Този сайт се предпазва от ReCAPTCHA и важат <2>Политиката за поверителност</2> и <6>Условията за ползване на услугата</6> на Google.<9></9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия</12>",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Това прави звука на говорителя да изглежда, че излиза от мястото където са позиционирани на екрана. (Експериментална функция: може да повлияе на стабилността на звука.)",
|
||||
"Turn off camera": "Изключи камерата",
|
||||
"Turn on camera": "Включи камерата",
|
||||
"Unmute microphone": "Включи микрофона",
|
||||
"User ID": "Потребителски идентификатор",
|
||||
"User menu": "Потребителско меню",
|
||||
"Username": "Потребителско име",
|
||||
"Version: {{version}}": "Версия: {{version}}",
|
||||
"Video": "Видео",
|
||||
"Video call": "Видео разговор",
|
||||
"Video call name": "Име на видео разговора",
|
||||
"Waiting for network": "Изчакване на мрежата",
|
||||
"Waiting for other participants…": "Изчакване на други участници…",
|
||||
"Walkie-talkie call": "Уоки-токи разговор",
|
||||
"Walkie-talkie call name": "Име на уоки-токи разговора",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не се поддържа или се блокира от браузъра.",
|
||||
"Yes, join call": "Да, присъедини се",
|
||||
"You can't talk at the same time": "Не можете да говорите едновременно",
|
||||
"Your recent calls": "Скорошните ви разговори",
|
||||
"{{count}} people connected|one": "{{count}} човек се свърза",
|
||||
"{{count}} people connected|other": "{{count}} човека се звързаха",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, разговорът ви приключи",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is presenting": "{{name}} презентира",
|
||||
"{{name}} is talking…": "{{name}} говори…",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - уоки-токи разговор"
|
||||
}
|
||||
83
public/locales/cs/app.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"Copy and share this call link": "Zkopírujte a sdílejte odkaz na hovor",
|
||||
"Copied!": "Zkopírováno!",
|
||||
"Connection lost": "Připojení ztraceno",
|
||||
"Confirm password": "Potvrdit heslo",
|
||||
"Close": "Zavřít",
|
||||
"Change layout": "Změnit rozložení",
|
||||
"Camera/microphone permissions needed to join the call.": "Oprávnění k přístupu ke kameře/mikrofonu jsou nutná pro připojení k hovoru.",
|
||||
"Camera {{n}}": "Kamera {{n}}",
|
||||
"Camera": "Kamera",
|
||||
"Call link copied": "Odkaz na hovor zkopírován",
|
||||
"Avatar": "Avatar",
|
||||
"Audio": "Audio",
|
||||
"Accept microphone permissions to join the call.": "Povolte přístup k mikrofonu pro připojení k hovoru.",
|
||||
"Accept camera/microphone permissions to join the call.": "Povolte přístup ke kameře/mikrofonu pro připojení do hovoru.",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Vytvořit účet</0> Or <2>Jako host</2>",
|
||||
"Your recent calls": "Vaše nedávné hovory",
|
||||
"You can't talk at the same time": "Teď nemůžete mluvit",
|
||||
"Yes, join call": "Ano, připojit se",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC není podporováno nebo je zakázáno tímto prohlížečem.",
|
||||
"Waiting for other participants…": "Čekání na další účastníky…",
|
||||
"Waiting for network": "Čekání na připojení",
|
||||
"Video call name": "Jméno videohovoru",
|
||||
"Video call": "Videohovor",
|
||||
"Video": "Video",
|
||||
"Version: {{version}}": "Verze: {{version}}",
|
||||
"Username": "Uživatelské jméno",
|
||||
"User menu": "Uživatelské menu",
|
||||
"User ID": "ID uživatele",
|
||||
"Unmute microphone": "Zapnout mikrofon",
|
||||
"Turn on camera": "Zapnout kameru",
|
||||
"Turn off camera": "Vypnout kameru",
|
||||
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
|
||||
"Thanks! We'll get right on it.": "Děkujeme! Hned se na to vrhneme.",
|
||||
"Take me Home": "Domovská obrazovka",
|
||||
"Submitting feedback…": "Odesílání zpětné vazby…",
|
||||
"Submit feedback": "Dát feedback",
|
||||
"Stop sharing screen": "Zastavit sdílení obrazovek",
|
||||
"Speaker {{n}}": "Reproduktor {{n}}",
|
||||
"Speaker": "Reproduktor",
|
||||
"Spatial audio": "Prostorový zvuk",
|
||||
"Sign out": "Odhlásit se",
|
||||
"Sign in": "Přihlásit se",
|
||||
"Show call inspector": "Zobrazit inspektor hovoru",
|
||||
"Share screen": "Sdílet obrazovku",
|
||||
"Settings": "Nastavení",
|
||||
"Sending…": "Posílání…",
|
||||
"Sending debug logs…": "Posílání ladícího záznamu…",
|
||||
"Send debug logs": "Poslat ladící záznam",
|
||||
"Select an option": "Vyberte možnost",
|
||||
"Saving…": "Ukládání…",
|
||||
"Save": "Uložit",
|
||||
"Return to home screen": "Vrátit se na domácí obrazovku",
|
||||
"Remove": "Odstranit",
|
||||
"Registering…": "Registrování…",
|
||||
"Register": "Registrace",
|
||||
"Profile": "Profil",
|
||||
"Press and hold to talk": "Stiskněte a držte pro mluvení",
|
||||
"Press and hold spacebar to talk": "Stiskněte a držte mezerník pro mluvení",
|
||||
"Passwords must match": "Hesla se musí shodovat",
|
||||
"Password": "Heslo",
|
||||
"Not now, return to home screen": "Teď ne, vrátit se na domovskou obrazovku",
|
||||
"No": "Ne",
|
||||
"Mute microphone": "Ztlumit mikrofon",
|
||||
"More": "Více",
|
||||
"Microphone permissions needed to join the call.": "Přístup k mikrofonu je nutný pro připojení se k hovoru.",
|
||||
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||
"Microphone": "Mikrofon",
|
||||
"Login to your account": "Přihlásit se ke svůmu účtu",
|
||||
"Login": "Přihlášení",
|
||||
"Logging in…": "Přihlašování se…",
|
||||
"Local volume": "Lokální hlasitost",
|
||||
"Loading…": "Načítání…",
|
||||
"Loading room…": "Načítání místnosti…",
|
||||
"Leave": "Opustit hovor",
|
||||
"Join call now": "Připojit se k hovoru",
|
||||
"Join call": "Připojit se k hovoru",
|
||||
"Invite people": "Pozvat lidi",
|
||||
"Invite": "Pozvat",
|
||||
"Inspector": "Insepktor",
|
||||
"Incompatible versions!": "Nekompatibilní verze!",
|
||||
"Incompatible versions": "Nekompatibilní verze"
|
||||
}
|
||||
134
public/locales/de/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Du hast bereits ein Konto?</0><1><0>Anmelden</0> Oder <2>Als Gast betreten</2></1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Konto erstellen</0> Oder <2>Als Gast betreten</2>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoppla, da ist etwas schief gelaufen.</0><1>Die Übermittlung von Debug-Protokollen wird uns helfen, das Problem zu finden.</1>",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Warum vergibst du nicht abschließend ein Passwort, um dein Konto zu erhalten?</0><1>Du kannst deinen Namen behalten und ein Profilbild für zukünftige Anrufe festlegen.</1>",
|
||||
"Accept camera/microphone permissions to join the call.": "Erlaube Zugriff auf Kamera/Mikrofon um dem Anruf beizutreten.",
|
||||
"Accept microphone permissions to join the call.": "Erlaube Zugriff auf das Mikrofon um dem Anruf beizutreten.",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.",
|
||||
"Audio": "Audio",
|
||||
"Avatar": "Avatar",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Los geht’s“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
|
||||
"Call link copied": "Anruflink kopiert",
|
||||
"Call type menu": "Anruftyp Menü",
|
||||
"Camera": "Kamera",
|
||||
"Camera {{n}}": "Kamera {{n}}",
|
||||
"Camera/microphone permissions needed to join the call.": "Kamera-/Mikrofonberechtigung für die Teilnahme am Anruf erforderlich.",
|
||||
"Change layout": "Layout ändern",
|
||||
"Close": "Schließen",
|
||||
"Confirm password": "Passwort bestätigen",
|
||||
"Connection lost": "Verbindung verloren",
|
||||
"Copied!": "Kopiert!",
|
||||
"Copy and share this call link": "Kopiere und teile diesen Anruflink",
|
||||
"Create account": "Konto erstellen",
|
||||
"Debug log": "Debug-Protokoll",
|
||||
"Debug log request": "Debug-Log Anfrage",
|
||||
"Description (optional)": "Beschreibung (optional)",
|
||||
"Details": "Details",
|
||||
"Developer": "Entwickler",
|
||||
"Display name": "Anzeigename",
|
||||
"Download debug logs": "Debug-Protokolle herunterladen",
|
||||
"Entering room…": "Betrete Raum …",
|
||||
"Exit full screen": "Vollbildmodus verlassen",
|
||||
"Freedom": "Freiraum",
|
||||
"Full screen": "Vollbild",
|
||||
"Go": "Los geht’s",
|
||||
"Grid layout menu": "Grid-Layout-Menü",
|
||||
"Having trouble? Help us fix it.": "Du hast ein Problem? Hilf uns, es zu beheben.",
|
||||
"Home": "Startseite",
|
||||
"Include debug logs": "Debug-Protokolle einschließen",
|
||||
"Incompatible versions": "Inkompatible Versionen",
|
||||
"Incompatible versions!": "Inkompatible Versionen!",
|
||||
"Inspector": "Inspektor",
|
||||
"Invite": "Einladen",
|
||||
"Invite people": "Personen einladen",
|
||||
"Join call": "Anruf beitreten",
|
||||
"Join call now": "Anruf beitreten",
|
||||
"Join existing call?": "An bestehendem Anruf teilnehmen?",
|
||||
"Leave": "Verlassen",
|
||||
"Loading room…": "Lade Raum …",
|
||||
"Loading…": "Lade …",
|
||||
"Local volume": "Lokale Lautstärke",
|
||||
"Logging in…": "Anmelden …",
|
||||
"Login": "Anmelden",
|
||||
"Login to your account": "Melde dich mit deinem Konto an",
|
||||
"Microphone": "Mikrofon",
|
||||
"Microphone permissions needed to join the call.": "Mikrofon-Berechtigung ist erforderlich, um dem Anruf beizutreten.",
|
||||
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||
"More": "Mehr",
|
||||
"More menu": "Weiteres Menü",
|
||||
"Mute microphone": "Mikrofon stummschalten",
|
||||
"No": "Nein",
|
||||
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
|
||||
"Not registered yet? <2>Create an account</2>": "Noch nicht registriert? <2>Konto erstellen</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Andere Benutzer versuchen, diesem Aufruf von einer inkompatiblen Softwareversion aus beizutreten. Diese Benutzer sollten ihre Web-Browser Seite neu laden:<1>{userLis}</1>",
|
||||
"Password": "Passwort",
|
||||
"Passwords must match": "Passwörter müssen übereinstimmen",
|
||||
"Press and hold spacebar to talk": "Halte zum Sprechen die Leertaste gedrückt",
|
||||
"Press and hold spacebar to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen die Leertaste gedrückt halten",
|
||||
"Press and hold to talk": "Zum Sprechen gedrückt halten",
|
||||
"Press and hold to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen gedrückt halten",
|
||||
"Profile": "Profil",
|
||||
"Recaptcha dismissed": "Recaptcha abgelehnt",
|
||||
"Recaptcha not loaded": "Recaptcha nicht geladen",
|
||||
"Register": "Registrieren",
|
||||
"Registering…": "Registrierung …",
|
||||
"Release spacebar key to stop": "Leertaste loslassen, um zu stoppen",
|
||||
"Release to stop": "Loslassen zum Stoppen",
|
||||
"Remove": "Entfernen",
|
||||
"Return to home screen": "Zurück zum Startbildschirm",
|
||||
"Save": "Speichern",
|
||||
"Saving…": "Speichere …",
|
||||
"Select an option": "Wähle eine Option",
|
||||
"Send debug logs": "Debug-Logs senden",
|
||||
"Sending…": "Senden …",
|
||||
"Settings": "Einstellungen",
|
||||
"Share screen": "Bildschirm teilen",
|
||||
"Show call inspector": "Anrufinspektor anzeigen",
|
||||
"Sign in": "Anmelden",
|
||||
"Sign out": "Abmelden",
|
||||
"Spatial audio": "Räumliche Audiowiedergabe",
|
||||
"Speaker": "Wiedergabegerät",
|
||||
"Speaker {{n}}": "Wiedergabegerät {{n}}",
|
||||
"Spotlight": "Rampenlicht",
|
||||
"Stop sharing screen": "Beenden der Bildschirmfreigabe",
|
||||
"Submit feedback": "Rückmeldung geben",
|
||||
"Submitting feedback…": "Sende Rückmeldung …",
|
||||
"Take me Home": "Zurück zur Startseite",
|
||||
"Talk over speaker": "Aktiven Sprecher verdrängen und sprechen",
|
||||
"Talking…": "Sprechen …",
|
||||
"Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.",
|
||||
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Diese Website wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6> von Google.<9></9>Indem Du auf „Registrieren“ klickst, stimmst du unseren <12>Geschäftsbedingungen</12> zu",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Dies wird die Audiowiedergabe eines Sprechers wirken lassen, als käme sie von der Stelle des zugehörigen Videobildes. (Experimentelle Funktion: Dies könnte die Stabilität der Audiowiedergabe beeinträchtigen.)",
|
||||
"Turn off camera": "Kamera ausschalten",
|
||||
"Turn on camera": "Kamera einschalten",
|
||||
"Unmute microphone": "Mikrofon aktivieren",
|
||||
"User ID": "Benutzer-ID",
|
||||
"User menu": "Benutzermenü",
|
||||
"Username": "Benutzername",
|
||||
"Version: {{version}}": "Version: {{version}}",
|
||||
"Video": "Video",
|
||||
"Video call": "Videoanruf",
|
||||
"Video call name": "Name des Videoanrufs",
|
||||
"Waiting for network": "Warte auf Netzwerk",
|
||||
"Waiting for other participants…": "Warte auf weitere Teilnehmer …",
|
||||
"Walkie-talkie call": "Walkie-Talkie-Anruf",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Browser nicht unterstützt oder ist blockiert.",
|
||||
"Yes, join call": "Ja, Anruf beitreten",
|
||||
"You can't talk at the same time": "Du kannst nicht gleichzeitig sprechen",
|
||||
"Your recent calls": "Deine letzten Anrufe",
|
||||
"{{count}} people connected|one": "{{count}} Person verbunden",
|
||||
"{{count}} people connected|other": "{{count}} Personen verbunden",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, dein Anruf wurde beendet",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is presenting": "{{name}} präsentiert",
|
||||
"{{name}} is talking…": "{{name}} spricht …",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} – Walkie-Talkie-Anruf",
|
||||
"Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.",
|
||||
"Walkie-talkie call name": "Name des Walkie-Talkie-Anrufs",
|
||||
"Sending debug logs…": "Sende Debug-Protokolle …",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Anruf beitreten</0><1>Oder</1><2>Anruflink kopieren und später beitreten</2>",
|
||||
"{{name}} (Connecting...)": "{{name}} (verbindet sich …)"
|
||||
}
|
||||
134
public/locales/en-GB/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"{{count}} people connected|one": "{{count}} person connected",
|
||||
"{{count}} people connected|other": "{{count}} people connected",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
|
||||
"{{name}} (Connecting...)": "{{name}} (Connecting...)",
|
||||
"{{name}} is presenting": "{{name}} is presenting",
|
||||
"{{name}} is talking…": "{{name}} is talking…",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
|
||||
"Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.",
|
||||
"Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.",
|
||||
"Audio": "Audio",
|
||||
"Avatar": "Avatar",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "By clicking \"Go\", you agree to our <2>Terms and conditions</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>",
|
||||
"Call link copied": "Call link copied",
|
||||
"Call type menu": "Call type menu",
|
||||
"Camera": "Camera",
|
||||
"Camera {{n}}": "Camera {{n}}",
|
||||
"Camera/microphone permissions needed to join the call.": "Camera/microphone permissions needed to join the call.",
|
||||
"Change layout": "Change layout",
|
||||
"Close": "Close",
|
||||
"Confirm password": "Confirm password",
|
||||
"Connection lost": "Connection lost",
|
||||
"Copied!": "Copied!",
|
||||
"Copy and share this call link": "Copy and share this call link",
|
||||
"Create account": "Create account",
|
||||
"Debug log": "Debug log",
|
||||
"Debug log request": "Debug log request",
|
||||
"Description (optional)": "Description (optional)",
|
||||
"Details": "Details",
|
||||
"Developer": "Developer",
|
||||
"Display name": "Display name",
|
||||
"Download debug logs": "Download debug logs",
|
||||
"Entering room…": "Entering room…",
|
||||
"Exit full screen": "Exit full screen",
|
||||
"Fetching group call timed out.": "Fetching group call timed out.",
|
||||
"Freedom": "Freedom",
|
||||
"Full screen": "Full screen",
|
||||
"Go": "Go",
|
||||
"Grid layout menu": "Grid layout menu",
|
||||
"Having trouble? Help us fix it.": "Having trouble? Help us fix it.",
|
||||
"Home": "Home",
|
||||
"Include debug logs": "Include debug logs",
|
||||
"Incompatible versions": "Incompatible versions",
|
||||
"Incompatible versions!": "Incompatible versions!",
|
||||
"Inspector": "Inspector",
|
||||
"Invite": "Invite",
|
||||
"Invite people": "Invite people",
|
||||
"Join call": "Join call",
|
||||
"Join call now": "Join call now",
|
||||
"Join existing call?": "Join existing call?",
|
||||
"Leave": "Leave",
|
||||
"Loading room…": "Loading room…",
|
||||
"Loading…": "Loading…",
|
||||
"Local volume": "Local volume",
|
||||
"Logging in…": "Logging in…",
|
||||
"Login": "Login",
|
||||
"Login to your account": "Login to your account",
|
||||
"Microphone": "Microphone",
|
||||
"Microphone {{n}}": "Microphone {{n}}",
|
||||
"Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.",
|
||||
"More": "More",
|
||||
"More menu": "More menu",
|
||||
"Mute microphone": "Mute microphone",
|
||||
"No": "No",
|
||||
"Not now, return to home screen": "Not now, return to home screen",
|
||||
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
||||
"Password": "Password",
|
||||
"Passwords must match": "Passwords must match",
|
||||
"Press and hold spacebar to talk": "Press and hold spacebar to talk",
|
||||
"Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}",
|
||||
"Press and hold to talk": "Press and hold to talk",
|
||||
"Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}",
|
||||
"Profile": "Profile",
|
||||
"Recaptcha dismissed": "Recaptcha dismissed",
|
||||
"Recaptcha not loaded": "Recaptcha not loaded",
|
||||
"Register": "Register",
|
||||
"Registering…": "Registering…",
|
||||
"Release spacebar key to stop": "Release spacebar key to stop",
|
||||
"Release to stop": "Release to stop",
|
||||
"Remove": "Remove",
|
||||
"Return to home screen": "Return to home screen",
|
||||
"Save": "Save",
|
||||
"Saving…": "Saving…",
|
||||
"Select an option": "Select an option",
|
||||
"Send debug logs": "Send debug logs",
|
||||
"Sending debug logs…": "Sending debug logs…",
|
||||
"Sending…": "Sending…",
|
||||
"Settings": "Settings",
|
||||
"Share screen": "Share screen",
|
||||
"Show call inspector": "Show call inspector",
|
||||
"Sign in": "Sign in",
|
||||
"Sign out": "Sign out",
|
||||
"Spatial audio": "Spatial audio",
|
||||
"Speaker": "Speaker",
|
||||
"Speaker {{n}}": "Speaker {{n}}",
|
||||
"Spotlight": "Spotlight",
|
||||
"Stop sharing screen": "Stop sharing screen",
|
||||
"Submit feedback": "Submit feedback",
|
||||
"Submitting feedback…": "Submitting feedback…",
|
||||
"Take me Home": "Take me Home",
|
||||
"Talk over speaker": "Talk over speaker",
|
||||
"Talking…": "Talking…",
|
||||
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.",
|
||||
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)",
|
||||
"Turn off camera": "Turn off camera",
|
||||
"Turn on camera": "Turn on camera",
|
||||
"Unmute microphone": "Unmute microphone",
|
||||
"User ID": "User ID",
|
||||
"User menu": "User menu",
|
||||
"Username": "Username",
|
||||
"Version: {{version}}": "Version: {{version}}",
|
||||
"Video": "Video",
|
||||
"Video call": "Video call",
|
||||
"Video call name": "Video call name",
|
||||
"Waiting for network": "Waiting for network",
|
||||
"Waiting for other participants…": "Waiting for other participants…",
|
||||
"Walkie-talkie call": "Walkie-talkie call",
|
||||
"Walkie-talkie call name": "Walkie-talkie call name",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
|
||||
"Yes, join call": "Yes, join call",
|
||||
"You can't talk at the same time": "You can't talk at the same time",
|
||||
"Your recent calls": "Your recent calls"
|
||||
}
|
||||
134
public/locales/es/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>¿Por qué no mantienes tu cuenta estableciendo una contraseña?</0><1>Podrás mantener tu nombre y establecer un avatar para usarlo en futuras llamadas</1>",
|
||||
"Press and hold to talk over {{name}}": "Mantén pulsado para hablar por encima de {{name}}",
|
||||
"Your recent calls": "Tus llamadas recientes",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "Tu navegador no soporta o está bloqueando WebRTC.",
|
||||
"This call already exists, would you like to join?": "Esta llamada ya existe, ¿te gustaría unirte?",
|
||||
"Register": "Registrarse",
|
||||
"Not registered yet? <2>Create an account</2>": "¿No estás registrado todavía? <2>Crear una cuenta</2>",
|
||||
"Login to your account": "Iniciar sesión en tu cuenta",
|
||||
"Camera/microphone permissions needed to join the call.": "Se necesitan los permisos de cámara/micrófono para unirse a la llamada.",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptarás nuestros <2>Términos y condiciones</2>",
|
||||
"Accept microphone permissions to join the call.": "Acepta el permiso del micrófono para unirte a la llamada.",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones</2>",
|
||||
"You can't talk at the same time": "No podéis hablar a la vez",
|
||||
"Yes, join call": "Si, unirse a la llamada",
|
||||
"Walkie-talkie call name": "Nombre de la llamada Walkie-talkie",
|
||||
"Walkie-talkie call": "Llamada Walkie-talkie",
|
||||
"Waiting for other participants…": "Esperando a los otros participantes…",
|
||||
"Waiting for network": "Esperando a la red",
|
||||
"Video call name": "Nombre de la videollamada",
|
||||
"Video call": "Videollamada",
|
||||
"Video": "Video",
|
||||
"Version: {{version}}": "Versión: {{version}}",
|
||||
"Username": "Nombre de usuario",
|
||||
"User menu": "Menú de usuario",
|
||||
"User ID": "ID de usuario",
|
||||
"Unmute microphone": "Desilenciar el micrófono",
|
||||
"Turn on camera": "Encender la cámara",
|
||||
"Turn off camera": "Apagar la cámara",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Esto hará que el audio de la persona que hable parezca que viene de dondé esté posicionado en la pantalla. (Función experimental: esto puede afectar a la estabilidad del audio.)",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Este sitio está protegido por ReCAPTCHA y se aplica <2>la Política de Privacidad</2> y <6>los Términos de Servicio</6> de Google.<9></9>Al hacer clic en \"Registrar\" aceptarás nuestros <12>Términos y condiciones</12>",
|
||||
"Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.",
|
||||
"Talking…": "Hablando…",
|
||||
"Talk over speaker": "Hablar por encima",
|
||||
"Take me Home": "Volver al inicio",
|
||||
"Submitting feedback…": "Enviando comentarios…",
|
||||
"Submit feedback": "Enviar comentarios",
|
||||
"Stop sharing screen": "Dejar de compartir pantalla",
|
||||
"Spotlight": "Foco",
|
||||
"Speaker {{n}}": "Altavoz {{n}}",
|
||||
"Speaker": "Altavoz",
|
||||
"Spatial audio": "Audio espacial",
|
||||
"Sign out": "Cerrar sesión",
|
||||
"Sign in": "Iniciar sesión",
|
||||
"Show call inspector": "Mostrar inspector de llamada",
|
||||
"Share screen": "Compartir pantalla",
|
||||
"Settings": "Ajustes",
|
||||
"Sending…": "Enviando…",
|
||||
"Sending debug logs…": "Enviando registros de depuración…",
|
||||
"Send debug logs": "Enviar registros de depuración",
|
||||
"Select an option": "Selecciona una opción",
|
||||
"Saving…": "Guardando…",
|
||||
"Save": "Guardar",
|
||||
"Return to home screen": "Volver a la pantalla de inicio",
|
||||
"Remove": "Eliminar",
|
||||
"Release to stop": "Suelta para parar",
|
||||
"Release spacebar key to stop": "Suelta la barra espaciadora para parar",
|
||||
"Registering…": "Registrando…",
|
||||
"Recaptcha not loaded": "No se ha cargado el Recaptcha",
|
||||
"Recaptcha dismissed": "Recaptcha cancelado",
|
||||
"Profile": "Perfil",
|
||||
"Press and hold to talk": "Mantén pulsado para hablar",
|
||||
"Press and hold spacebar to talk over {{name}}": "Mantén pulsada la barra espaciadora para hablar por encima de {{name}}",
|
||||
"Press and hold spacebar to talk": "Mantén pulsada la barra espaciadora para hablar",
|
||||
"Passwords must match": "Las contraseñas deben coincidir",
|
||||
"Password": "Contraseña",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Otros usuarios están intentando unirse a la llamada con versiones incompatibles. Estos usuarios deberán asegurarse de que han refrescado sus navegadores:<1>{userLis}</1>",
|
||||
"Not now, return to home screen": "Ahora no, volver a la pantalla de inicio",
|
||||
"No": "No",
|
||||
"Mute microphone": "Silenciar micrófono",
|
||||
"More menu": "Menú Más",
|
||||
"More": "Más",
|
||||
"Microphone permissions needed to join the call.": "Se necesitan permisos del micrófono para unirse a la llamada.",
|
||||
"Microphone {{n}}": "Micrófono {{n}}",
|
||||
"Microphone": "Micrófono",
|
||||
"Login": "Iniciar sesión",
|
||||
"Logging in…": "Iniciando sesión…",
|
||||
"Local volume": "Volumen local",
|
||||
"Loading…": "Cargando…",
|
||||
"Loading room…": "Cargando sala…",
|
||||
"Leave": "Abandonar",
|
||||
"Join existing call?": "¿Unirse a llamada existente?",
|
||||
"Join call now": "Unirse a la llamada ahora",
|
||||
"Join call": "Unirse a la llamada",
|
||||
"Invite people": "Invitar a gente",
|
||||
"Invite": "Invitar",
|
||||
"Inspector": "Inspector",
|
||||
"Incompatible versions!": "¡Versiones incompatibles!",
|
||||
"Incompatible versions": "Versiones incompatibles",
|
||||
"Include debug logs": "Incluir registros de depuración",
|
||||
"Home": "Inicio",
|
||||
"Having trouble? Help us fix it.": "¿Tienes problemas? Ayúdanos a resolverlos.",
|
||||
"Grid layout menu": "Menú de distribución de cuadrícula",
|
||||
"Go": "Empezar",
|
||||
"Full screen": "Pantalla completa",
|
||||
"Freedom": "Libre",
|
||||
"Fetching group call timed out.": "Se ha agotado el tiempo de espera para obtener la llamada grupal.",
|
||||
"Exit full screen": "Salir de pantalla completa",
|
||||
"Entering room…": "Entrando a la sala…",
|
||||
"Download debug logs": "Descargar registros de depuración",
|
||||
"Display name": "Nombre a mostrar",
|
||||
"Developer": "Desarrollador",
|
||||
"Details": "Detalles",
|
||||
"Description (optional)": "Descripción (opcional)",
|
||||
"Debug log request": "Petición de registros de depuración",
|
||||
"Debug log": "Registro de depuración",
|
||||
"Create account": "Crear cuenta",
|
||||
"Copy and share this call link": "Copiar y compartir el enlace de la llamada",
|
||||
"Copied!": "¡Copiado!",
|
||||
"Connection lost": "Conexión perdida",
|
||||
"Confirm password": "Confirmar contraseña",
|
||||
"Close": "Cerrar",
|
||||
"Change layout": "Cambiar distribución",
|
||||
"Camera {{n}}": "Cámara {{n}}",
|
||||
"Camera": "Cámara",
|
||||
"Call type menu": "Menú de tipo de llamada",
|
||||
"Call link copied": "Enlace de la llamada copiado",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Otro usuario en esta llamada está teniendo problemas. Para diagnosticar estos problemas nos gustaría recopilar un registro de depuración.",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"Audio": "Audio",
|
||||
"Avatar": "Avatar",
|
||||
"Accept camera/microphone permissions to join the call.": "Acepta los permisos de cámara/micrófono para unirte a la llamada.",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, algo ha salido mal.</0><1>Enviar los registros de depuración nos ayudará a localizar el problema.</1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Crear una cuenta</0> o <2>Acceder como invitado</2>",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Unirse ahora</0><1>Or</1><2>Copiar el enlace y unirse más tarde</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>¿Ya tienes una cuenta?</0><1><0>Iniciar sesión</0> o <2>Acceder como invitado</2></1>",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Llamada de Walkie-talkie",
|
||||
"{{name}} is talking…": "{{name}} está hablando…",
|
||||
"{{name}} is presenting": "{{name}} está presentando",
|
||||
"{{name}} (Connecting...)": "{{name}} (Conectando...)",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, tu llamada ha finalizado",
|
||||
"{{count}} people connected|other": "{{count}} personas conectadas",
|
||||
"{{count}} people connected|one": "{{count}} persona conectada"
|
||||
}
|
||||
134
public/locales/et/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"Accept camera/microphone permissions to join the call.": "Kõnega anna õigused kaamera/mikrofoni kasutamiseks.",
|
||||
"Accept microphone permissions to join the call.": "Kõnega liitumiseks anna õigused mikrofoni kasutamiseks.",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Kas hoopis tahad salasõna seadistada ja sellega oma kasutajakonto alles jätta?</0><1>Siis saad säilitada oma nime ja määrata tunnuspildi, mida saad kasutada tulevastes kõnedes</1>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, midagi läks valesti.</0><1>Logide saatmine meile aitab meil probleemi lahendada.</1>",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Liitu kõnega kohe</0><1> Või</1><2>Kopeeri kõne link ja liitu hiljem</2>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Loo konto</0> Või <2>Sisene külalisena</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>On sul juba konto?</0><1><0>Logi sisse</0> Või <2>Logi sisse külalisena</2></1>",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - walkie-talkie-kõne",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is talking…": "{{nimi}} räägib…",
|
||||
"{{name}} is presenting": "{{nimi}} esitab",
|
||||
"{{name}} (Connecting...)": "{{nimi}} (ühendamisel...)",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, sinu kõne on nüüd lõppenud",
|
||||
"{{count}} people connected|other": "{{count}} osalejat liitunud",
|
||||
"{{count}} people connected|one": "{{count}} osaleja liitunud",
|
||||
"Invite people": "Kutsu inimesi",
|
||||
"Invite": "Kutsu",
|
||||
"Inspector": "Inspektor",
|
||||
"Incompatible versions!": "Ühildumatud versioonid!",
|
||||
"Incompatible versions": "Ühildumatud versioonid",
|
||||
"Include debug logs": "Lisa veatuvastuslogid",
|
||||
"Home": "Avavaatesse",
|
||||
"Having trouble? Help us fix it.": "Kas on probleeme? Aita meil asja parandada.",
|
||||
"Grid layout menu": "Ruudustikvaate menüü",
|
||||
"Go": "Jätka",
|
||||
"Full screen": "Täisekraan",
|
||||
"Freedom": "Vaba",
|
||||
"Fetching group call timed out.": "Grupikõne kättesaamine aegus.",
|
||||
"Exit full screen": "Välju täisekraanivaatest",
|
||||
"Entering room…": "Ruumi sisenemine…",
|
||||
"Download debug logs": "Lae alla veatuvastuslogid",
|
||||
"Display name": "Kuvatav nimi",
|
||||
"Developer": "Arendaja",
|
||||
"Details": "Täpsemalt",
|
||||
"Description (optional)": "Kirjeldus (valikuline)",
|
||||
"Debug log request": "Veaotsingulogi päring",
|
||||
"Debug log": "Veaotsingulogi",
|
||||
"Create account": "Loo konto",
|
||||
"Copy and share this call link": "Kopeeri ja jaga selle kõne linki",
|
||||
"Copied!": "Kopeeritud!",
|
||||
"Connection lost": "Ühendus on katkenud",
|
||||
"Confirm password": "Kinnita salasõna",
|
||||
"Close": "Sulge",
|
||||
"Change layout": "Muuda paigutust",
|
||||
"Camera/microphone permissions needed to join the call.": "Kõnega liitumiseks vajalikud kaamera/mikrofoni kasutamise load.",
|
||||
"Camera {{n}}": "Kaamera {{n}}",
|
||||
"Camera": "Kaamera",
|
||||
"Call type menu": "Kõnetüübi valik",
|
||||
"Call link copied": "Kõne link on kopeeritud",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Liitu kõnega“nõustud sa meie <2>kasutustingimustega</2>",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega</2>",
|
||||
"Avatar": "Tunnuspilt",
|
||||
"Audio": "Heli",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ühel teisel selles kõnes osalejal on lahenduse kasutamisel tekkinud probleem ning selle põhjuse leidmiseks me sooviksime koguda silumislogisid.",
|
||||
"Press and hold spacebar to talk": "Rääkimiseks vajuta ja hoia all tühikuklahvi",
|
||||
"Passwords must match": "Salasõnad ei klapi",
|
||||
"Password": "Salasõna",
|
||||
"Not registered yet? <2>Create an account</2>": "Pole veel registreerunud? <2>Loo kasutajakonto</2>",
|
||||
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
|
||||
"No": "Ei",
|
||||
"Mute microphone": "Summuta mikrofon",
|
||||
"Your recent calls": "Hiljutised kõned",
|
||||
"You can't talk at the same time": "Üheaegselt ei saa rääkida",
|
||||
"More menu": "Rohkem valikuid",
|
||||
"More": "Rohkem",
|
||||
"Microphone permissions needed to join the call.": "Kõnega liitumiseks on vaja lubada mikrofoni kasutamine.",
|
||||
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||
"Microphone": "Mikrofon",
|
||||
"Login to your account": "Logi oma kontosse sisse",
|
||||
"Login": "Sisselogimine",
|
||||
"Logging in…": "Sisselogimine …",
|
||||
"Local volume": "Kohalik helitugevus",
|
||||
"Loading…": "Laadimine …",
|
||||
"Loading room…": "Ruumi laadimine …",
|
||||
"Leave": "Lahku",
|
||||
"Join existing call?": "Liitu juba käimasoleva kõnega?",
|
||||
"Join call now": "Kõnega liitumine",
|
||||
"Join call": "Kõnega liitumine",
|
||||
"User ID": "Kasutajatunnus",
|
||||
"Turn on camera": "Lülita kaamera sisse",
|
||||
"Turn off camera": "Lülita kaamera välja",
|
||||
"Submitting feedback…": "Tagasiside saatmine…",
|
||||
"Take me Home": "Mine avalehele",
|
||||
"Submit feedback": "Jaga tagasisidet",
|
||||
"Stop sharing screen": "Lõpeta ekraani jagamine",
|
||||
"Spotlight": "Rambivalgus",
|
||||
"Speaker {{n}}": "Kõlar {{n}}",
|
||||
"Speaker": "Kõlar",
|
||||
"Spatial audio": "Ruumiline heli",
|
||||
"Sign out": "Logi välja",
|
||||
"Sign in": "Logi sisse",
|
||||
"Show call inspector": "Näita kõneteavet",
|
||||
"Share screen": "Jaga ekraani",
|
||||
"Settings": "Seadistused",
|
||||
"Sending…": "Saatmine…",
|
||||
"Sending debug logs…": "Veaotsingulogide saatmine…",
|
||||
"Send debug logs": "Saada veaotsingulogid",
|
||||
"Select an option": "Vali oma eelistus",
|
||||
"Saving…": "Salvestamine…",
|
||||
"Save": "Salvesta",
|
||||
"Return to home screen": "Tagasi avalehele",
|
||||
"Remove": "Eemalda",
|
||||
"Release to stop": "Peatamiseks vabasta klahv",
|
||||
"Release spacebar key to stop": "Peatamiseks vabasta tühikuklahv",
|
||||
"Registering…": "Registreerimine…",
|
||||
"Register": "Registreeru",
|
||||
"Recaptcha not loaded": "Robotilõks pole laetud",
|
||||
"Recaptcha dismissed": "Robotilõks on vahele jäetud",
|
||||
"Profile": "Profiil",
|
||||
"Press and hold to talk over {{name}}": "{{name}} ülerääkimiseks vajuta ja hoia all",
|
||||
"Press and hold to talk": "Rääkimiseks vajuta ja hoia all",
|
||||
"Press and hold spacebar to talk over {{name}}": "{{name}} ülerääkimiseks vajuta ja hoia all tühikuklahvi",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Teised kasutajad üritavad selle kõnega liituda ühildumatuid versioone kasutades. Need kasutajad peaksid oma brauseris lehe uuestilaadimise tegema:<1>{userLis}</1>",
|
||||
"Waiting for other participants…": "Ootame teiste osalejate lisandumist…",
|
||||
"Waiting for network": "Ootame võrguühendust",
|
||||
"Video call name": "Videokõne nimi",
|
||||
"Video call": "Videokõne",
|
||||
"Video": "Video",
|
||||
"Version: {{version}}": "Versioon: {{version}}",
|
||||
"Username": "Kasutajanimi",
|
||||
"This call already exists, would you like to join?": "See kõne on juba olemas, kas soovid liituda?",
|
||||
"Talking…": "Jutt käib…",
|
||||
"Talk over speaker": "Räägi teisest kõnelejast üle",
|
||||
"Thanks! We'll get right on it.": "Tänud! Tegeleme sellega esimesel võimalusel.",
|
||||
"Unmute microphone": "Aktiveeri mikrofon",
|
||||
"User menu": "Kasutajamenüü",
|
||||
"Yes, join call": "Jah, liitu kõnega",
|
||||
"Walkie-talkie call": "Walkie-talkie stiilis kõne",
|
||||
"Walkie-talkie call name": "Walkie-talkie stiilis kõne nimi",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Muudab kõneleja heli nii, nagu tuleks see sealt, kus on tema pilt ekraanil. (See on katseline funktsionaalsus ja võib mõjutada heli stabiilsust.)",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika</2> ja <6>teenusetingimused</6>.<9></9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega</12>"
|
||||
}
|
||||
132
public/locales/fa/app.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"Your recent calls": "تماسهای اخیر شما",
|
||||
"Video call": "تماس تصویری",
|
||||
"Video": "ویدیو",
|
||||
"Username": "نام کاربری",
|
||||
"User ID": "آی دی کاربر",
|
||||
"Turn on camera": "روشن کردن دوربین",
|
||||
"Turn off camera": "خاموش کردن دوربین",
|
||||
"Take me Home": "مرا به خانه ببر",
|
||||
"Speaker": "بلندگو",
|
||||
"Sign out": "خروج",
|
||||
"Sign in": "ورود",
|
||||
"Settings": "تنظیمات",
|
||||
"Save": "ذخیره",
|
||||
"Profile": "پروفایل",
|
||||
"Password": "رمز عبور",
|
||||
"No": "خیر",
|
||||
"Mute microphone": "بیصدا کردن میکروفون",
|
||||
"More": "بیشتر",
|
||||
"Microphone": "میکروفون",
|
||||
"Login to your account": "به حساب کاربری خود وارد شوید",
|
||||
"Login": "ورود",
|
||||
"Loading…": "بارگزاری…",
|
||||
"Loading room…": "بارگزاری اتاق…",
|
||||
"Leave": "خروج",
|
||||
"Join existing call?": "پیوست به تماس؟",
|
||||
"Join call now": "الان به تماس بپیوند",
|
||||
"Join call": "پیوستن به تماس",
|
||||
"Invite people": "دعوت از افراد",
|
||||
"Invite": "دعوت",
|
||||
"Home": "خانه",
|
||||
"Go": "رفتن",
|
||||
"Full screen": "تمام صحفه",
|
||||
"Freedom": "آزادی",
|
||||
"Exit full screen": "خروج از حالت تمام صفحه",
|
||||
"Entering room…": "درحال وارد شدن به اتاق…",
|
||||
"Download debug logs": "دانلود لاگ عیبیابی",
|
||||
"Display name": "نام نمایشی",
|
||||
"Developer": "توسعه دهنده",
|
||||
"Details": "جزئیات",
|
||||
"Description (optional)": "توضیحات (اختیاری)",
|
||||
"Debug log request": "درخواست لاگ عیبیابی",
|
||||
"Debug log": "لاگ عیبیابی",
|
||||
"Create account": "ساخت حساب کاربری",
|
||||
"Copy and share this call link": "لینک تماس را کپی کنید و به اشتراک بگذارید",
|
||||
"Copied!": "کپی شد!",
|
||||
"Connection lost": "ارتباط قطع شد",
|
||||
"Confirm password": "تایید رمزعبور",
|
||||
"Close": "بستن",
|
||||
"Change layout": "تغییر طرح",
|
||||
"Camera/microphone permissions needed to join the call.": "برای پیوستن به تماس، دسترسی به دوربین/ میکروفون نیاز است.",
|
||||
"Camera {{n}}": "دوربین {{n}}",
|
||||
"Camera": "دوربین",
|
||||
"Call type menu": "منوی نوع تماس",
|
||||
"Call link copied": "لینک تماس کپی شد",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "با کلیک بر روی پیوستن به تماس، شما با <2>شرایط و قوانین استفاده</2> موافقت میکنید",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "با کلیک بر روی برو، شما با <2>شرایط و قوانین استفاده</2> موافقت میکنید",
|
||||
"Avatar": "آواتار",
|
||||
"Audio": "صدا",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیبیابی را جمعآوری کنیم.",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"Accept microphone permissions to join the call.": "پذیرفتن دسترسی به میکروفون برای پیوستن به تماس.",
|
||||
"Accept camera/microphone permissions to join the call.": "پذیرفتن دسترسی دوربین/ میکروفون برای پیوستن به تماس.",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>چرا یک رمز عبور برای حساب کاربری خود تنظیم نمیکنید؟</0><1>شما میتوانید نام خود را حفظ کنید و یک آواتار برای تماسهای آینده بسازید</1>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>اوه، مشکلی پیش آمده.</0><1>ثبت کردن لاگ رفع اشکال به پیدا کردن مشکل توسط ما کمک میکند</1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>ساخت حساب کاربری</0> Or <2>دسترسی به عنوان میهمان</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>از قبل حساب کاربری دارید؟</0><1><0>ورود</0> Or <2>به عنوان یک میهمان وارد شوید</2></1>",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - تماس واکی-تاکی",
|
||||
"{{name}} is talking…": "{{name}} در حال صحبت است…",
|
||||
"{{name}} is presenting": "{{name}} حاضر است",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}} تماس شما پایان یافت",
|
||||
"{{count}} people connected|other": "{{count}} نفر متصل هستند",
|
||||
"{{count}} people connected|one": "{{count}} فرد متصل هستند",
|
||||
"Local volume": "حجم داخلی",
|
||||
"Inspector": "بازرس",
|
||||
"Incompatible versions!": "نسخههای ناسازگار!",
|
||||
"Incompatible versions": "نسخههای ناسازگار",
|
||||
"Spotlight": "نور افکن",
|
||||
"Speaker {{n}}": "بلندگو {{n}}",
|
||||
"Show call inspector": "نمایش بازرس تماس",
|
||||
"Share screen": "اشتراک گذاری صفحه نمایش",
|
||||
"Sending…": "در حال ارسال…",
|
||||
"Sending debug logs…": "در حال ارسال باگهای عیبیابی…",
|
||||
"Send debug logs": "ارسال لاگهای عیبیابی",
|
||||
"Select an option": "یک گزینه را انتخاب کنید",
|
||||
"Saving…": "در حال ذخیره…",
|
||||
"Return to home screen": "برگشت به صفحه اصلی",
|
||||
"Remove": "حذف",
|
||||
"Release to stop": "برای توقف رها کنید",
|
||||
"Release spacebar key to stop": "اسپیس بار را برای توقف رها کنید",
|
||||
"Registering…": "ثبتنام…",
|
||||
"Register": "ثبتنام",
|
||||
"Recaptcha not loaded": "کپچا بارگیری نشد",
|
||||
"Recaptcha dismissed": "بازکپچا رد شد",
|
||||
"Press and hold to talk over {{name}}": "برای صحبت فشار دهید و نگهدارید {{name}}",
|
||||
"Press and hold to talk": "برای صحبت فشار دهید و نگهدارید",
|
||||
"Press and hold spacebar to talk over {{name}}": "برای صحبت کردن دکمه اسپیس بار را فشار دهید و نگه دارید {{name}}",
|
||||
"Press and hold spacebar to talk": "برای صحبت کردن کلید فاصله را فشار داده و نگه دارید",
|
||||
"Passwords must match": "رمز عبور باید همخوانی داشته باشد",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "کاربران دیگر تلاش میکنند با ورژنهای ناسازگار به مکالمه بپیوندند. این کاربران باید از بروزرسانی مرورگرشان اطمینان داشته باشند:<1>{userLis}</1>",
|
||||
"Not registered yet? <2>Create an account</2>": "هنوز ثبتنام نکردهاید؟ <2>ساخت حساب کاربری</2>",
|
||||
"Not now, return to home screen": "الان نه، به صفحه اصلی برگردید",
|
||||
"More menu": "تنظیمات بیشتر",
|
||||
"Microphone permissions needed to join the call.": "برای پیوستن به مکالمه دسترسی به میکروفون نیاز است.",
|
||||
"Microphone {{n}}": "میکروفون {{n}}",
|
||||
"Logging in…": "ورود…",
|
||||
"Include debug logs": "شامل لاگهای عیبیابی",
|
||||
"Having trouble? Help us fix it.": "با مشکلی رو به رو شدید؟ به ما کمک کنید رفعش کنیم.",
|
||||
"Grid layout menu": "منوی طرحبندی شبکهای",
|
||||
"Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.",
|
||||
"You can't talk at the same time": "شما نمی توانید همزمان تماس بگیرید",
|
||||
"Yes, join call": "بله، به تماس بپیوندید",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC (ارتباطات رسانهای بلادرنگ مانند انتقال صدا، ویدئو و داده) در این مرورگر پشتیبانی نمیشود یا در حال مسدود شدن است.",
|
||||
"Walkie-talkie call name": "نامِ تماسِ واکی-تاکی",
|
||||
"Walkie-talkie call": "تماسِ واکی-تاکی",
|
||||
"Waiting for other participants…": "در انتظار برای دیگر شرکتکنندگان…",
|
||||
"Waiting for network": "در انتظار شبکه",
|
||||
"Video call name": "نامِ تماسِ تصویری",
|
||||
"Version: {{version}}": "نسخه: {{نسخه}}",
|
||||
"User menu": "فهرست کاربر",
|
||||
"Unmute microphone": "میکروفون را باصدا کنید",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "این کار باعث میشود صدای بلندگو از جایی که کاشیهای آن روی صفحه قرار گرفته است به نظر برسد. (ویژگی آزمایشی: این ممکن است بر پایداری صدا تأثیر بگذارد.)",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "این سایت توسط ReCAPTCHA محافظت می شود و <2>خط مشی رازداری</2> و <6>شرایط خدمات</6> Google اعمال می شود.<9></9>با کلیک کردن بر روی \"ثبت نام\"، شما با <12 >شرایط و ضوابط </12> ما موافقت می کنید",
|
||||
"This call already exists, would you like to join?": "این تماس از قبل وجود دارد، میخواهید بپیوندید؟",
|
||||
"Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.",
|
||||
"Talking…": "در حال صحبت کردن…",
|
||||
"Talk over speaker": "روی بلندگو صحبت کنید",
|
||||
"Submitting feedback…": "در حال ارسال بازخورد…",
|
||||
"Submit feedback": "بازخورد ارائه دهید",
|
||||
"Stop sharing screen": "توقف اشتراکگذاری صفحه نمایش",
|
||||
"Spatial audio": "صدای فضایی"
|
||||
}
|
||||
134
public/locales/fr/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Créer un compte</0> Or <2>Accès invité</2>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Mince, une erreur est survenue.</0><1>Envoyer les journaux de débogage nous aidera à résoudre le problème.</1>",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Pourquoi ne pas créer un mot de passe pour conserver votre compte ?</0><1>Vous pourrez garder votre nom et définir un avatar pour vos futurs appels</1>",
|
||||
"Accept camera/microphone permissions to join the call.": "Autorisez l’accès à votre caméra et microphone pour rejoindre l’appel.",
|
||||
"Accept microphone permissions to join the call.": "Autorisez l’accès au microphone pour rejoindre l’appel.",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.",
|
||||
"Audio": "Audio",
|
||||
"Avatar": "Avatar",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Commencer » vous acceptez nos <2>conditions d’utilisation</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Rejoindre l’appel » vous acceptez nos <2>conditions d’utilisation</2>",
|
||||
"Call link copied": "Lien de l’appel copié",
|
||||
"Call type menu": "Menu de type d’appel",
|
||||
"Camera": "Caméra",
|
||||
"Camera {{n}}": "Caméra {{n}}",
|
||||
"Camera/microphone permissions needed to join the call.": "Accès à la caméra et au microphone requis pour rejoindre l’appel.",
|
||||
"Change layout": "Changer la disposition",
|
||||
"Close": "Fermer",
|
||||
"Confirm password": "Confirmer le mot de passe",
|
||||
"Connection lost": "Connexion interrompue",
|
||||
"Copied!": "Copié !",
|
||||
"Copy and share this call link": "Copier et partager le lien de cet appel",
|
||||
"Create account": "Créer un compte",
|
||||
"Debug log": "Journal de débogage",
|
||||
"Debug log request": "Demande d’un journal de débogage",
|
||||
"Description (optional)": "Description (facultatif)",
|
||||
"Details": "Informations",
|
||||
"Developer": "Développeur",
|
||||
"Display name": "Nom d’affichage",
|
||||
"Download debug logs": "Télécharger les journaux de débogage",
|
||||
"Entering room…": "Entrée dans le salon…",
|
||||
"Exit full screen": "Quitter le plein écran",
|
||||
"Freedom": "Libre",
|
||||
"Full screen": "Plein écran",
|
||||
"Go": "Commencer",
|
||||
"Grid layout menu": "Menu en grille",
|
||||
"Having trouble? Help us fix it.": "Un problème ? Aidez nous à le résoudre.",
|
||||
"Home": "Accueil",
|
||||
"Include debug logs": "Inclure les journaux de débogage",
|
||||
"Incompatible versions": "Versions incompatibles",
|
||||
"Incompatible versions!": "Versions incompatibles !",
|
||||
"Inspector": "Inspecteur",
|
||||
"Invite people": "Inviter des gens",
|
||||
"Join call": "Rejoindre l’appel",
|
||||
"Join call now": "Rejoindre l’appel maintenant",
|
||||
"Join existing call?": "Rejoindre un appel existant ?",
|
||||
"Leave": "Partir",
|
||||
"Loading room…": "Chargement du salon…",
|
||||
"Loading…": "Chargement…",
|
||||
"Local volume": "Volume local",
|
||||
"Logging in…": "Connexion…",
|
||||
"Login": "Connexion",
|
||||
"Login to your account": "Connectez vous à votre compte",
|
||||
"Microphone": "Microphone",
|
||||
"Microphone permissions needed to join the call.": "Accès au microphone requis pour rejoindre l’appel.",
|
||||
"Microphone {{n}}": "Microphone {{n}}",
|
||||
"More": "Plus",
|
||||
"More menu": "Menu plus",
|
||||
"Mute microphone": "Couper le micro",
|
||||
"No": "Non",
|
||||
"Not now, return to home screen": "Pas maintenant, retourner à l’accueil",
|
||||
"Not registered yet? <2>Create an account</2>": "Pas encore de compte ? <2>En créer un</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Des utilisateurs essayent de rejoindre cet appel à partir de versions incompatibles. Ces utilisateurs doivent rafraîchir la page dans leur navigateur : <1>{userLis}</1>",
|
||||
"Password": "Mot de passe",
|
||||
"Passwords must match": "Les mots de passe doivent correspondre",
|
||||
"Press and hold spacebar to talk": "Appuyez et maintenez la barre d’espace enfoncée pour parler",
|
||||
"Press and hold spacebar to talk over {{name}}": "Appuyez et maintenez la barre d’espace enfoncée pour parler par dessus {{name}}",
|
||||
"Press and hold to talk": "Appuyez et maintenez enfoncé pour parler",
|
||||
"Press and hold to talk over {{name}}": "Appuyez et maintenez enfoncé pour parler par dessus {{name}}",
|
||||
"Profile": "Profil",
|
||||
"Recaptcha dismissed": "Recaptcha refusé",
|
||||
"Recaptcha not loaded": "Recaptcha non chargé",
|
||||
"Register": "S’enregistrer",
|
||||
"Registering…": "Enregistrement…",
|
||||
"Release spacebar key to stop": "Relâcher la barre d’espace pour arrêter",
|
||||
"Release to stop": "Relâcher pour arrêter",
|
||||
"Remove": "Supprimer",
|
||||
"Return to home screen": "Retour à l’accueil",
|
||||
"Save": "Enregistrer",
|
||||
"Saving…": "Enregistrement…",
|
||||
"Select an option": "Sélectionnez une option",
|
||||
"Send debug logs": "Envoyer les journaux de débogage",
|
||||
"Sending…": "Envoi…",
|
||||
"Settings": "Paramètres",
|
||||
"Share screen": "Partage d’écran",
|
||||
"Show call inspector": "Afficher l’inspecteur d’appel",
|
||||
"Sign in": "Connexion",
|
||||
"Sign out": "Déconnexion",
|
||||
"Spatial audio": "Audio spatialisé",
|
||||
"Spotlight": "Premier plan",
|
||||
"Stop sharing screen": "Arrêter le partage d’écran",
|
||||
"Submit feedback": "Envoyer des retours",
|
||||
"Submitting feedback…": "Envoi des retours…",
|
||||
"Take me Home": "Retouner à l’accueil",
|
||||
"Talk over speaker": "Parler par dessus l’intervenant",
|
||||
"Thanks! We'll get right on it.": "Merci ! Nous allons nous y attaquer.",
|
||||
"This call already exists, would you like to join?": "Cet appel existe déjà, voulez-vous le rejoindre ?",
|
||||
"{{name}} is presenting": "{{name}} est le présentateur",
|
||||
"Fetching group call timed out.": "Échec de connexion à l’appel de groupe dans le temps imparti.",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} — Appel talkie-walkie",
|
||||
"{{name}} is talking…": "{{name}} est en train de parler…",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, votre appel est désormais terminé",
|
||||
"{{count}} people connected|other": "{{count}} personnes connectées",
|
||||
"{{count}} people connected|one": "{{count}} personne connectée",
|
||||
"Your recent calls": "Appels récents",
|
||||
"You can't talk at the same time": "Vous ne pouvez pas parler en même temps",
|
||||
"Yes, join call": "Oui, rejoindre l’appel",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC n’est pas pris en charge ou est bloqué par ce navigateur.",
|
||||
"Walkie-talkie call name": "Nom de l’appel talkie-walkie",
|
||||
"Walkie-talkie call": "Appel talkie-walkie",
|
||||
"Waiting for other participants…": "En attente d’autres participants…",
|
||||
"Waiting for network": "En attente du réseau",
|
||||
"Video call name": "Nom de l’appel vidéo",
|
||||
"Video call": "Appel vidéo",
|
||||
"Video": "Vidéo",
|
||||
"Version: {{version}}": "Version : {{version}}",
|
||||
"Username": "Nom d’utilisateur",
|
||||
"User menu": "Menu utilisateur",
|
||||
"User ID": "Identifiant utilisateur",
|
||||
"Unmute microphone": "Allumer le micro",
|
||||
"Turn on camera": "Allumer la caméra",
|
||||
"Turn off camera": "Couper la caméra",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Cela donnera l’impression que le son de l’intervenant provient de là où leur tuile est positionnée sur l’écran. (Fonctionnalité expérimentale : ceci pourrait avoir un impact sur la stabilité du son.)",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions d’utilisation</6> de Google s’appliquent.<9></9>En cliquant sur « S’enregistrer » vous acceptez également nos <12>conditions d’utilisation</12>",
|
||||
"Talking…": "Vous parlez…",
|
||||
"Speaker {{n}}": "Intervenant {{n}}",
|
||||
"Speaker": "Intervenant",
|
||||
"Invite": "Inviter",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
|
||||
"Sending debug logs…": "Envoi des journaux de débogage…",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Rejoindre l’appel maintenant</0><1>Ou</1><2>Copier le lien de l’appel et rejoindre plus tard</2>",
|
||||
"{{name}} (Connecting...)": "{{name}} (Connexion…)"
|
||||
}
|
||||
134
public/locales/id/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Sudah punya akun?</0><1><0>Masuk</0> Atau <2>Akses sebagai tamu</2></1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Buat akun</0> Atau <2>Akses sebagai tamu</2>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Aduh, ada yang salah.</0><1>Mengirimkan catatan pengawakutuan akan membantu kami melacak masalahnya.</1>",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Kenapa tidak selesaikan dengan mengatur sebuah kata sandi untuk menjaga akun Anda?</0><1>Anda akan dapat tetap menggunakan nama Anda dan atur sebuah avatar untuk digunakan dalam panggilan di masa mendatang</1>",
|
||||
"Accept camera/microphone permissions to join the call.": "Terima izin kamera/mikrofon untuk bergabung ke panggilan.",
|
||||
"Accept microphone permissions to join the call.": "Terima izin mikrofon untuk bergabung ke panggilan.",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.",
|
||||
"Audio": "Audio",
|
||||
"Avatar": "Avatar",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan</2> kami",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan</2> kami",
|
||||
"Call link copied": "Tautan panggilan disalin",
|
||||
"Call type menu": "Menu jenis panggilan",
|
||||
"Camera": "Kamera",
|
||||
"Camera {{n}}": "Kamera {{n}}",
|
||||
"Camera/microphone permissions needed to join the call.": "Izin kamera/mikrofon dibutuhkan untuk bergabung ke panggilan.",
|
||||
"Change layout": "Ubah tata letak",
|
||||
"Close": "Tutup",
|
||||
"Confirm password": "Konfirmasi kata sandi",
|
||||
"Connection lost": "Koneksi hilang",
|
||||
"Copied!": "Disalin!",
|
||||
"Copy and share this call link": "Salin dan bagikan tautan panggilan ini",
|
||||
"Create account": "Buat akun",
|
||||
"Debug log": "Catatan pengawakutuan",
|
||||
"Debug log request": "Permintaan catatan pengawakutuan",
|
||||
"Description (optional)": "Deskripsi (opsional)",
|
||||
"Details": "Detail",
|
||||
"Developer": "Pengembang",
|
||||
"Display name": "Nama tampilan",
|
||||
"Download debug logs": "Unduh catatan pengawakutuan",
|
||||
"Entering room…": "Memasuki ruangan…",
|
||||
"Exit full screen": "Keluar dari layar penuh",
|
||||
"Fetching group call timed out.": "Waktu pendapatan panggilan grup habis.",
|
||||
"Freedom": "Bebas",
|
||||
"Full screen": "Layar penuh",
|
||||
"Go": "Bergabung",
|
||||
"Grid layout menu": "Menu tata letak kisi",
|
||||
"Having trouble? Help us fix it.": "Mengalami masalah? Bantu kami memperbaikinya.",
|
||||
"Home": "Beranda",
|
||||
"Include debug logs": "Termasuk catatan pengawakutuan",
|
||||
"Incompatible versions": "Versi tidak kompatibel",
|
||||
"Incompatible versions!": "Versi tidak kompatibel!",
|
||||
"Inspector": "Inspektur",
|
||||
"Invite": "Undang",
|
||||
"Invite people": "Undang orang",
|
||||
"Join call": "Bergabung ke panggilan",
|
||||
"Join call now": "Bergabung ke panggilan sekarang",
|
||||
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
|
||||
"Leave": "Keluar",
|
||||
"Loading room…": "Memuat ruangan…",
|
||||
"Loading…": "Memuat…",
|
||||
"Local volume": "Volume lokal",
|
||||
"Logging in…": "Memasuki…",
|
||||
"Login": "Masuk",
|
||||
"Login to your account": "Masuk ke akun Anda",
|
||||
"Microphone": "Mikrofon",
|
||||
"Microphone permissions needed to join the call.": "Izin mikrofon dibutuhkan untuk bergabung ke panggilan ini.",
|
||||
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||
"More": "Lainnya",
|
||||
"More menu": "Menu lainnya",
|
||||
"Mute microphone": "Bisukan mikrofon",
|
||||
"No": "Tidak",
|
||||
"Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda",
|
||||
"Not registered yet? <2>Create an account</2>": "Belum terdaftar? <2>Buat sebuah akun</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Pengguna lain sedang mencoba bergabung ke panggilan ini dari versi yang tidak kompatibel. Pengguna berikut seharusnya memastikan bahwa mereka telah memuat ulang peramban mereka: <1>{userLis}</1>",
|
||||
"Password": "Kata sandi",
|
||||
"Passwords must match": "Kata sandi harus cocok",
|
||||
"Press and hold spacebar to talk": "Tekan dan tahan bilah spasi untuk berbicara",
|
||||
"Press and hold spacebar to talk over {{name}}": "Tekan dan tahan bilah spasi untuk berbicara pada {{name}}",
|
||||
"Press and hold to talk": "Tekan dan tahan untuk berbicara",
|
||||
"Press and hold to talk over {{name}}": "Tekan dan tahan untuk berbicara pada {{name}}",
|
||||
"Profile": "Profil",
|
||||
"Recaptcha dismissed": "Recaptcha ditutup",
|
||||
"Recaptcha not loaded": "Recaptcha tidak dimuat",
|
||||
"Register": "Daftar",
|
||||
"Registering…": "Mendaftarkan…",
|
||||
"Release spacebar key to stop": "Lepaskan bilah spasi untuk berhenti",
|
||||
"Release to stop": "Lepaskan untuk berhenti",
|
||||
"Remove": "Hapus",
|
||||
"Return to home screen": "Kembali ke layar beranda",
|
||||
"Save": "Simpan",
|
||||
"Saving…": "Menyimpan…",
|
||||
"Select an option": "Pilih sebuah opsi",
|
||||
"Send debug logs": "Kirim catatan pengawakutuan",
|
||||
"Sending…": "Mengirimkan…",
|
||||
"Settings": "Pengaturan",
|
||||
"Share screen": "Bagikan layar",
|
||||
"Show call inspector": "Tampilkan inspektur panggilan",
|
||||
"Sign in": "Masuk",
|
||||
"Sign out": "Keluar",
|
||||
"Spatial audio": "Audio spasial",
|
||||
"Speaker": "Pembicara",
|
||||
"Speaker {{n}}": "Pembicara {{n}}",
|
||||
"Spotlight": "Sorotan",
|
||||
"Stop sharing screen": "Berhenti membagikan layar",
|
||||
"Submit feedback": "Kirim masukan",
|
||||
"Submitting feedback…": "Mengirimkan masukan…",
|
||||
"Take me Home": "Bawa saya ke Beranda",
|
||||
"Talk over speaker": "Bicara pada pembicara",
|
||||
"Talking…": "Berbicara…",
|
||||
"Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.",
|
||||
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan</12> kami",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Ini akan membuat suara pembicara seolah-olah berasal dari tempat ubin mereka diposisikan di layar. (Fitur uji coba: ini dapat memengaruhi stabilitas audio.)",
|
||||
"Turn off camera": "Matikan kamera",
|
||||
"Turn on camera": "Nyalakan kamera",
|
||||
"Unmute microphone": "Suarakan mikrofon",
|
||||
"User ID": "ID pengguna",
|
||||
"User menu": "Menu pengguna",
|
||||
"Username": "Nama pengguna",
|
||||
"Version: {{version}}": "Versi: {{version}}",
|
||||
"Video": "Video",
|
||||
"Video call": "Panggilan video",
|
||||
"Video call name": "Nama panggilan video",
|
||||
"Waiting for network": "Menunggu jaringan",
|
||||
"Waiting for other participants…": "Menunggu peserta lain…",
|
||||
"Walkie-talkie call": "Panggilan protofon",
|
||||
"Walkie-talkie call name": "Nama panggilan protofon",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC tidak didukung atau diblokir di peramban ini.",
|
||||
"Yes, join call": "Ya, bergabung ke panggilan",
|
||||
"You can't talk at the same time": "Anda tidak dapat berbicara pada waktu yang sama",
|
||||
"Your recent calls": "Panggilan Anda terkini",
|
||||
"{{count}} people connected|one": "{{count}} orang terhubung",
|
||||
"{{count}} people connected|other": "{{count}} orang terhubung",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, panggilan Anda sekarang telah berakhir",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is presenting": "{{name}} sedang mempresentasi",
|
||||
"{{name}} is talking…": "{{name}} sedang berbicara…",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Panggilan protofon",
|
||||
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
|
||||
"{{name}} (Connecting...)": "{{name}} (Menghubungkan...)"
|
||||
}
|
||||
12
public/locales/ko/app.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "",
|
||||
"{{count}} people connected|one": "{{count}}명 연결됨",
|
||||
"{{count}} people connected|other": "{{count}}명 연결됨",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}님, 전화가 종료되었습니다",
|
||||
"{{names}}, {{name}}": "{{names}}님, {{name}}님",
|
||||
"{{name}} is presenting": "{{name}}님이 발표 중",
|
||||
"{{name}} is talking…": "{{name}}님이 말하는 중…",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - 워키토키 전화"
|
||||
}
|
||||
130
public/locales/pl/app.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"More menu": "Menu \"więcej\"",
|
||||
"Login": "Zaloguj się",
|
||||
"Go": "Kontynuuj",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Warunki</2>",
|
||||
"{{count}} people connected|other": "{{count}} ludzi połączono",
|
||||
"Your recent calls": "Twoje ostatnie połączenia",
|
||||
"You can't talk at the same time": "Nie możesz mówić w tym samym czasie",
|
||||
"Yes, join call": "Tak, dołącz do połączenia",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC jest niewspierane lub zablokowane w tej przeglądarce.",
|
||||
"Walkie-talkie call name": "Nazwa połączenia walkie-talkie",
|
||||
"Walkie-talkie call": "Połączenie walkie-talkie",
|
||||
"Waiting for other participants…": "Oczekiwanie na pozostałych uczestników…",
|
||||
"Waiting for network": "Oczekiwanie na sieć",
|
||||
"Video call name": "Nazwa połączenia wideo",
|
||||
"Video call": "Połączenie wideo",
|
||||
"Video": "Wideo",
|
||||
"Version: {{version}}": "Wersja: {{version}}",
|
||||
"Username": "Nazwa użytkownika",
|
||||
"User menu": "Menu użytkownika",
|
||||
"User ID": "ID użytkownika",
|
||||
"Unmute microphone": "Wyłącz wyciszenie mikrofonu",
|
||||
"Turn on camera": "Włącz kamerę",
|
||||
"Turn off camera": "Wyłącz kamerę",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Sprawi to, że dźwięk mówcy będzie zdawał się dochodzić z jego miejsca na ekranie. (Funkcja eksperymentalna: może mieć wpływ na stabilność dźwięku.)",
|
||||
"This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
|
||||
"Thanks! We'll get right on it.": "Dziękujemy! Zaraz się tym zajmiemy.",
|
||||
"Talking…": "Mówienie…",
|
||||
"Take me Home": "Zabierz mnie do ekranu startowego",
|
||||
"Submitting feedback…": "Przesyłanie opinii…",
|
||||
"Submit feedback": "Prześlij opinię",
|
||||
"Stop sharing screen": "Zatrzymaj udostępnianie ekranu",
|
||||
"Spotlight": "Centrum uwagi",
|
||||
"Speaker {{n}}": "Głośnik {{n}}",
|
||||
"Speaker": "Głośnik",
|
||||
"Spatial audio": "Dźwięk przestrzenny",
|
||||
"Sign out": "Wyloguj się",
|
||||
"Sign in": "Zaloguj się",
|
||||
"Show call inspector": "Pokaż inspektora połączenia",
|
||||
"Share screen": "Udostępnij ekran",
|
||||
"Settings": "Ustawienia",
|
||||
"Sending…": "Wysyłanie…",
|
||||
"Sending debug logs…": "Wysyłanie dzienników debugowania…",
|
||||
"Send debug logs": "Wyślij dzienniki debugowania",
|
||||
"Select an option": "Wybierz opcję",
|
||||
"Saving…": "Zapisywanie…",
|
||||
"Save": "Zapisz",
|
||||
"Return to home screen": "Powróć do ekranu domowego",
|
||||
"Remove": "Usuń",
|
||||
"Release to stop": "Puść przycisk, aby przestać",
|
||||
"Release spacebar key to stop": "Puść spację, aby przestać",
|
||||
"Registering…": "Rejestrowanie…",
|
||||
"Register": "Zarejestruj",
|
||||
"Recaptcha not loaded": "Recaptcha nie została załadowana",
|
||||
"Recaptcha dismissed": "Recaptcha odrzucona",
|
||||
"Profile": "Profil",
|
||||
"Press and hold to talk over {{name}}": "Przytrzymaj, aby mówić wraz z {{name}}",
|
||||
"Press and hold to talk": "Przytrzymaj, aby mówić",
|
||||
"Press and hold spacebar to talk over {{name}}": "Przytrzymaj spację, aby mówić wraz z {{name}}",
|
||||
"Press and hold spacebar to talk": "Przytrzymaj spację, aby mówić",
|
||||
"Passwords must match": "Hasła muszą być identyczne",
|
||||
"Password": "Hasło",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Inni użytkownicy próbują dołączyć do tego połączenia przy użyciu niekompatybilnych wersji. Powinni oni upewnić się, że odświeżyli stronę w swoich przeglądarkach:<1>{userLis}</1>",
|
||||
"Not registered yet? <2>Create an account</2>": "Nie masz konta? <2>Utwórz je</2>",
|
||||
"Not now, return to home screen": "Nie teraz, powróć do ekranu domowego",
|
||||
"No": "Nie",
|
||||
"Mute microphone": "Wycisz mikrofon",
|
||||
"More": "Więcej",
|
||||
"Microphone permissions needed to join the call.": "Aby dołączyć do połączenia, potrzebne są uprawnienia do mikrofonu.",
|
||||
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||
"Microphone": "Mikrofon",
|
||||
"Login to your account": "Zaloguj się do swojego konta",
|
||||
"Logging in…": "Logowanie…",
|
||||
"Local volume": "Lokalna głośność",
|
||||
"Loading…": "Ładowanie…",
|
||||
"Loading room…": "Ładowanie pokoju…",
|
||||
"Leave": "Opuść",
|
||||
"Join existing call?": "Dołączyć do istniejącego połączenia?",
|
||||
"Join call now": "Dołącz do połączenia teraz",
|
||||
"Join call": "Dołącz do połączenia",
|
||||
"Invite people": "Zaproś ludzi",
|
||||
"Invite": "Zaproś",
|
||||
"Inspector": "Inspektor",
|
||||
"Incompatible versions!": "Niekompatybilne wersje!",
|
||||
"Incompatible versions": "Niekompatybilne wersje",
|
||||
"Include debug logs": "Dołącz dzienniki debugowania",
|
||||
"Home": "Strona domowa",
|
||||
"Having trouble? Help us fix it.": "Masz problem? Pomóż nam go naprawić.",
|
||||
"Grid layout menu": "Menu układu siatki",
|
||||
"Full screen": "Pełen ekran",
|
||||
"Freedom": "Wolność",
|
||||
"Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.",
|
||||
"Exit full screen": "Zamknij pełny ekran",
|
||||
"Entering room…": "Wchodzenie do pokoju…",
|
||||
"Download debug logs": "Pobierz dzienniki debugowania",
|
||||
"Display name": "Wyświetlana nazwa",
|
||||
"Developer": "Deweloper",
|
||||
"Details": "Szczegóły",
|
||||
"Description (optional)": "Opis (opcjonalny)",
|
||||
"Debug log request": "Prośba o dzienniki debugowania",
|
||||
"Debug log": "Dzienniki debugowania",
|
||||
"Create account": "Utwórz konto",
|
||||
"Copy and share this call link": "Skopiuj i podziel się linkiem do połączenia",
|
||||
"Copied!": "Skopiowano!",
|
||||
"Connection lost": "Połączenie utracone",
|
||||
"Confirm password": "Potwierdź hasło",
|
||||
"Close": "Zamknij",
|
||||
"Change layout": "Zmień układ",
|
||||
"Camera/microphone permissions needed to join the call.": "Aby dołączyć do tego połączenia, potrzebne są uprawnienia do kamery/mikrofonu.",
|
||||
"Camera {{n}}": "Kamera {{n}}",
|
||||
"Camera": "Kamera",
|
||||
"Call type menu": "Menu rodzaju połączenia",
|
||||
"Call link copied": "Skopiowano link do połączenia",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Warunki</2>",
|
||||
"Avatar": "Awatar",
|
||||
"Audio": "Dźwięk",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Inny użytkownik w tym połączeniu napotkał problem. Aby lepiej zdiagnozować tę usterkę, chcielibyśmy zebrać dzienniki debugowania.",
|
||||
"Accept microphone permissions to join the call.": "Przyznaj uprawnienia do mikrofonu aby dołączyć do połączenia.",
|
||||
"Accept camera/microphone permissions to join the call.": "Przyznaj uprawnienia do kamery/mikrofonu aby dołączyć do połączenia.",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, coś poszło nie tak.</0><1>Przesłanie dzienników debugowania pomoże nam odnaleźć ten błąd.</1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> Albo <2>Dołącz jako gość</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> Albo <2>Dołącz jako gość</2></1>",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - połączenie walkie-talkie",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is talking…": "{{name}} mówi…",
|
||||
"{{name}} is presenting": "{{name}} prezentuje",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, twoje połączenie zostało zakończone",
|
||||
"{{count}} people connected|one": "{{count}} osoba połączona"
|
||||
}
|
||||
132
public/locales/ru/app.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"Register": "Зарегистрироваться",
|
||||
"Saving…": "Сохранение…",
|
||||
"Registering…": "Регистрация…",
|
||||
"Logging in…": "Вход…",
|
||||
"Entering room…": "Вход в комнату…",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"Waiting for other participants…": "Ожидание других участников…",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Эта функция балансирует звук к расположению плитки на экране. (Экспериментальная функция: может повлиять на стабильность аудио.)",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Этот сайт защищён ReCAPTCHA от Google, ознакомьтесь с их <2>Политикой конфиденциальности</2> и <6>Пользовательским соглашением</6>.<9></9>Нажимая \"Зарегистрироваться\", вы также принимаете наши <12>Положения и условия</12>.",
|
||||
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
|
||||
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
|
||||
"Talking…": "Говорите…",
|
||||
"Submitting feedback…": "Отправка отзыва…",
|
||||
"Submit feedback": "Отправить отзыв",
|
||||
"Sending debug logs…": "Отправка журнала отладки…",
|
||||
"Select an option": "Выберите вариант",
|
||||
"Release to stop": "Отпустите, чтобы прекратить вещание",
|
||||
"Release spacebar key to stop": "Чтобы прекратить вещание, отпустите [Пробел]",
|
||||
"Press and hold to talk over {{name}}": "Зажмите, чтобы говорить поверх участника {{name}}",
|
||||
"Press and hold spacebar to talk over {{name}}": "Чтобы говорить поверх участника {{name}}, нажмите и удерживайте [Пробел]",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Другие пользователи пытаются присоединиться с неподдерживаемых версий программы. Этим участникам надо перезагрузить браузер: <1>{userLis}</1>",
|
||||
"Grid layout menu": "Меню \"Расположение сеткой\"",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Присоединиться сейчас\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>",
|
||||
"Your recent calls": "Ваши недавние звонки",
|
||||
"You can't talk at the same time": "Вы не можете говорить одновременно",
|
||||
"Yes, join call": "Да, присоединиться",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не поддерживается или заблокирован в этом браузере.",
|
||||
"Walkie-talkie call name": "Название звонка-рации",
|
||||
"Walkie-talkie call": "Звонок-рация",
|
||||
"Waiting for network": "Ожидание сети",
|
||||
"Video call name": "Название видео-звонка",
|
||||
"Video call": "Видео-звонок",
|
||||
"Video": "Видео",
|
||||
"Version: {{version}}": "Версия: {{version}}",
|
||||
"Username": "Имя пользователя",
|
||||
"User menu": "Меню пользователя",
|
||||
"User ID": "ID пользователя",
|
||||
"Unmute microphone": "Включить микрофон",
|
||||
"Turn on camera": "Включить камеру",
|
||||
"Turn off camera": "Отключить камеру",
|
||||
"Talk over speaker": "Говорить через динамик",
|
||||
"Take me Home": "Перейти в Начало",
|
||||
"Stop sharing screen": "Остановить показ экрана",
|
||||
"Spotlight": "Внимание",
|
||||
"Speaker {{n}}": "Динамик {{n}}",
|
||||
"Speaker": "Динамик",
|
||||
"Spatial audio": "Пространственное аудио",
|
||||
"Sign out": "Выйти",
|
||||
"Sign in": "Войти",
|
||||
"Show call inspector": "Показать инспектор",
|
||||
"Share screen": "Поделиться экраном",
|
||||
"Settings": "Настройки",
|
||||
"Sending…": "Отправка…",
|
||||
"Local volume": "Местная громкость",
|
||||
"Call type menu": "Меню \"Тип звонка\"",
|
||||
"More menu": "Полное меню",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Звонок-рация",
|
||||
"Include debug logs": "Приложить журнал отладки",
|
||||
"Download debug logs": "Скачать журнал отладки",
|
||||
"Debug log request": "Запрос журнала отладки",
|
||||
"Debug log": "Журнал отладки",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ой, что-то пошло не так.</0><1>Отправив журнал отладки, вы поможете нам найти проблемный участок.</1>",
|
||||
"Send debug logs": "Отправить журнал отладки",
|
||||
"Save": "Сохранить",
|
||||
"Return to home screen": "Вернуться в Начало",
|
||||
"Remove": "Удалить",
|
||||
"Recaptcha not loaded": "Невозможно начать проверку",
|
||||
"Recaptcha dismissed": "Проверка не пройдена",
|
||||
"Profile": "Профиль",
|
||||
"Press and hold to talk": "Зажмите, чтобы говорить",
|
||||
"Press and hold spacebar to talk": "Чтобы говорить, нажмите и удерживайте [Пробел]",
|
||||
"Passwords must match": "Пароли должны совпадать",
|
||||
"Password": "Пароль",
|
||||
"Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>",
|
||||
"Not now, return to home screen": "Не сейчас, вернуться в Начало",
|
||||
"No": "Нет",
|
||||
"Mute microphone": "Отключить микрофон",
|
||||
"More": "Больше",
|
||||
"Microphone permissions needed to join the call.": "Нужно разрешение на доступ к микрофону для присоединения к звонку.",
|
||||
"Microphone {{n}}": "Микрофон {{n}}",
|
||||
"Microphone": "Микрофон",
|
||||
"Login to your account": "Войдите в свой аккаунт",
|
||||
"Login": "Вход",
|
||||
"Loading…": "Загрузка…",
|
||||
"Loading room…": "Загрузка комнаты…",
|
||||
"Leave": "Покинуть",
|
||||
"Join existing call?": "Присоединиться к существующему звонку?",
|
||||
"Join call now": "Присоединиться сейчас",
|
||||
"Join call": "Присоединиться",
|
||||
"Invite people": "Пригласить участников",
|
||||
"Invite": "Пригласить",
|
||||
"Inspector": "Инспектор",
|
||||
"Incompatible versions!": "Несовместимые версии!",
|
||||
"Incompatible versions": "Несовместимые версии",
|
||||
"Home": "Начало",
|
||||
"Having trouble? Help us fix it.": "Есть проблема? Помогите нам её устранить.",
|
||||
"Go": "Далее",
|
||||
"Full screen": "Полноэкранный режим",
|
||||
"Freedom": "Свобода",
|
||||
"Fetching group call timed out.": "Истекло время ожидания для группового звонка.",
|
||||
"Exit full screen": "Выйти из полноэкранного режима",
|
||||
"Display name": "Видимое имя",
|
||||
"Developer": "Разработчику",
|
||||
"Details": "Подробности",
|
||||
"Description (optional)": "Описание (необязательно)",
|
||||
"Create account": "Создать аккаунт",
|
||||
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
|
||||
"Copied!": "Скопировано!",
|
||||
"Connection lost": "Соединение потеряно",
|
||||
"Confirm password": "Подтвердите пароль",
|
||||
"Close": "Закрыть",
|
||||
"Change layout": "Изменить расположение",
|
||||
"Camera/microphone permissions needed to join the call.": "Нужны разрешения на доступ к камере/микрофону для присоединения к звонку.",
|
||||
"Camera {{n}}": "Камера {{n}}",
|
||||
"Camera": "Камера",
|
||||
"Call link copied": "Ссылка на звонок скопирована",
|
||||
"Avatar": "Аватар",
|
||||
"Audio": "Аудио",
|
||||
"Accept microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к микрофону.",
|
||||
"Accept camera/microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к камере/микрофону.",
|
||||
"{{name}} is talking…": "{{name}} говорит…",
|
||||
"{{name}} is presenting": "{{name}} показывает",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, ваш звонок завершён",
|
||||
"{{count}} people connected|other": "{{count}} подключилось",
|
||||
"{{count}} people connected|one": "{{count}} подключился"
|
||||
}
|
||||
102
public/locales/tr/app.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Hesabınızı tutmak için niye bir parola açmıyorsunuz?</0><1>Böylece ileriki aramalarda adınızı ve avatarınızı kullanabileceksiniz</1>",
|
||||
"Accept camera/microphone permissions to join the call.": "Aramaya katılmanız için kamera/mikrofon erişimine izin verin.",
|
||||
"Accept microphone permissions to join the call.": "Aramaya katılmak için mikrofon erişim izni verin.",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.",
|
||||
"Audio": "Ses",
|
||||
"Avatar": "Avatar",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "\"Git\"e tıklayarak,<2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
|
||||
"Call link copied": "Arama bağlantısı kopyalandı",
|
||||
"Call type menu": "Arama tipi menüsü",
|
||||
"Camera": "Kamera",
|
||||
"Camera {{n}}": "{{n}}. kamera",
|
||||
"Camera/microphone permissions needed to join the call.": "Aramaya katılmak için kamera/mikrofon izinleri gerek.",
|
||||
"Change layout": "Yerleşimi değiştir",
|
||||
"Close": "Kapat",
|
||||
"Confirm password": "Parolayı tekrar edin",
|
||||
"Connection lost": "Bağlantı koptu",
|
||||
"Copied!": "Kopyalandı",
|
||||
"Copy and share this call link": "Arama bağlantısını kopyala ve paylaş",
|
||||
"Create account": "Hesap aç",
|
||||
"Debug log": "Hata ayıklama kütüğü",
|
||||
"Debug log request": "Hata ayıklama kütük istemi",
|
||||
"Description (optional)": "Tanım (isteğe bağlı)",
|
||||
"Details": "Ayrıntı",
|
||||
"Developer": "Geliştirici",
|
||||
"Display name": "Ekran adı",
|
||||
"Download debug logs": "Hata ayıklama kütüğünü indir",
|
||||
"Entering room…": "Odaya giriliyor…",
|
||||
"Exit full screen": "Tam ekranı terk et",
|
||||
"Fetching group call timed out.": "Grup çağrısı zaman aşımına uğradı.",
|
||||
"Freedom": "Özgürlük",
|
||||
"Full screen": "Tam ekran",
|
||||
"Go": "Git",
|
||||
"Grid layout menu": "Izgara plan menü",
|
||||
"Having trouble? Help us fix it.": "Sorun mu var? Çözmemize yardım edin.",
|
||||
"Home": "Ev",
|
||||
"Include debug logs": "Hata ayıklama kütüğünü dahil et",
|
||||
"Incompatible versions": "Uyumsuz sürümler",
|
||||
"Incompatible versions!": "Sürüm uyumsuz!",
|
||||
"Inspector": "Denetçi",
|
||||
"Invite people": "Kişileri davet et",
|
||||
"Join call": "Aramaya katıl",
|
||||
"Join call now": "Aramaya katıl",
|
||||
"Join existing call?": "Mevcut aramaya katıl?",
|
||||
"Leave": "Çık",
|
||||
"Loading room…": "Oda yükleniyor…",
|
||||
"Loading…": "Yükleniyor…",
|
||||
"Local volume": "Yerel ses seviyesi",
|
||||
"Logging in…": "Giriliyor…",
|
||||
"Login": "Gir",
|
||||
"Login to your account": "Hesabınıza girin",
|
||||
"Microphone": "Mikrofon",
|
||||
"Microphone permissions needed to join the call.": "Aramaya katılmak için mikrofon erişim izni gerek.",
|
||||
"Microphone {{n}}": "{{n}}. mikrofon",
|
||||
"More": "Daha",
|
||||
"More menu": "Daha fazla",
|
||||
"Mute microphone": "Mikrofonu kapat",
|
||||
"No": "Hayır",
|
||||
"Not now, return to home screen": "Şimdi değil, ev ekranına dön",
|
||||
"Not registered yet? <2>Create an account</2>": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Başka kullanıcılar uyumsuz sürümden katılmaya çalışıyorlar. <1>{userLis}</1> tarayıcılarını mutlaka tazelemeliler.",
|
||||
"Password": "Parola",
|
||||
"Passwords must match": "Parolalar aynı olmalı",
|
||||
"Press and hold spacebar to talk": "Konuşmak için boşluk çubuğunu basılı tutun",
|
||||
"Press and hold to talk": "Konuşmak için basılı tutun",
|
||||
"Recaptcha dismissed": "reCAPTCHA atlandı",
|
||||
"Recaptcha not loaded": "reCAPTCHA yüklenmedi",
|
||||
"Register": "Kaydol",
|
||||
"Registering…": "Kaydediyor…",
|
||||
"Release spacebar key to stop": "Kesmek için boşluk tuşunu bırakın",
|
||||
"Release to stop": "Kesmek için bırakın",
|
||||
"Remove": "Çıkar",
|
||||
"Return to home screen": "Ev ekranına geri dön",
|
||||
"Save": "Kaydet",
|
||||
"Saving…": "Kaydediliyor…",
|
||||
"Select an option": "Bir seçenek seç",
|
||||
"Send debug logs": "Hata ayıklama kütüğünü gönder",
|
||||
"Sending…": "Gönderiliyor…",
|
||||
"Settings": "Ayarlar",
|
||||
"Share screen": "Ekran paylaş",
|
||||
"Show call inspector": "Arama denetçisini göster",
|
||||
"Sign in": "Gir",
|
||||
"Sign out": "Çık",
|
||||
"Spatial audio": "Uzamsal ses",
|
||||
"Stop sharing screen": "Ekran paylaşmayı terk et",
|
||||
"Submit feedback": "Geri bildirim ver",
|
||||
"Submitting feedback…": "Geri bildirimler gönderiliyor…",
|
||||
"Take me Home": "Ev ekranına gir",
|
||||
"Talking…": "Konuşuyor…",
|
||||
"Thanks! We'll get right on it.": "Sağol! Bununla ilgileneceğiz.",
|
||||
"This call already exists, would you like to join?": "Bu arama zaten var, katılmak ister misiniz?",
|
||||
"{{count}} people connected|one": "{{count}} kişi bağlı",
|
||||
"{{count}} people connected|other": "{{count}} kişi bağlı",
|
||||
"{{displayName}}, your call is now ended": "Aramanız bitti, {{displayName]}!",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is presenting": "{{name}} sunuyor",
|
||||
"{{name}} is talking…": "{{name}} konuşuyor…",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoop, bir şeyler yanlış.</0><1>Hata ayıklama kütüğünü göndermek sorunu incelememize yardımcı olur.</1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Hesap oluştur</0> yahut <2>Konuk olarak gir</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Mevcut hesabınız mı var?</0><1><0>Gir</0> yahut <2>Konuk girişi</2></1>"
|
||||
}
|
||||
134
public/locales/uk/app.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"Loading…": "Завантаження…",
|
||||
"Your recent calls": "Ваші недавні виклики",
|
||||
"You can't talk at the same time": "Не можна говорити одночасно",
|
||||
"Yes, join call": "Так, приєднатися до виклику",
|
||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не підтримується або блокується в цьому браузері.",
|
||||
"Walkie-talkie call name": "Назва виклику-рації",
|
||||
"Walkie-talkie call": "Виклик-рація",
|
||||
"Waiting for other participants…": "Очікування на інших учасників…",
|
||||
"Waiting for network": "Очікування мережі",
|
||||
"Video call name": "Назва відеовиклику",
|
||||
"Video call": "Відеовиклик",
|
||||
"Video": "Відео",
|
||||
"Version: {{version}}": "Версія: {{version}}",
|
||||
"Username": "Ім'я користувача",
|
||||
"User menu": "Меню користувача",
|
||||
"User ID": "ID користувача",
|
||||
"Unmute microphone": "Увімкнути мікрофон",
|
||||
"Turn on camera": "Увімкнути камеру",
|
||||
"Turn off camera": "Вимкнути камеру",
|
||||
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Це призведе до того, що звук мовця здаватиметься таким, ніби він надходить з того місця, де розміщено його плитку на екрані. (Експериментальна можливість: це може вплинути на стабільність звуку.)",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи кнопку «Зареєструватися», ви погоджуєтеся з нашими <12>Умовами та положеннями</12>",
|
||||
"This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?",
|
||||
"Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.",
|
||||
"Talking…": "Говоріть…",
|
||||
"Talk over speaker": "Говорити через динамік",
|
||||
"Take me Home": "Перейти до Домівки",
|
||||
"Submitting feedback…": "Надсилання відгуку…",
|
||||
"Submit feedback": "Надіслати відгук",
|
||||
"Stop sharing screen": "Припинити показ екрана",
|
||||
"Spotlight": "У центрі уваги",
|
||||
"Speaker {{n}}": "Динамік {{n}}",
|
||||
"Speaker": "Динамік",
|
||||
"Spatial audio": "Просторовий звук",
|
||||
"Sign out": "Вийти",
|
||||
"Sign in": "Увійти",
|
||||
"Show call inspector": "Показати інспектора виклику",
|
||||
"Share screen": "Поділитися екраном",
|
||||
"Settings": "Налаштування",
|
||||
"Sending…": "Надсилання…",
|
||||
"Sending debug logs…": "Надсилання журналу зневадження…",
|
||||
"Send debug logs": "Надіслати журнал зневадження",
|
||||
"Select an option": "Вибрати опцію",
|
||||
"Saving…": "Збереження…",
|
||||
"Save": "Зберегти",
|
||||
"Return to home screen": "Повернутися на екран домівки",
|
||||
"Remove": "Вилучити",
|
||||
"Release to stop": "Відпустіть, щоб закінчити",
|
||||
"Release spacebar key to stop": "Відпустіть пробіл, щоб закінчити",
|
||||
"Registering…": "Реєстрація…",
|
||||
"Register": "Зареєструватися",
|
||||
"Recaptcha not loaded": "Recaptcha не завантажено",
|
||||
"Recaptcha dismissed": "Recaptcha не пройдено",
|
||||
"Profile": "Профіль",
|
||||
"Press and hold to talk over {{name}}": "Затисніть, щоб говорити одночасно з {{name}}",
|
||||
"Press and hold to talk": "Затисніть, щоб говорити",
|
||||
"Press and hold spacebar to talk over {{name}}": "Щоб говорити одночасно з {{name}}, затисніть пробіл",
|
||||
"Press and hold spacebar to talk": "Затисніть пробіл, щоб говорити",
|
||||
"Passwords must match": "Паролі відрізняються",
|
||||
"Password": "Пароль",
|
||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Інші користувачі намагаються приєднатися до цього виклику з несумісних версій. Ці користувачі повинні переконатися, що вони оновили сторінки своїх браузерів:<1>{userLis}</1>",
|
||||
"Not registered yet? <2>Create an account</2>": "Ще не зареєстровані? <2>Створіть обліковий запис</2>",
|
||||
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
|
||||
"No": "Ні",
|
||||
"Mute microphone": "Заглушити мікрофон",
|
||||
"More menu": "Усе меню",
|
||||
"More": "Докладніше",
|
||||
"Microphone permissions needed to join the call.": "Для участі у виклику необхідний дозвіл на користування мікрофоном.",
|
||||
"Microphone {{n}}": "Мікрофон {{n}}",
|
||||
"Microphone": "Мікрофон",
|
||||
"Login to your account": "Увійдіть до свого облікового запису",
|
||||
"Login": "Увійти",
|
||||
"Logging in…": "Вхід…",
|
||||
"Local volume": "Локальна гучність",
|
||||
"Loading room…": "Завантаження кімнати…",
|
||||
"Leave": "Вийти",
|
||||
"Join existing call?": "Приєднатися до наявного виклику?",
|
||||
"Join call now": "Приєднатися до виклику зараз",
|
||||
"Join call": "Приєднатися до виклику",
|
||||
"Invite people": "Запросити людей",
|
||||
"Invite": "Запросити",
|
||||
"Inspector": "Інспектор",
|
||||
"Incompatible versions!": "Несумісні версії!",
|
||||
"Incompatible versions": "Несумісні версії",
|
||||
"Include debug logs": "Долучити журнали зневадження",
|
||||
"Home": "Домівка",
|
||||
"Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.",
|
||||
"Grid layout menu": "Меню у вигляді сітки",
|
||||
"Go": "Далі",
|
||||
"Full screen": "Повноекранний режим",
|
||||
"Freedom": "Свобода",
|
||||
"Fetching group call timed out.": "Вичерпано час очікування групового виклику.",
|
||||
"Exit full screen": "Вийти з повноекранного режиму",
|
||||
"Entering room…": "Вхід у кімнату…",
|
||||
"Download debug logs": "Завантажити журнали зневадження",
|
||||
"Display name": "Показуване ім'я",
|
||||
"Developer": "Розробнику",
|
||||
"Details": "Подробиці",
|
||||
"Description (optional)": "Опис (необов'язково)",
|
||||
"Debug log request": "Запит журналу зневадження",
|
||||
"Debug log": "Журнал зневадження",
|
||||
"Create account": "Створити обліковий запис",
|
||||
"Copy and share this call link": "Скопіювати та поділитися цим посиланням на виклик",
|
||||
"Copied!": "Скопійовано!",
|
||||
"Connection lost": "З'єднання розірвано",
|
||||
"Confirm password": "Підтвердити пароль",
|
||||
"Close": "Закрити",
|
||||
"Change layout": "Змінити макет",
|
||||
"Camera/microphone permissions needed to join the call.": "Для приєднання до виклику необхідні дозволи камери/мікрофона.",
|
||||
"Camera {{n}}": "Камера {{n}}",
|
||||
"Camera": "Камера",
|
||||
"Call type menu": "Меню типу виклику",
|
||||
"Call link copied": "Посилання на виклик скопійовано",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Приєднатися до виклику зараз», ви погодитеся з нашими <2>Умовами та положеннями</2>",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Далі», ви погодитеся з нашими <2>Умовами та положеннями</2>",
|
||||
"Avatar": "Аватар",
|
||||
"Audio": "Звук",
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал зневадження.",
|
||||
"Accept microphone permissions to join the call.": "Надайте дозволи на використання мікрофонів для приєднання до виклику.",
|
||||
"Accept camera/microphone permissions to join the call.": "Надайте дозвіл на використання камери/мікрофона для приєднання до виклику.",
|
||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?</0><1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів</1>",
|
||||
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Халепа, щось пішло не так.</0><1>Надсилання журналів зневадження допоможе нам виявити проблему.</1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Створити обліковий запис</0> або <2>Отримати доступ як гість</2>",
|
||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже маєте обліковий запис?</0><1><0>Увійти</0> Or <2>Отримати доступ як гість</2></1>",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Виклик-рація",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{name}} is talking…": "{{name}} балакає…",
|
||||
"{{name}} is presenting": "{{name}} показує",
|
||||
"{{displayName}}, your call is now ended": "{{displayName}}, ваш виклик завершено",
|
||||
"{{count}} people connected|other": "{{count}} під'єдналися",
|
||||
"{{count}} people connected|one": "{{count}} під'єднується",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
|
||||
"{{name}} (Connecting...)": "{{name}} (З'єднання...)"
|
||||
}
|
||||
6
src/@types/global.d.ts
vendored
@@ -21,4 +21,10 @@ declare global {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
63
src/App.jsx
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
import { HomePage } from "./home/HomePage";
|
||||
import { LoginPage } from "./auth/LoginPage";
|
||||
import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { RoomRedirect } from "./room/RoomRedirect";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
export default function App({ history }) {
|
||||
usePageFocusStyle();
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<ClientProvider>
|
||||
<OverlayProvider>
|
||||
<Switch>
|
||||
<SentryRoute exact path="/">
|
||||
<HomePage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
</OverlayProvider>
|
||||
</ClientProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
78
src/App.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from "react";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
|
||||
import { HomePage } from "./home/HomePage";
|
||||
import { LoginPage } from "./auth/LoginPage";
|
||||
import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { RoomRedirect } from "./room/RoomRedirect";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||
import { CrashView } from "./FullScreenView";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
interface AppProps {
|
||||
history: History;
|
||||
}
|
||||
|
||||
export default function App({ history }: AppProps) {
|
||||
usePageFocusStyle();
|
||||
|
||||
const errorPage = <CrashView />;
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Suspense fallback={null}>
|
||||
<ClientProvider>
|
||||
<InspectorContextProvider>
|
||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||
<OverlayProvider>
|
||||
<Switch>
|
||||
<SentryRoute exact path="/">
|
||||
<HomePage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
</OverlayProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
</InspectorContextProvider>
|
||||
</ClientProvider>
|
||||
</Suspense>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
@@ -48,11 +48,11 @@ const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
bgKey?: string;
|
||||
src: string;
|
||||
fallback: string;
|
||||
src?: string;
|
||||
size?: Size | number;
|
||||
className: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
export const Avatar: React.FC<Props> = ({
|
||||
|
||||
@@ -22,13 +22,23 @@ import React, {
|
||||
createContext,
|
||||
useMemo,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import { initClient, defaultHomeserver } from "./matrix-utils";
|
||||
import {
|
||||
initClient,
|
||||
defaultHomeserver,
|
||||
CryptoStoreIntegrityError,
|
||||
fallbackICEServerAllowed,
|
||||
} from "./matrix-utils";
|
||||
import { widget } from "./widget";
|
||||
import { translatedError } from "./TranslatedError";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -62,6 +72,7 @@ interface ClientState {
|
||||
changePassword: (password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
setClient: (client: MatrixClient, session: Session) => void;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
const ClientContext = createContext<ClientState>(null);
|
||||
@@ -71,8 +82,13 @@ type ClientProviderState = Omit<
|
||||
"changePassword" | "logout" | "setClient"
|
||||
> & { error?: Error };
|
||||
|
||||
export const ClientProvider: FC = ({ children }) => {
|
||||
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,
|
||||
@@ -86,37 +102,82 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const restore = async (): Promise<
|
||||
// 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">
|
||||
> => {
|
||||
try {
|
||||
const session = loadSession();
|
||||
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");
|
||||
|
||||
if (session) {
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } =
|
||||
session;
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
try {
|
||||
return {
|
||||
client: await initClient(
|
||||
{
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
},
|
||||
true
|
||||
),
|
||||
isPasswordlessUser: passwordlessUser,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoStoreIntegrityError) {
|
||||
// We can't use this session anymore, so let's log it out
|
||||
try {
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
},
|
||||
false // Don't need the crypto store just to log out
|
||||
);
|
||||
await client.logout(true);
|
||||
} catch (err_) {
|
||||
logger.warn(
|
||||
"The previous session was lost, and we couldn't log it out, " +
|
||||
"either"
|
||||
);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
return { client, isPasswordlessUser: passwordlessUser };
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { client: undefined, isPasswordlessUser: false };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
restore()
|
||||
init()
|
||||
.then(({ client, isPasswordlessUser }) => {
|
||||
setState({
|
||||
client,
|
||||
@@ -124,17 +185,21 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
isAuthenticated: Boolean(client),
|
||||
isPasswordlessUser,
|
||||
userName: client?.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.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(
|
||||
@@ -162,6 +227,7 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
},
|
||||
[client]
|
||||
@@ -182,6 +248,7 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: session.passwordlessUser,
|
||||
userName: newClient.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
} else {
|
||||
clearSession();
|
||||
@@ -192,19 +259,36 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
const logout = useCallback(async () => {
|
||||
await client.logout(true);
|
||||
await client.clearStores();
|
||||
clearSession();
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: true,
|
||||
userName: "",
|
||||
error: undefined,
|
||||
});
|
||||
history.push("/");
|
||||
}, [history]);
|
||||
}, [history, client]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
// To protect against multiple sessions writing to the same storage
|
||||
// simultaneously, we send a to-device message that shuts down all other
|
||||
// running instances of the app. This isn't necessary if the app is running
|
||||
// in a widget though, since then it'll be mostly stateless.
|
||||
if (!widget && client) {
|
||||
const loadTime = Date.now();
|
||||
|
||||
const onToDeviceEvent = (event: MatrixEvent) => {
|
||||
@@ -219,8 +303,9 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: new Error(
|
||||
"This application has been opened in another tab."
|
||||
error: translatedError(
|
||||
"This application has been opened in another tab.",
|
||||
t
|
||||
),
|
||||
}));
|
||||
}
|
||||
@@ -238,7 +323,7 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
||||
};
|
||||
}
|
||||
}, [client]);
|
||||
}, [client, t]);
|
||||
|
||||
const context = useMemo<ClientState>(
|
||||
() => ({
|
||||
@@ -250,6 +335,7 @@ export const ClientProvider: FC = ({ children }) => {
|
||||
logout,
|
||||
userName,
|
||||
setClient,
|
||||
error: undefined,
|
||||
}),
|
||||
[
|
||||
loading,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import React from "react";
|
||||
import styles from "./Facepile.module.css";
|
||||
import classNames from "classnames";
|
||||
import { Avatar, sizes } from "./Avatar";
|
||||
|
||||
const overlapMap = {
|
||||
xs: 2,
|
||||
sm: 4,
|
||||
md: 8,
|
||||
};
|
||||
|
||||
export function Facepile({
|
||||
className,
|
||||
client,
|
||||
participants,
|
||||
max,
|
||||
size,
|
||||
...rest
|
||||
}) {
|
||||
const _size = sizes.get(size);
|
||||
const _overlap = overlapMap[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.facepile, styles[size], className)}
|
||||
title={participants.map((member) => member.name).join(", ")}
|
||||
style={{ width: participants.length * (_size - _overlap) + _overlap }}
|
||||
{...rest}
|
||||
>
|
||||
{participants.slice(0, max).map((member, i) => {
|
||||
const avatarUrl = member.user?.avatarUrl;
|
||||
return (
|
||||
<Avatar
|
||||
key={member.userId}
|
||||
size={size}
|
||||
src={avatarUrl}
|
||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
style={{ left: i * (_size - _overlap) }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{participants.length > max && (
|
||||
<Avatar
|
||||
key="additional"
|
||||
size={size}
|
||||
fallback={`+${participants.length - max}`}
|
||||
className={styles.avatar}
|
||||
style={{ left: max * (_size - _overlap) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Facepile.defaultProps = {
|
||||
max: 3,
|
||||
size: "xs",
|
||||
};
|
||||
98
src/Facepile.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes, useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./Facepile.module.css";
|
||||
import { Avatar, Size, sizes } from "./Avatar";
|
||||
|
||||
const overlapMap: Partial<Record<Size, number>> = {
|
||||
[Size.XS]: 2,
|
||||
[Size.SM]: 4,
|
||||
[Size.MD]: 8,
|
||||
};
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
className: string;
|
||||
client: MatrixClient;
|
||||
participants: RoomMember[];
|
||||
max?: number;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export function Facepile({
|
||||
className,
|
||||
client,
|
||||
participants,
|
||||
max = 3,
|
||||
size = Size.XS,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _size = sizes.get(size);
|
||||
const _overlap = overlapMap[size];
|
||||
|
||||
const title = useMemo(() => {
|
||||
return participants.reduce<string | null>(
|
||||
(prev, curr) =>
|
||||
prev === null
|
||||
? curr.name
|
||||
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
||||
null
|
||||
) as string;
|
||||
}, [participants, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.facepile, styles[size], className)}
|
||||
title={title}
|
||||
style={{
|
||||
width:
|
||||
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
||||
_overlap,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{participants.slice(0, max).map((member, i) => {
|
||||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
return (
|
||||
<Avatar
|
||||
key={member.userId}
|
||||
size={size}
|
||||
src={avatarUrl ?? undefined}
|
||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
style={{ left: i * (_size - _overlap) }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{participants.length > max && (
|
||||
<Avatar
|
||||
key="additional"
|
||||
size={size}
|
||||
fallback={`+${participants.length - max}`}
|
||||
className={styles.avatar}
|
||||
style={{ left: max * (_size - _overlap) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styles from "./FullScreenView.module.css";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import classNames from "classnames";
|
||||
import { LinkButton, Button } from "./button";
|
||||
|
||||
export function FullScreenView({ className, children }) {
|
||||
return (
|
||||
<div className={classNames(styles.page, className)}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav />
|
||||
</Header>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorView({ error }) {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location = "/";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Error</h1>
|
||||
<p>{error.message}</p>
|
||||
{location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
Return to home screen
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
Return to home screen
|
||||
</LinkButton>
|
||||
)}
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingView() {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,12 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.homeLink {
|
||||
/* Make the buttons the same width */
|
||||
.wideButton {
|
||||
width: 291px;
|
||||
}
|
||||
|
||||
/* Fixed height to avoid content jumping around*/
|
||||
.sendLogsSection {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
144
src/FullScreenView.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { ReactNode, useCallback, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import { LinkButton, Button } from "./button";
|
||||
import { useSubmitRageshake } from "./settings/submit-rageshake";
|
||||
import { ErrorMessage } from "./input/Input";
|
||||
import styles from "./FullScreenView.module.css";
|
||||
import { translatedError, TranslatedError } from "./TranslatedError";
|
||||
|
||||
interface FullScreenViewProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function FullScreenView({ className, children }: FullScreenViewProps) {
|
||||
return (
|
||||
<div className={classNames(styles.page, className)}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav />
|
||||
</Header>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ErrorViewProps {
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export function ErrorView({ error }: ErrorViewProps) {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location.href = "/";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Error</h1>
|
||||
<p>
|
||||
{error instanceof TranslatedError
|
||||
? error.translatedMessage
|
||||
: error.message}
|
||||
</p>
|
||||
{location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("Return to home screen")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
{t("Return to home screen")}
|
||||
</LinkButton>
|
||||
)}
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function CrashView() {
|
||||
const { t } = useTranslation();
|
||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||
|
||||
const sendDebugLogs = useCallback(() => {
|
||||
submitRageshake({
|
||||
description: "**Soft Crash**",
|
||||
sendLogs: true,
|
||||
});
|
||||
}, [submitRageshake]);
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
window.location.href = "/";
|
||||
}, []);
|
||||
|
||||
let logsComponent: JSX.Element | null = null;
|
||||
if (sent) {
|
||||
logsComponent = <div>{t("Thanks! We'll get right on it.")}</div>;
|
||||
} else if (sending) {
|
||||
logsComponent = <div>{t("Sending…")}</div>;
|
||||
} else {
|
||||
logsComponent = (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
onPress={sendDebugLogs}
|
||||
className={styles.wideButton}
|
||||
>
|
||||
{t("Send debug logs")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Trans>
|
||||
<h1>Oops, something's gone wrong.</h1>
|
||||
<p>Submitting debug logs will help us track down the problem.</p>
|
||||
</Trans>
|
||||
<div className={styles.sendLogsSection}>{logsComponent}</div>
|
||||
{error && (
|
||||
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
|
||||
)}
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.wideButton}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("Return to home screen")}
|
||||
</Button>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingView() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Loading…")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +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, avatarUrl }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.roomAvatar}>
|
||||
<Avatar
|
||||
size="md"
|
||||
src={avatarUrl}
|
||||
bgKey={roomName}
|
||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<VideoIcon width={16} height={16} />
|
||||
</div>
|
||||
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomSetupHeaderInfo({ roomName, avatarUrl, ...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} avatarUrl={avatarUrl} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -104,6 +104,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,
|
||||
|
||||
180
src/Header.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import classNames from "classnames";
|
||||
import React, { HTMLAttributes, ReactNode, useCallback, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { AriaButtonProps } from "@react-types/button";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./Header.module.css";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { Button } from "./button";
|
||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { Subtitle } from "./typography/Typography";
|
||||
import { Avatar, Size } from "./Avatar";
|
||||
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
|
||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||
|
||||
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Header({ children, className, ...rest }: HeaderProps) {
|
||||
return (
|
||||
<header className={classNames(styles.header, className)} {...rest}>
|
||||
{children}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
interface LeftNavProps extends HTMLAttributes<HTMLElement> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
hideMobile?: boolean;
|
||||
}
|
||||
|
||||
export function LeftNav({
|
||||
children,
|
||||
className,
|
||||
hideMobile,
|
||||
...rest
|
||||
}: LeftNavProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.nav,
|
||||
styles.leftNav,
|
||||
{ [styles.hideMobile]: hideMobile },
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RightNavProps extends HTMLAttributes<HTMLElement> {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
hideMobile?: boolean;
|
||||
}
|
||||
|
||||
export function RightNav({
|
||||
children,
|
||||
className,
|
||||
hideMobile,
|
||||
...rest
|
||||
}: RightNavProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.nav,
|
||||
styles.rightNav,
|
||||
{ [styles.hideMobile]: hideMobile },
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface HeaderLogoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HeaderLogo({ className }: HeaderLogoProps) {
|
||||
return (
|
||||
<Link className={classNames(styles.headerLogo, className)} to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
interface RoomHeaderInfo {
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export function RoomHeaderInfo({ roomName, avatarUrl }: RoomHeaderInfo) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.roomAvatar}>
|
||||
<Avatar
|
||||
size={Size.MD}
|
||||
src={avatarUrl}
|
||||
bgKey={roomName}
|
||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<VideoIcon width={16} height={16} />
|
||||
</div>
|
||||
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface RoomSetupHeaderInfoProps extends AriaButtonProps<"button"> {
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
isEmbedded: boolean;
|
||||
}
|
||||
|
||||
export function RoomSetupHeaderInfo({
|
||||
roomName,
|
||||
avatarUrl,
|
||||
isEmbedded,
|
||||
...rest
|
||||
}: RoomSetupHeaderInfoProps) {
|
||||
const ref = useRef();
|
||||
const { buttonProps } = useButton(rest, ref);
|
||||
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
||||
<ArrowLeftIcon width={16} height={16} />
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface VersionMismatchWarningProps {
|
||||
users: Set<string>;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export function VersionMismatchWarning({
|
||||
users,
|
||||
room,
|
||||
}: VersionMismatchWarningProps) {
|
||||
const { t } = useTranslation();
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onDetailsClick = useCallback(() => {
|
||||
modalState.open();
|
||||
}, [modalState]);
|
||||
|
||||
if (users.size === 0) return null;
|
||||
|
||||
return (
|
||||
<span className={styles.versionMismatchWarning}>
|
||||
{t("Incompatible versions!")}
|
||||
<Button variant="link" onClick={onDetailsClick}>
|
||||
{t("Details")}
|
||||
</Button>
|
||||
{modalState.isOpen && (
|
||||
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
53
src/IncompatibleVersionModal.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import React, { useMemo } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent } from "./Modal";
|
||||
import { Body } from "./typography/Typography";
|
||||
|
||||
interface Props {
|
||||
userIds: Set<string>;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export const IncompatibleVersionModal: React.FC<Props> = ({
|
||||
userIds,
|
||||
room,
|
||||
...rest
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const userLis = useMemo(
|
||||
() => [...userIds].map((u) => <li>{room.getMember(u)?.name ?? u}</li>),
|
||||
[userIds, room]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title={t("Incompatible versions")} isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<Body>
|
||||
<Trans>
|
||||
Other users are trying to join this call from incompatible versions.
|
||||
These users should ensure that they have refreshed their browsers:
|
||||
<ul>{userLis}</ul>
|
||||
</Trans>
|
||||
</Body>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
||||
|
||||
const remoteWorker = new IndexedDBStoreWorker(self.postMessage);
|
||||
|
||||
self.onmessage = remoteWorker.onMessage;
|
||||
6
src/IndexedDBWorker.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const remoteWorker = new IndexedDBStoreWorker((self as any).postMessage);
|
||||
|
||||
self.onmessage = remoteWorker.onMessage;
|
||||
90
src/LazyEventEmitter.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
/**
|
||||
* An event emitter that lets events pile up in a backlog until a listener is
|
||||
* present, at which point any events that were missed are re-emitted.
|
||||
*/
|
||||
export class LazyEventEmitter extends EventEmitter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private eventBacklogs = new Map<string | symbol, NonEmptyArray<any[]>>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public emit(type: string | symbol, ...args: any[]): boolean {
|
||||
const hasListeners = super.emit(type, ...args);
|
||||
|
||||
if (!hasListeners) {
|
||||
// The event was missed, so add it to the backlog
|
||||
const backlog = this.eventBacklogs.get(type);
|
||||
if (backlog) {
|
||||
backlog.push(args);
|
||||
} else {
|
||||
// Start a new backlog
|
||||
this.eventBacklogs.set(type, [args]);
|
||||
}
|
||||
}
|
||||
|
||||
return hasListeners;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public on(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||
super.on(type, listener);
|
||||
|
||||
const backlog = this.eventBacklogs.get(type);
|
||||
if (backlog) {
|
||||
// That was the first listener for this type, so let's send it all the
|
||||
// events that have piled up
|
||||
for (const args of backlog) super.emit(type, ...args);
|
||||
// Backlog is now clear
|
||||
this.eventBacklogs.delete(type);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public addListener(
|
||||
type: string | symbol,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
return this.on(type, listener);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public once(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||
super.once(type, listener);
|
||||
|
||||
const backlog = this.eventBacklogs.get(type);
|
||||
if (backlog) {
|
||||
// That was the first listener for this type, so let's send it the first
|
||||
// of the events that have piled up
|
||||
super.emit(type, ...backlog[0]);
|
||||
// Clear the event from the backlog
|
||||
if (backlog.length === 1) {
|
||||
this.eventBacklogs.delete(type);
|
||||
} else {
|
||||
backlog.shift();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useListBox, useOption } from "@react-aria/listbox";
|
||||
import styles from "./ListBox.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function ListBox(props) {
|
||||
const ref = useRef();
|
||||
let { listBoxRef = ref, state } = props;
|
||||
const { listBoxProps } = useListBox(props, state, listBoxRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...listBoxProps}
|
||||
ref={listBoxRef}
|
||||
className={classNames(styles.listBox, props.className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<Option
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
className={props.optionClassName}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function Option({ item, state, className }) {
|
||||
const ref = useRef();
|
||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||
{ key: item.key },
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...optionProps}
|
||||
ref={ref}
|
||||
className={classNames(styles.option, className, {
|
||||
[styles.selected]: isSelected,
|
||||
[styles.focused]: isFocused,
|
||||
[styles.disables]: isDisabled,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
89
src/ListBox.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
|
||||
import { ListState } from "@react-stately/list";
|
||||
import { Node } from "@react-types/shared";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./ListBox.module.css";
|
||||
|
||||
interface ListBoxProps<T> extends AriaListBoxOptions<T> {
|
||||
optionClassName: string;
|
||||
state: ListState<T>;
|
||||
className?: string;
|
||||
listBoxRef?: React.MutableRefObject<HTMLUListElement>;
|
||||
}
|
||||
|
||||
export function ListBox<T>({
|
||||
state,
|
||||
optionClassName,
|
||||
className,
|
||||
listBoxRef,
|
||||
...rest
|
||||
}: ListBoxProps<T>) {
|
||||
const ref = useRef<HTMLUListElement>();
|
||||
if (!listBoxRef) listBoxRef = ref;
|
||||
|
||||
const { listBoxProps } = useListBox(rest, state, listBoxRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...listBoxProps}
|
||||
ref={listBoxRef}
|
||||
className={classNames(styles.listBox, className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<Option
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
className={optionClassName}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
interface OptionProps<T> {
|
||||
className: string;
|
||||
state: ListState<T>;
|
||||
item: Node<T>;
|
||||
}
|
||||
|
||||
function Option<T>({ item, state, className }: OptionProps<T>) {
|
||||
const ref = useRef();
|
||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||
{ key: item.key },
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...optionProps}
|
||||
ref={ref}
|
||||
className={classNames(styles.option, className, {
|
||||
[styles.selected]: isSelected,
|
||||
[styles.focused]: isFocused,
|
||||
[styles.disables]: isDisabled,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
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 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";
|
||||
|
||||
export function Menu({ className, onAction, ...rest }) {
|
||||
const state = useTreeState({ ...rest, selectionMode: "none" });
|
||||
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(rest, state, menuRef);
|
||||
const { menuProps } = useMenu<T>(rest, state, menuRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
@@ -23,19 +38,25 @@ export function Menu({ className, onAction, ...rest }) {
|
||||
item={item}
|
||||
state={state}
|
||||
onAction={onAction}
|
||||
onClose={rest.onClose}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ item, state, onAction, onClose }) {
|
||||
interface MenuItemProps<T> {
|
||||
item: Node<T>;
|
||||
state: TreeState<T>;
|
||||
onAction: (value: Key) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
|
||||
const ref = useRef();
|
||||
const { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
isDisabled: item.isDisabled,
|
||||
onAction,
|
||||
onClose,
|
||||
},
|
||||
132
src/Modal.jsx
@@ -1,132 +0,0 @@
|
||||
import React, { useRef, useMemo } from "react";
|
||||
import {
|
||||
useOverlay,
|
||||
usePreventScroll,
|
||||
useModal,
|
||||
OverlayContainer,
|
||||
} from "@react-aria/overlays";
|
||||
import { useOverlayTriggerState } from "@react-stately/overlays";
|
||||
import { useDialog } from "@react-aria/dialog";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||
import styles from "./Modal.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Modal(props) {
|
||||
const { title, children, className, mobileFullScreen } = props;
|
||||
const modalRef = useRef();
|
||||
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(props, modalRef);
|
||||
const closeButtonRef = useRef();
|
||||
const { buttonProps: closeButtonProps } = useButton({
|
||||
onPress: () => props.onClose(),
|
||||
});
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<div className={styles.modalOverlay} {...underlayProps}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={classNames(
|
||||
styles.modal,
|
||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
<button
|
||||
{...closeButtonProps}
|
||||
ref={closeButtonRef}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalContent({ children, className, ...rest }) {
|
||||
return (
|
||||
<div className={classNames(styles.content, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState() {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
}
|
||||
|
||||
export function useToggleModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.toggle(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.open(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useCloseModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.close(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalTrigger({ children }) {
|
||||
const { modalState, modalProps } = useModalState();
|
||||
const buttonRef = useRef();
|
||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [modalTrigger, modal] = children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<modalTrigger.type
|
||||
{...modalTrigger.props}
|
||||
{...buttonProps}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{modalState.isOpen && modal(modalProps)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
}
|
||||
|
||||
.modalHeader h3 {
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
204
src/Modal.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
|
||||
import React, { useRef, useMemo, ReactNode } from "react";
|
||||
import {
|
||||
useOverlay,
|
||||
usePreventScroll,
|
||||
useModal,
|
||||
OverlayContainer,
|
||||
OverlayProps,
|
||||
} from "@react-aria/overlays";
|
||||
import {
|
||||
OverlayTriggerState,
|
||||
useOverlayTriggerState,
|
||||
} from "@react-stately/overlays";
|
||||
import { useDialog } from "@react-aria/dialog";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { ButtonAria, useButton } from "@react-aria/button";
|
||||
import classNames from "classnames";
|
||||
import { AriaDialogProps } from "@react-types/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||
import styles from "./Modal.module.css";
|
||||
|
||||
export interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
mobileFullScreen?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
mobileFullScreen,
|
||||
onClose,
|
||||
...rest
|
||||
}: ModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const modalRef = useRef();
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ ...rest, onClose },
|
||||
modalRef
|
||||
);
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||
const closeButtonRef = useRef();
|
||||
const { buttonProps: closeButtonProps } = useButton(
|
||||
{
|
||||
onPress: () => onClose(),
|
||||
},
|
||||
closeButtonRef
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<div className={styles.modalOverlay} {...underlayProps}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={classNames(
|
||||
styles.modal,
|
||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
<button
|
||||
{...closeButtonProps}
|
||||
ref={closeButtonRef}
|
||||
className={styles.closeButton}
|
||||
title={t("Close")}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalContentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ModalContent({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: ModalContentProps) {
|
||||
return (
|
||||
<div className={classNames(styles.content, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState(): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: { isOpen: boolean; onClose: () => void };
|
||||
} {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
}
|
||||
|
||||
export function useToggleModalButton(
|
||||
modalState: OverlayTriggerState,
|
||||
ref: React.RefObject<HTMLButtonElement>
|
||||
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.toggle(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenModalButton(
|
||||
modalState: OverlayTriggerState,
|
||||
ref: React.RefObject<HTMLButtonElement>
|
||||
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.open(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useCloseModalButton(
|
||||
modalState: OverlayTriggerState,
|
||||
ref: React.RefObject<HTMLButtonElement>
|
||||
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.close(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalTriggerProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ModalTrigger({ children }: ModalTriggerProps) {
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const buttonRef = useRef();
|
||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [modalTrigger, modal] = children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<modalTrigger.type
|
||||
{...modalTrigger.props}
|
||||
{...buttonProps}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{modalState.isOpen && modal(modalProps)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
||||
import { FieldRow, InputField } from "./input/Input";
|
||||
import { usePageTitle } from "./usePageTitle";
|
||||
|
||||
export function SequenceDiagramViewerPage() {
|
||||
usePageTitle("Inspector");
|
||||
|
||||
const [debugLog, setDebugLog] = useState();
|
||||
const [selectedUserId, setSelectedUserId] = useState();
|
||||
const onChangeDebugLog = useCallback((e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
e.target.files[0].text().then((text) => {
|
||||
setDebugLog(JSON.parse(text));
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
id="debugLog"
|
||||
name="debugLog"
|
||||
label="Debug Log"
|
||||
onChange={onChangeDebugLog}
|
||||
/>
|
||||
</FieldRow>
|
||||
{debugLog && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={debugLog.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={debugLog.remoteUserIds}
|
||||
events={debugLog.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
src/SequenceDiagramViewerPage.tsx
Normal file
@@ -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,
|
||||
};
|
||||
@@ -3,10 +3,12 @@
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
padding: 10px;
|
||||
color: var(--primary-content);
|
||||
border-radius: 8px;
|
||||
max-width: 135px;
|
||||
width: max-content;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
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
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import i18n from "i18next";
|
||||
|
||||
/**
|
||||
* An error with messages in both English and the user's preferred language.
|
||||
*/
|
||||
// Abstract to force consumers to use the function below rather than calling the
|
||||
// constructor directly
|
||||
export abstract class TranslatedError extends Error {
|
||||
/**
|
||||
* The error message in the user's preferred language.
|
||||
*/
|
||||
public readonly translatedMessage: string;
|
||||
|
||||
public constructor(messageKey: string, translationFn: typeof i18n.t) {
|
||||
super(translationFn(messageKey, { lng: "en-GB" }));
|
||||
this.translatedMessage = translationFn(messageKey);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslatedErrorImpl extends TranslatedError {}
|
||||
|
||||
// i18next-parser can't detect calls to a constructor, so we expose a bare
|
||||
// function instead
|
||||
export const translatedError = (messageKey: string, t: typeof i18n.t) =>
|
||||
new TranslatedErrorImpl(messageKey, t);
|
||||
109
src/UrlParams.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export interface UrlParams {
|
||||
roomAlias: string | null;
|
||||
roomId: string | null;
|
||||
viaServers: string[];
|
||||
// Whether the app is running in embedded mode, and should keep the user
|
||||
// confined to the current room
|
||||
isEmbedded: boolean;
|
||||
// Whether the app should pause before joining the call until it sees an
|
||||
// io.element.join widget action, allowing it to be preloaded
|
||||
preload: boolean;
|
||||
// Whether to hide the room header when in a call
|
||||
hideHeader: boolean;
|
||||
// Whether to hide the screen-sharing button
|
||||
hideScreensharing: boolean;
|
||||
// Whether to start a walkie-talkie call instead of a video call
|
||||
isPtt: boolean;
|
||||
// Whether to use end-to-end encryption
|
||||
e2eEnabled: boolean;
|
||||
// The user's ID (only used in matryoshka mode)
|
||||
userId: string | null;
|
||||
// The display name to use for auto-registration
|
||||
displayName: string | null;
|
||||
// The device's ID (only used in matryoshka mode)
|
||||
deviceId: string | null;
|
||||
// The base URL of the homeserver to use for media lookups in matryoshka mode
|
||||
baseUrl: string | null;
|
||||
// The BCP 47 code of the language the app should use
|
||||
lang: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the app parameters for the current URL.
|
||||
* @param query The URL query string
|
||||
* @param fragment The URL fragment string
|
||||
* @returns The app parameters encoded in the URL
|
||||
*/
|
||||
export const getUrlParams = (
|
||||
query: string = window.location.search,
|
||||
fragment: string = window.location.hash
|
||||
): UrlParams => {
|
||||
const fragmentQueryStart = fragment.indexOf("?");
|
||||
const fragmentParams = new URLSearchParams(
|
||||
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
|
||||
);
|
||||
const queryParams = new URLSearchParams(query);
|
||||
|
||||
// Normally, URL params should be encoded in the fragment so as to avoid
|
||||
// leaking them to the server. However, we also check the normal query
|
||||
// string for backwards compatibility with versions that only used that.
|
||||
const hasParam = (name: string): boolean =>
|
||||
fragmentParams.has(name) || queryParams.has(name);
|
||||
const getParam = (name: string): string | null =>
|
||||
fragmentParams.get(name) ?? queryParams.get(name);
|
||||
const getAllParams = (name: string): string[] => [
|
||||
...fragmentParams.getAll(name),
|
||||
...queryParams.getAll(name),
|
||||
];
|
||||
|
||||
// The part of the fragment before the ?
|
||||
const fragmentRoute =
|
||||
fragmentQueryStart === -1
|
||||
? fragment
|
||||
: fragment.substring(0, fragmentQueryStart);
|
||||
|
||||
return {
|
||||
roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null,
|
||||
roomId: getParam("roomId"),
|
||||
viaServers: getAllParams("via"),
|
||||
isEmbedded: hasParam("embed"),
|
||||
preload: hasParam("preload"),
|
||||
hideHeader: hasParam("hideHeader"),
|
||||
hideScreensharing: hasParam("hideScreensharing"),
|
||||
isPtt: hasParam("ptt"),
|
||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||
userId: getParam("userId"),
|
||||
displayName: getParam("displayName"),
|
||||
deviceId: getParam("deviceId"),
|
||||
baseUrl: getParam("baseUrl"),
|
||||
lang: getParam("lang"),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to simplify use of getUrlParams.
|
||||
* @returns The app parameters for the current URL
|
||||
*/
|
||||
export const useUrlParams = (): UrlParams => {
|
||||
const { hash, search } = useLocation();
|
||||
return useMemo(() => getUrlParams(search, hash), [search, hash]);
|
||||
};
|
||||
@@ -1,16 +1,27 @@
|
||||
import React, { useMemo } from "react";
|
||||
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 +30,8 @@ export function UserMenu({
|
||||
displayName,
|
||||
avatarUrl,
|
||||
onAction,
|
||||
}) {
|
||||
}: UserMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const items = useMemo(() => {
|
||||
@@ -35,7 +47,7 @@ export function UserMenu({
|
||||
if (isPasswordlessUser && !preventNavigation) {
|
||||
arr.push({
|
||||
key: "login",
|
||||
label: "Sign In",
|
||||
label: t("Sign in"),
|
||||
icon: LoginIcon,
|
||||
});
|
||||
}
|
||||
@@ -43,14 +55,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 +76,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 +89,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,17 @@
|
||||
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 +20,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onAction = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
modalState.open();
|
||||
@@ -23,6 +23,7 @@ import React, {
|
||||
useMemo,
|
||||
} 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";
|
||||
@@ -34,7 +35,8 @@ import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export const LoginPage: FC = () => {
|
||||
usePageTitle("Login");
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Login"));
|
||||
|
||||
const { setClient } = useClient();
|
||||
const login = useInteractiveLogin();
|
||||
@@ -93,8 +95,8 @@ export const LoginPage: FC = () => {
|
||||
<InputField
|
||||
type="text"
|
||||
ref={usernameRef}
|
||||
placeholder="Username"
|
||||
label="Username"
|
||||
placeholder={t("Username")}
|
||||
label={t("Username")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
prefix="@"
|
||||
@@ -105,18 +107,18 @@ export const LoginPage: FC = () => {
|
||||
<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>
|
||||
@@ -124,9 +126,11 @@ export const LoginPage: FC = () => {
|
||||
<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>
|
||||
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
FormEvent,
|
||||
useCallback,
|
||||
@@ -25,6 +26,7 @@ import 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";
|
||||
@@ -39,7 +41,8 @@ import { Caption, Link } from "../typography/Typography";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export const RegisterPage: FC = () => {
|
||||
usePageTitle("Register");
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Register"));
|
||||
|
||||
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
||||
useClient();
|
||||
@@ -100,7 +103,7 @@ export const RegisterPage: FC = () => {
|
||||
submit()
|
||||
.then(() => {
|
||||
if (location.state?.from) {
|
||||
history.push(location.state.from);
|
||||
history.push(location.state?.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
@@ -125,11 +128,11 @@ export const RegisterPage: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
||||
confirmPasswordRef.current?.setCustomValidity("Passwords must match");
|
||||
confirmPasswordRef.current?.setCustomValidity(t("Passwords must match"));
|
||||
} else {
|
||||
confirmPasswordRef.current?.setCustomValidity("");
|
||||
}
|
||||
}, [password, passwordConfirmation]);
|
||||
}, [password, passwordConfirmation, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||
@@ -153,8 +156,8 @@ export const RegisterPage: FC = () => {
|
||||
<InputField
|
||||
type="text"
|
||||
name="userName"
|
||||
placeholder="Username"
|
||||
label="Username"
|
||||
placeholder={t("Username")}
|
||||
label={t("Username")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
prefix="@"
|
||||
@@ -166,10 +169,12 @@ export const RegisterPage: FC = () => {
|
||||
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>
|
||||
@@ -177,47 +182,53 @@ export const RegisterPage: FC = () => {
|
||||
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>
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
adjectives,
|
||||
colors,
|
||||
animals,
|
||||
Config,
|
||||
} from "unique-names-generator";
|
||||
|
||||
const elements = [
|
||||
@@ -143,12 +142,11 @@ const elements = [
|
||||
"oganesson",
|
||||
];
|
||||
|
||||
export function generateRandomName(config: Config): string {
|
||||
export function generateRandomName(): string {
|
||||
return uniqueNamesGenerator({
|
||||
dictionaries: [colors, adjectives, animals, elements],
|
||||
style: "lowerCase",
|
||||
length: 3,
|
||||
separator: "-",
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const useInteractiveLogin = () =>
|
||||
password: string
|
||||
) => Promise<[MatrixClient, Session]>
|
||||
>(async (homeserver: string, username: string, password: string) => {
|
||||
const authClient = createClient(homeserver);
|
||||
const authClient = createClient({ baseUrl: homeserver });
|
||||
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient,
|
||||
@@ -57,12 +57,15 @@ export const useInteractiveLogin = () =>
|
||||
passwordlessUser: false,
|
||||
};
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
false
|
||||
);
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
return [client, session];
|
||||
|
||||
@@ -37,7 +37,7 @@ export const useInteractiveRegistration = (): [
|
||||
|
||||
const authClient = useRef<MatrixClient>();
|
||||
if (!authClient.current) {
|
||||
authClient.current = createClient(defaultHomeserver);
|
||||
authClient.current = createClient({ baseUrl: defaultHomeserver });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,12 +90,15 @@ export const useInteractiveRegistration = (): [
|
||||
const { user_id, access_token, device_id } =
|
||||
(await interactiveAuth.attemptAuth()) as any;
|
||||
|
||||
const client = await initClient({
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
});
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
await client.setDisplayName(displayName);
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ 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 {
|
||||
@@ -32,6 +35,7 @@ interface RecaptchaPromiseRef {
|
||||
}
|
||||
|
||||
export const useRecaptcha = (sitekey: string) => {
|
||||
const { t } = useTranslation();
|
||||
const [recaptchaId] = useState(() => randomString(16));
|
||||
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||
|
||||
@@ -71,14 +75,14 @@ export const useRecaptcha = (sitekey: string) => {
|
||||
|
||||
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 as HTMLElement)?.style?.visibility !== "visible") {
|
||||
reject(new Error("Recaptcha dismissed"));
|
||||
reject(translatedError("Recaptcha dismissed", t));
|
||||
observer.disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +112,7 @@ export const useRecaptcha = (sitekey: string) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [sitekey]);
|
||||
}, [sitekey, t]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
window.grecaptcha?.reset();
|
||||
|
||||
59
src/auth/useRegisterPasswordlessUser.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||
import { generateRandomName } from "../auth/generateRandomName";
|
||||
import { useRecaptcha } from "../auth/useRecaptcha";
|
||||
|
||||
export interface UseRegisterPasswordlessUserType {
|
||||
privacyPolicyUrl: string;
|
||||
registerPasswordlessUser: (displayName: string) => Promise<void>;
|
||||
recaptchaId: string;
|
||||
}
|
||||
|
||||
export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
||||
const { setClient } = useClient();
|
||||
const [privacyPolicyUrl, recaptchaKey, register] =
|
||||
useInteractiveRegistration();
|
||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||
|
||||
const registerPasswordlessUser = useCallback(
|
||||
async (displayName: string) => {
|
||||
try {
|
||||
const recaptchaResponse = await execute();
|
||||
const userName = generateRandomName();
|
||||
const [client, session] = await register(
|
||||
userName,
|
||||
randomString(16),
|
||||
displayName,
|
||||
recaptchaResponse,
|
||||
true
|
||||
);
|
||||
setClient(client, session);
|
||||
} catch (e) {
|
||||
reset();
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
[execute, reset, register, setClient]
|
||||
);
|
||||
|
||||
return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId };
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Button.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import { TooltipTrigger } from "../Tooltip";
|
||||
|
||||
export const variantToClassName = {
|
||||
default: [styles.button],
|
||||
toolbar: [styles.toolbarButton],
|
||||
toolbarSecondary: [styles.toolbarButtonSecondary],
|
||||
icon: [styles.iconButton],
|
||||
secondary: [styles.secondary],
|
||||
copy: [styles.copyButton],
|
||||
secondaryCopy: [styles.secondaryCopy, styles.copyButton],
|
||||
iconCopy: [styles.iconCopyButton],
|
||||
secondaryHangup: [styles.secondaryHangup],
|
||||
dropdown: [styles.dropdownButton],
|
||||
};
|
||||
|
||||
export const sizeToClassName = {
|
||||
lg: [styles.lg],
|
||||
};
|
||||
|
||||
export const Button = forwardRef(
|
||||
(
|
||||
{
|
||||
variant = "default",
|
||||
size,
|
||||
on,
|
||||
off,
|
||||
iconStyle,
|
||||
className,
|
||||
children,
|
||||
onPress,
|
||||
onPressStart,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const buttonRef = useObjectRef(ref);
|
||||
const { buttonProps } = useButton(
|
||||
{ onPress, onPressStart, ...rest },
|
||||
buttonRef
|
||||
);
|
||||
|
||||
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
||||
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
||||
let filteredButtonProps = buttonProps;
|
||||
|
||||
if (rest.type === "submit" && !rest.onPress) {
|
||||
const { onKeyDown, onKeyUp, ...filtered } = buttonProps;
|
||||
filteredButtonProps = filtered;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
variantToClassName[variant],
|
||||
sizeToClassName[size],
|
||||
styles[iconStyle],
|
||||
className,
|
||||
{
|
||||
[styles.on]: on,
|
||||
[styles.off]: off,
|
||||
}
|
||||
)}
|
||||
{...mergeProps(rest, filteredButtonProps)}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{children}
|
||||
{variant === "dropdown" && <ArrowDownIcon />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export function MicButton({ muted, ...rest }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||
</Button>
|
||||
{() => (muted ? "Unmute microphone" : "Mute microphone")}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoButton({ muted, ...rest }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||
</Button>
|
||||
{() => (muted ? "Turn on camera" : "Turn off camera")}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||
<ScreenshareIcon />
|
||||
</Button>
|
||||
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function HangupButton({ className, ...rest }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="toolbar"
|
||||
className={classNames(styles.hangupButton, className)}
|
||||
{...rest}
|
||||
>
|
||||
<HangupIcon />
|
||||
</Button>
|
||||
{() => "Leave"}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsButton({ className, ...rest }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button variant="toolbar" {...rest}>
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
{() => "Settings"}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function InviteButton({ className, ...rest }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button variant="toolbar" {...rest}>
|
||||
<AddUserIcon />
|
||||
</Button>
|
||||
{() => "Invite"}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
@@ -207,3 +207,10 @@ limitations under the License.
|
||||
.lg {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
307
src/button/Button.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React, { forwardRef, useCallback } from "react";
|
||||
import { PressEvent } from "@react-types/shared";
|
||||
import classNames from "classnames";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./Button.module.css";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
|
||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
||||
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
|
||||
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
|
||||
import { TooltipTrigger } from "../Tooltip";
|
||||
import { VolumeIcon } from "./VolumeIcon";
|
||||
|
||||
export type ButtonVariant =
|
||||
| "default"
|
||||
| "toolbar"
|
||||
| "toolbarSecondary"
|
||||
| "icon"
|
||||
| "secondary"
|
||||
| "copy"
|
||||
| "secondaryCopy"
|
||||
| "iconCopy"
|
||||
| "secondaryHangup"
|
||||
| "dropdown"
|
||||
| "link";
|
||||
|
||||
export const variantToClassName = {
|
||||
default: [styles.button],
|
||||
toolbar: [styles.toolbarButton],
|
||||
toolbarSecondary: [styles.toolbarButtonSecondary],
|
||||
icon: [styles.iconButton],
|
||||
secondary: [styles.secondary],
|
||||
copy: [styles.copyButton],
|
||||
secondaryCopy: [styles.secondaryCopy, styles.copyButton],
|
||||
iconCopy: [styles.iconCopyButton],
|
||||
secondaryHangup: [styles.secondaryHangup],
|
||||
dropdown: [styles.dropdownButton],
|
||||
link: [styles.linkButton],
|
||||
};
|
||||
|
||||
export type ButtonSize = "lg";
|
||||
|
||||
export const sizeToClassName: { lg: string[] } = {
|
||||
lg: [styles.lg],
|
||||
};
|
||||
interface Props {
|
||||
variant: ButtonVariant;
|
||||
size: ButtonSize;
|
||||
on: () => void;
|
||||
off: () => void;
|
||||
iconStyle: string;
|
||||
className: string;
|
||||
children: Element[];
|
||||
onPress: (e: PressEvent) => void;
|
||||
onPressStart: (e: PressEvent) => void;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
(
|
||||
{
|
||||
variant = "default",
|
||||
size,
|
||||
on,
|
||||
off,
|
||||
iconStyle,
|
||||
className,
|
||||
children,
|
||||
onPress,
|
||||
onPressStart,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const buttonRef = useObjectRef<HTMLButtonElement>(ref);
|
||||
const { buttonProps } = useButton(
|
||||
{ onPress, onPressStart, ...rest },
|
||||
buttonRef
|
||||
);
|
||||
|
||||
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
||||
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
||||
let filteredButtonProps = buttonProps;
|
||||
|
||||
if (rest.type === "submit" && !rest.onPress) {
|
||||
const { ...filtered } = buttonProps;
|
||||
filteredButtonProps = filtered;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
variantToClassName[variant],
|
||||
sizeToClassName[size],
|
||||
styles[iconStyle],
|
||||
className,
|
||||
{
|
||||
[styles.on]: on,
|
||||
[styles.off]: off,
|
||||
}
|
||||
)}
|
||||
{...mergeProps(rest, filteredButtonProps)}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<>
|
||||
{children}
|
||||
{variant === "dropdown" && <ArrowDownIcon />}
|
||||
</>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export function MicButton({
|
||||
muted,
|
||||
...rest
|
||||
}: {
|
||||
muted: boolean;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
tooltip={() => (muted ? t("Unmute microphone") : t("Mute microphone"))}
|
||||
>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoButton({
|
||||
muted,
|
||||
...rest
|
||||
}: {
|
||||
muted: boolean;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
tooltip={() => (muted ? t("Turn on camera") : t("Turn off camera"))}
|
||||
>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScreenshareButton({
|
||||
enabled,
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
enabled: boolean;
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
tooltip={() => (enabled ? t("Stop sharing screen") : t("Share screen"))}
|
||||
>
|
||||
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||
<ScreenshareIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function HangupButton({
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Leave"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Button
|
||||
variant="toolbar"
|
||||
className={classNames(styles.hangupButton, className)}
|
||||
{...rest}
|
||||
>
|
||||
<HangupIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsButton({
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Settings"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Button variant="toolbar" {...rest}>
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function InviteButton({
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Invite"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Button variant="toolbar" {...rest}>
|
||||
<AddUserIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface AudioButtonProps extends Omit<Props, "variant"> {
|
||||
/**
|
||||
* A number between 0 and 1
|
||||
*/
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Local volume"), [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Button variant="icon" {...rest}>
|
||||
<VolumeIcon volume={volume} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface FullscreenButtonProps extends Omit<Props, "variant"> {
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
export function FullscreenButton({
|
||||
fullscreen,
|
||||
...rest
|
||||
}: FullscreenButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => {
|
||||
return fullscreen ? t("Exit full screen") : t("Full screen");
|
||||
}, [fullscreen, t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Button variant="icon" {...rest}>
|
||||
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
@@ -15,11 +15,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useClipboard from "react-use-clipboard";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
|
||||
import { Button } from "./Button";
|
||||
import { Button, ButtonVariant } from "./Button";
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
children?: JSX.Element | string;
|
||||
className?: string;
|
||||
variant?: ButtonVariant;
|
||||
copiedMessage?: string;
|
||||
}
|
||||
export function CopyButton({
|
||||
value,
|
||||
children,
|
||||
@@ -27,7 +36,8 @@ export function CopyButton({
|
||||
variant,
|
||||
copiedMessage,
|
||||
...rest
|
||||
}) {
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
||||
|
||||
return (
|
||||
@@ -41,7 +51,7 @@ export function CopyButton({
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
{variant !== "icon" && <span>{copiedMessage || "Copied!"}</span>}
|
||||
{variant !== "icon" && <span>{copiedMessage || t("Copied!")}</span>}
|
||||
<CheckIcon />
|
||||
</>
|
||||
) : (
|
||||
@@ -14,12 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { variantToClassName, sizeToClassName } from "./Button";
|
||||
import * as H from "history";
|
||||
|
||||
export function LinkButton({ className, variant, size, children, ...rest }) {
|
||||
import {
|
||||
variantToClassName,
|
||||
sizeToClassName,
|
||||
ButtonVariant,
|
||||
ButtonSize,
|
||||
} from "./Button";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||
children: JSX.Element | string;
|
||||
to: H.LocationDescriptor | ((location: H.Location) => H.LocationDescriptor);
|
||||
size?: ButtonSize;
|
||||
variant?: ButtonVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LinkButton({
|
||||
children,
|
||||
to,
|
||||
size,
|
||||
variant,
|
||||
className,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
@@ -27,6 +49,7 @@ export function LinkButton({ className, variant, size, children, ...rest }) {
|
||||
sizeToClassName[size],
|
||||
className
|
||||
)}
|
||||
to={to}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
35
src/button/VolumeIcon.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
|
||||
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
|
||||
import { ReactComponent as Audio } from "../icons/Audio.svg";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Number between 0 and 1
|
||||
*/
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export function VolumeIcon({ volume }: Props) {
|
||||
if (volume <= 0) return <AudioMuted />;
|
||||
if (volume <= 0.5) return <AudioLow />;
|
||||
return <Audio />;
|
||||
}
|
||||
40
src/form/Form.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { FormEventHandler, forwardRef } from "react";
|
||||
|
||||
import styles from "./Form.module.css";
|
||||
|
||||
interface FormProps {
|
||||
className: string;
|
||||
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||
children: JSX.Element[];
|
||||
}
|
||||
|
||||
export const Form = forwardRef<HTMLFormElement, FormProps>(
|
||||
({ children, className, onSubmit }, ref) => {
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className={classNames(styles.form, className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -16,14 +16,23 @@ limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { CopyButton } from "../button";
|
||||
import { Facepile } from "../Facepile";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { Avatar, Size } from "../Avatar";
|
||||
import styles from "./CallList.module.css";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { Body, Caption } from "../typography/Typography";
|
||||
import { GroupCallRoom } from "./useGroupCallRooms";
|
||||
|
||||
export function CallList({ rooms, client, disableFacepile }) {
|
||||
interface CallListProps {
|
||||
rooms: GroupCallRoom[];
|
||||
client: MatrixClient;
|
||||
disableFacepile?: boolean;
|
||||
}
|
||||
export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.callList}>
|
||||
@@ -48,7 +57,14 @@ export function CallList({ rooms, client, disableFacepile }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface CallTileProps {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
roomId: string;
|
||||
participants: RoomMember[];
|
||||
client: MatrixClient;
|
||||
disableFacepile?: boolean;
|
||||
}
|
||||
function CallTile({
|
||||
name,
|
||||
avatarUrl,
|
||||
@@ -56,12 +72,12 @@ function CallTile({
|
||||
participants,
|
||||
client,
|
||||
disableFacepile,
|
||||
}) {
|
||||
}: CallTileProps) {
|
||||
return (
|
||||
<div className={styles.callTile}>
|
||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||
<Avatar
|
||||
size="lg"
|
||||
size={Size.LG}
|
||||
bgKey={name}
|
||||
src={avatarUrl}
|
||||
fallback={name.slice(0, 1).toUpperCase()}
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Headline } from "../typography/Typography";
|
||||
import { Button } from "../button";
|
||||
@@ -39,25 +40,29 @@ interface Props {
|
||||
}
|
||||
|
||||
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom">
|
||||
<Button variant="dropdown" className={commonStyles.headline}>
|
||||
<Headline className={styles.label}>
|
||||
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
|
||||
{callType === CallType.Video
|
||||
? t("Video call")
|
||||
: t("Walkie-talkie call")}
|
||||
</Headline>
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Menu {...props} label="Call type menu" onAction={setCallType}>
|
||||
<Item key={CallType.Video} textValue="Video call">
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu {...props} label={t("Call type menu")} onAction={setCallType}>
|
||||
<Item key={CallType.Video} textValue={t("Video call")}>
|
||||
<VideoIcon />
|
||||
<span>Video call</span>
|
||||
<span>{t("Video call")}</span>
|
||||
{callType === CallType.Video && (
|
||||
<CheckIcon className={menuStyles.checkIcon} />
|
||||
)}
|
||||
</Item>
|
||||
<Item key={CallType.Radio} textValue="Walkie-talkie call">
|
||||
<Item key={CallType.Radio} textValue={t("Walkie-talkie call")}>
|
||||
<MicIcon />
|
||||
<span>Walkie-talkie call</span>
|
||||
<span>{t("Walkie-talkie call")}</span>
|
||||
{callType === CallType.Radio && (
|
||||
<CheckIcon className={menuStyles.checkIcon} />
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useClient } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { UnauthenticatedView } from "./UnauthenticatedView";
|
||||
@@ -22,7 +24,8 @@ import { RegisteredView } from "./RegisteredView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export function HomePage() {
|
||||
usePageTitle("Home");
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Home"));
|
||||
|
||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||
useClient();
|
||||
@@ -15,19 +15,30 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { PressEvent } from "@react-types/shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow } from "../input/Input";
|
||||
import styles from "./JoinExistingCallModal.module.css";
|
||||
|
||||
export function JoinExistingCallModal({ onJoin, ...rest }) {
|
||||
interface Props {
|
||||
onJoin: (e: PressEvent) => void;
|
||||
onClose: (e: PressEvent) => void;
|
||||
// TODO: add used parameters for <Modal>
|
||||
[index: string]: unknown;
|
||||
}
|
||||
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal title="Join existing call?" isDismissable {...rest}>
|
||||
<Modal title={t("Join existing call?")} isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<p>This call already exists, would you like to join?</p>
|
||||
<p>{t("This call already exists, would you like to join?")}</p>
|
||||
<FieldRow rightAlign className={styles.buttons}>
|
||||
<Button onPress={rest.onClose}>No</Button>
|
||||
<Button onPress={onJoin}>Yes, join call</Button>
|
||||
<Button onPress={onClose}>{t("No")}</Button>
|
||||
<Button onPress={onJoin}>{t("Yes, join call")}</Button>
|
||||
</FieldRow>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
@@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
FormEvent,
|
||||
FormEventHandler,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
||||
import { useGroupCallRooms } from "./useGroupCallRooms";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||
@@ -26,28 +35,36 @@ import { CallList } from "./CallList";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Title } from "../typography/Typography";
|
||||
import { Form } from "../form/Form";
|
||||
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||
|
||||
export function RegisteredView({ client }) {
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
}
|
||||
|
||||
export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||
const [callType, setCallType] = useState(CallType.Video);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [error, setError] = useState<Error>();
|
||||
const history = useHistory();
|
||||
const onSubmit = useCallback(
|
||||
(e) => {
|
||||
const { t } = useTranslation();
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
(e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("callName");
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
const roomNameData = data.get("callName");
|
||||
const roomName = typeof roomNameData === "string" ? roomNameData : "";
|
||||
const ptt = callType === CallType.Radio;
|
||||
|
||||
async function submit() {
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
|
||||
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||
const [roomIdOrAlias] = await createRoom(client, roomName, ptt);
|
||||
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
@@ -64,23 +81,23 @@ export function RegisteredView({ client }) {
|
||||
console.error(error);
|
||||
setLoading(false);
|
||||
setError(error);
|
||||
reset();
|
||||
}
|
||||
});
|
||||
},
|
||||
[client, callType]
|
||||
[client, history, modalState, callType]
|
||||
);
|
||||
|
||||
const recentRooms = useGroupCallRooms(client);
|
||||
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [existingRoomId, setExistingRoomId] = useState();
|
||||
const [existingRoomId, setExistingRoomId] = useState<string>();
|
||||
const onJoinExistingRoom = useCallback(() => {
|
||||
history.push(`/${existingRoomId}`);
|
||||
}, [history, existingRoomId]);
|
||||
|
||||
const callNameLabel =
|
||||
callType === CallType.Video ? "Video call name" : "Walkie-talkie call name";
|
||||
callType === CallType.Video
|
||||
? t("Video call name")
|
||||
: t("Walkie-talkie call name");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -114,19 +131,19 @@ export function RegisteredView({ client }) {
|
||||
className={styles.button}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Loading..." : "Go"}
|
||||
{loading ? t("Loading…") : t("Go")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
<ErrorMessage error={error} />
|
||||
</FieldRow>
|
||||
)}
|
||||
</Form>
|
||||
{recentRooms.length > 0 && (
|
||||
<>
|
||||
<Title className={styles.recentCallsTitle}>
|
||||
Your recent Calls
|
||||
{t("Your recent calls")}
|
||||
</Title>
|
||||
<CallList rooms={recentRooms} client={client} disableFacepile />
|
||||
</>
|
||||
@@ -14,45 +14,48 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { FC, useCallback, useState, FormEventHandler } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { useClient } from "../ClientContext";
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
||||
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { useRecaptcha } from "../auth/useRecaptcha";
|
||||
import { Body, Caption, Link, Headline } from "../typography/Typography";
|
||||
import { Body, Caption, Link } from "../typography/Typography";
|
||||
import { Form } from "../form/Form";
|
||||
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||
import styles from "./UnauthenticatedView.module.css";
|
||||
import commonStyles from "./common.module.css";
|
||||
import { generateRandomName } from "../auth/generateRandomName";
|
||||
|
||||
export function UnauthenticatedView() {
|
||||
export const UnauthenticatedView: FC = () => {
|
||||
const { setClient } = useClient();
|
||||
const [callType, setCallType] = useState(CallType.Video);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [error, setError] = useState<Error>();
|
||||
const [privacyPolicyUrl, recaptchaKey, register] =
|
||||
useInteractiveRegistration();
|
||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [onFinished, setOnFinished] = useState();
|
||||
const [onFinished, setOnFinished] = useState<() => void>();
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const roomName = data.get("callName");
|
||||
const displayName = data.get("displayName");
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
const roomName = data.get("callName") as string;
|
||||
const displayName = data.get("displayName") as string;
|
||||
const ptt = callType === CallType.Radio;
|
||||
|
||||
async function submit() {
|
||||
@@ -68,12 +71,12 @@ export function UnauthenticatedView() {
|
||||
true
|
||||
);
|
||||
|
||||
let roomIdOrAlias;
|
||||
let roomIdOrAlias: string;
|
||||
try {
|
||||
roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||
[roomIdOrAlias] = await createRoom(client, roomName, ptt);
|
||||
} catch (error) {
|
||||
if (error.errcode === "M_ROOM_IN_USE") {
|
||||
setOnFinished(() => () => {
|
||||
setOnFinished(() => {
|
||||
setClient(client, session);
|
||||
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
|
||||
const [, serverName] = client.getUserId().split(":");
|
||||
@@ -100,11 +103,13 @@ export function UnauthenticatedView() {
|
||||
reset();
|
||||
});
|
||||
},
|
||||
[register, reset, execute, history, callType]
|
||||
[register, reset, execute, history, callType, modalState, setClient]
|
||||
);
|
||||
|
||||
const callNameLabel =
|
||||
callType === CallType.Video ? "Video call name" : "Walkie-talkie call name";
|
||||
callType === CallType.Video
|
||||
? t("Video call name")
|
||||
: t("Walkie-talkie call name");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -136,24 +141,26 @@ export function UnauthenticatedView() {
|
||||
<InputField
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
label="Display Name"
|
||||
placeholder="Display Name"
|
||||
label={t("Display name")}
|
||||
placeholder={t("Display name")}
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FieldRow>
|
||||
<Caption>
|
||||
By clicking "Go", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
<Trans>
|
||||
By clicking "Go", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
</Trans>
|
||||
</Caption>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
<ErrorMessage error={error} />
|
||||
</FieldRow>
|
||||
)}
|
||||
<Button type="submit" size="lg" disabled={loading}>
|
||||
{loading ? "Loading..." : "Go"}
|
||||
{loading ? t("Loading…") : t("Go")}
|
||||
</Button>
|
||||
<div id={recaptchaId} />
|
||||
</Form>
|
||||
@@ -161,14 +168,16 @@ export function UnauthenticatedView() {
|
||||
<footer className={styles.footer}>
|
||||
<Body className={styles.mobileLoginLink}>
|
||||
<Link color="primary" to="/login">
|
||||
Login to your account
|
||||
{t("Login to your account")}
|
||||
</Link>
|
||||
</Body>
|
||||
<Body>
|
||||
Not registered yet?{" "}
|
||||
<Link color="primary" to="/register">
|
||||
Create an account
|
||||
</Link>
|
||||
<Trans>
|
||||
Not registered yet?{" "}
|
||||
<Link color="primary" to="/register">
|
||||
Create an account
|
||||
</Link>
|
||||
</Trans>
|
||||
</Body>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -177,4 +186,4 @@ export function UnauthenticatedView() {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -14,11 +14,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const tsCache = {};
|
||||
export interface GroupCallRoom {
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
room: Room;
|
||||
groupCall: GroupCall;
|
||||
participants: RoomMember[];
|
||||
}
|
||||
const tsCache: { [index: string]: number } = {};
|
||||
|
||||
function getLastTs(client, r) {
|
||||
function getLastTs(client: MatrixClient, r: Room) {
|
||||
if (tsCache[r.roomId]) {
|
||||
return tsCache[r.roomId];
|
||||
}
|
||||
@@ -59,13 +72,13 @@ function getLastTs(client, r) {
|
||||
return ts;
|
||||
}
|
||||
|
||||
function sortRooms(client, rooms) {
|
||||
function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
|
||||
return rooms.sort((a, b) => {
|
||||
return getLastTs(client, b) - getLastTs(client, a);
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroupCallRooms(client) {
|
||||
export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,12 +103,15 @@ export function useGroupCallRooms(client) {
|
||||
|
||||
updateRooms();
|
||||
|
||||
client.on("GroupCall.incoming", updateRooms);
|
||||
client.on("GroupCall.participants", updateRooms);
|
||||
client.on(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||
client.on(GroupCallEventHandlerEvent.Participants, updateRooms);
|
||||
|
||||
return () => {
|
||||
client.removeListener("GroupCall.incoming", updateRooms);
|
||||
client.removeListener("GroupCall.participants", updateRooms);
|
||||
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||
client.removeListener(
|
||||
GroupCallEventHandlerEvent.Participants,
|
||||
updateRooms
|
||||
);
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
3
src/icons/AlertTriangleFilled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.47012 18H17.5301C19.0701 18 20.0301 16.33 19.2601 15L11.7301 1.98999C10.9601 0.659993 9.04012 0.659993 8.27012 1.98999L0.740121 15C-0.0298788 16.33 0.930121 18 2.47012 18ZM10.0001 11C9.45012 11 9.00012 10.55 9.00012 9.99999V7.99999C9.00012 7.44999 9.45012 6.99999 10.0001 6.99999C10.5501 6.99999 11.0001 7.44999 11.0001 7.99999V9.99999C11.0001 10.55 10.5501 11 10.0001 11ZM11.0001 15H9.00012V13H11.0001V15Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 540 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.97991 1.48403L4 4.80062L1 4.80062C0.447715 4.80062 0 5.24834 0 5.80062V10.2006C0 10.7529 0.447714 11.2006 0.999999 11.2006L4 11.2006L7.97991 14.5172C8.30557 14.7886 8.8 14.557 8.8 14.1331V1.86814C8.8 1.44422 8.30557 1.21265 7.97991 1.48403Z" fill="white"/>
|
||||
<path d="M14.1258 2.79107C13.8998 2.50044 13.4809 2.44808 13.1903 2.67413C12.9 2.89992 12.8475 3.3181 13.0726 3.6087L13.0731 3.60935L13.0738 3.61021L13.0829 3.62231C13.0917 3.63418 13.1059 3.65355 13.1248 3.68011C13.1625 3.73326 13.2187 3.81496 13.2872 3.92256C13.4243 4.13812 13.6097 4.45554 13.7955 4.85371C14.169 5.65407 14.5329 6.75597 14.5329 8.00036C14.5329 9.24475 14.169 10.3466 13.7955 11.147C13.6097 11.5452 13.4243 11.8626 13.2872 12.0782C13.2187 12.1858 13.1625 12.2675 13.1248 12.3206C13.1059 12.3472 13.0917 12.3665 13.0829 12.3784L13.0738 12.3905L13.0731 12.3914L13.0725 12.3921C12.8475 12.6827 12.9 13.1008 13.1903 13.3266C13.4809 13.5526 13.8998 13.5003 14.1258 13.2097L13.629 12.8232C14.1258 13.2096 14.1258 13.2097 14.1258 13.2097L14.1272 13.2079L14.1291 13.2055L14.1346 13.1982L14.1523 13.1748C14.1669 13.1552 14.187 13.1277 14.2119 13.0926C14.2617 13.0225 14.3305 12.9221 14.4121 12.794C14.5749 12.5381 14.7895 12.1698 15.0037 11.7109C15.4302 10.7969 15.8663 9.49883 15.8663 8.00036C15.8663 6.50189 15.4302 5.20379 15.0037 4.28987C14.7895 3.83089 14.5749 3.4626 14.4121 3.20673C14.3305 3.07862 14.2617 2.97818 14.2119 2.90811C14.187 2.87306 14.1669 2.84556 14.1523 2.82596L14.1346 2.80249L14.1291 2.79525L14.1272 2.79278L14.1264 2.79183C14.1264 2.79183 14.1258 2.79107 13.5996 3.20036L14.1258 2.79107Z" fill="white"/>
|
||||
<path d="M11.7264 5.19121C11.5004 4.90058 11.0815 4.84823 10.7909 5.07427C10.501 5.29973 10.4482 5.71698 10.6722 6.00752L10.6745 6.01057C10.6775 6.01457 10.6831 6.02223 10.691 6.03338C10.7069 6.05572 10.7318 6.09189 10.7628 6.14057C10.8249 6.23827 10.9103 6.38426 10.9961 6.56815C11.1696 6.93993 11.3335 7.44183 11.3335 8.00051C11.3335 8.55918 11.1696 9.06108 10.9961 9.43287C10.9103 9.61675 10.8249 9.76275 10.7628 9.86045C10.7318 9.90912 10.7069 9.94529 10.691 9.96763C10.6831 9.97879 10.6775 9.98645 10.6745 9.99044L10.6722 9.9935C10.4482 10.284 10.501 10.7013 10.7909 10.9267C11.0815 11.1528 11.5004 11.1004 11.7264 10.8098L11.2002 10.4005C11.7264 10.8098 11.7264 10.8098 11.7264 10.8098L11.7276 10.8083L11.7291 10.8064L11.7329 10.8014L11.7439 10.7868C11.7526 10.7751 11.7642 10.7593 11.7781 10.7396C11.806 10.7004 11.8436 10.6455 11.8876 10.5763C11.9755 10.4383 12.0901 10.2414 12.2043 9.99672C12.4308 9.51136 12.6669 8.81326 12.6669 8.00051C12.6669 7.18775 12.4308 6.48965 12.2043 6.0043C12.0901 5.75961 11.9755 5.56275 11.8876 5.42473C11.8436 5.35555 11.806 5.30065 11.7781 5.26138C11.7642 5.24173 11.7526 5.22596 11.7439 5.21422L11.7329 5.19964L11.7291 5.19465L11.7276 5.19274L11.727 5.19193C11.727 5.19193 11.7264 5.19121 11.2002 5.60051L11.7264 5.19121Z" fill="white"/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9699 2.22605L6 7.20093L1.5 7.20093C0.671573 7.20093 0 7.8725 0 8.70093V15.3009C0 16.1294 0.671571 16.8009 1.5 16.8009L6 16.8009L11.9699 21.7758C12.4584 22.1829 13.2 21.8355 13.2 21.1996V2.80221C13.2 2.16634 12.4584 1.81897 11.9699 2.22605Z" fill="white"/>
|
||||
<path d="M21.1888 4.1866C20.8497 3.75065 20.2214 3.67212 19.7855 4.01119C19.35 4.34988 19.2712 4.97715 19.6089 5.41304L19.6097 5.41402L19.6107 5.41531L19.6243 5.43347C19.6376 5.45126 19.6589 5.48033 19.6872 5.52017C19.7438 5.59988 19.828 5.72244 19.9308 5.88385C20.1365 6.20718 20.4145 6.68332 20.6932 7.28057C21.2535 8.48111 21.7994 10.134 21.7994 12.0005C21.7994 13.8671 21.2535 15.52 20.6932 16.7205C20.4145 17.3178 20.1365 17.7939 19.9308 18.1172C19.828 18.2786 19.7438 18.4012 19.6872 18.4809C19.6589 18.5208 19.6376 18.5498 19.6243 18.5676L19.6107 18.5858L19.6097 18.5871L19.6088 18.5882C19.2712 19.0241 19.3501 19.6512 19.7855 19.9899C20.2214 20.329 20.8497 20.2504 21.1888 19.8145L20.4435 19.2348C21.1888 19.8145 21.1888 19.8145 21.1888 19.8145L21.1908 19.8119L21.1936 19.8082L21.2019 19.7974L21.2284 19.7621C21.2503 19.7327 21.2805 19.6915 21.3179 19.6389C21.3925 19.5338 21.4958 19.3832 21.6181 19.191C21.8623 18.8072 22.1843 18.2547 22.5056 17.5663C23.1453 16.1954 23.7994 14.2482 23.7994 12.0005C23.7994 9.75284 23.1453 7.80569 22.5056 6.4348C22.1843 5.74634 21.8623 5.1939 21.6181 4.81009C21.4958 4.61793 21.3925 4.46727 21.3179 4.36217C21.2805 4.30959 21.2503 4.26835 21.2284 4.23893L21.2019 4.20373L21.1936 4.19288L21.1908 4.18917L21.1897 4.18774C21.1897 4.18774 21.1888 4.1866 20.3994 4.80054L21.1888 4.1866Z" fill="white"/>
|
||||
<path d="M17.5896 7.78682C17.2506 7.35087 16.6223 7.27234 16.1864 7.61141C15.7515 7.94959 15.6723 8.57548 16.0083 9.01128L16.0117 9.01586C16.0162 9.02185 16.0246 9.03334 16.0365 9.05007C16.0603 9.08359 16.0977 9.13784 16.1441 9.21085C16.2374 9.3574 16.3654 9.57639 16.4941 9.85222C16.7544 10.4099 17.0003 11.1627 17.0003 12.0008C17.0003 12.8388 16.7544 13.5916 16.4941 14.1493C16.3654 14.4251 16.2374 14.6441 16.1441 14.7907C16.0977 14.8637 16.0603 14.9179 16.0365 14.9514C16.0246 14.9682 16.0162 14.9797 16.0117 14.9857L16.0083 14.9903C15.6723 15.4261 15.7515 16.0519 16.1864 16.3901C16.6223 16.7292 17.2506 16.6506 17.5896 16.2147L16.8003 15.6008C17.5896 16.2147 17.5896 16.2147 17.5896 16.2147L17.5914 16.2124L17.5936 16.2095L17.5994 16.2021L17.6158 16.1802C17.6289 16.1626 17.6463 16.1389 17.6672 16.1094C17.709 16.0505 17.7654 15.9682 17.8315 15.8644C17.9632 15.6574 18.1352 15.3621 18.3065 14.9951C18.6462 14.267 19.0003 13.2199 19.0003 12.0008C19.0003 10.7816 18.6462 9.73448 18.3065 9.00645C18.1352 8.63942 17.9632 8.34412 17.8315 8.1371C17.7654 8.03333 17.709 7.95097 17.6672 7.89207C17.6463 7.8626 17.6289 7.83893 17.6158 7.82132L17.5994 7.79946L17.5936 7.79198L17.5914 7.78911L17.5905 7.78789C17.5905 7.78789 17.5896 7.78682 16.8003 8.40076L17.5896 7.78682Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
4
src/icons/AudioLow.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9699 2.22605L6 7.20093L1.5 7.20093C0.671573 7.20093 0 7.8725 0 8.70093V15.3009C0 16.1294 0.671571 16.8009 1.5 16.8009L6 16.8009L11.9699 21.7758C12.4584 22.1829 13.2 21.8355 13.2 21.1996V2.80221C13.2 2.16634 12.4584 1.81897 11.9699 2.22605Z" fill="white"/>
|
||||
<path d="M17.5896 7.78682C17.2506 7.35087 16.6223 7.27234 16.1864 7.61141C15.7515 7.94959 15.6723 8.57548 16.0083 9.01128L16.0117 9.01586C16.0162 9.02185 16.0246 9.03334 16.0365 9.05007C16.0603 9.08359 16.0977 9.13784 16.1441 9.21085C16.2374 9.3574 16.3654 9.57639 16.4941 9.85222C16.7544 10.4099 17.0003 11.1627 17.0003 12.0008C17.0003 12.8388 16.7544 13.5916 16.4941 14.1493C16.3654 14.4251 16.2374 14.6441 16.1441 14.7907C16.0977 14.8637 16.0603 14.9179 16.0365 14.9514C16.0246 14.9682 16.0162 14.9797 16.0117 14.9857L16.0083 14.9903C15.6723 15.4261 15.7515 16.0519 16.1864 16.3901C16.6223 16.7292 17.2506 16.6506 17.5896 16.2147L16.8003 15.6008C17.5896 16.2147 17.5896 16.2147 17.5896 16.2147L17.5914 16.2124L17.5936 16.2095L17.5994 16.2021L17.6158 16.1802C17.6289 16.1626 17.6463 16.1389 17.6672 16.1094C17.709 16.0505 17.7654 15.9682 17.8315 15.8644C17.9632 15.6574 18.1352 15.3621 18.3065 14.9951C18.6462 14.267 19.0003 13.2199 19.0003 12.0008C19.0003 10.7816 18.6462 9.73448 18.3065 9.00645C18.1352 8.63942 17.9632 8.34412 17.8315 8.1371C17.7654 8.03333 17.709 7.95097 17.6672 7.89207C17.6463 7.8626 17.6289 7.83893 17.6158 7.82132L17.5994 7.79946L17.5936 7.79198L17.5914 7.78911L17.5905 7.78789C17.5905 7.78789 17.5896 7.78682 16.8003 8.40076L17.5896 7.78682Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
src/icons/AudioMuted.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.63174 0.583224C2.15798 0.109466 1.38987 0.109466 0.91611 0.583224C0.442351 1.05698 0.442351 1.8251 0.91611 2.29885L5.3958 6.77855H5.37083L15.3629 16.7706V16.7456L20.7144 22.0972C21.1882 22.5709 21.9563 22.5709 22.4301 22.0972C22.9038 21.6234 22.9038 20.8553 22.4301 20.3816L2.63174 0.583224ZM15.3629 3.23319V9.88521L10.2275 4.74987L13.2404 2.2391C14.0833 1.53675 15.3629 2.13608 15.3629 3.23319ZM4.07191 16.8718H7.7929V16.872L13.2404 21.4116C14.0833 22.114 15.3629 21.5146 15.3629 20.4175V20.2018L2.4839 7.32287C1.87536 7.79641 1.48389 8.53577 1.48389 9.36657V14.2838C1.48389 15.7131 2.64258 16.8718 4.07191 16.8718Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
3
src/icons/Fullscreen.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 8.59V4C21 3.45 20.55 3 20 3H15.41C14.52 3 14.07 4.08 14.7 4.71L16.29 6.3L6.29 16.3L4.7 14.71C4.08 14.08 3 14.52 3 15.41V20C3 20.55 3.45 21 4 21H8.59C9.48 21 9.93 19.92 9.3 19.29L7.71 17.7L17.71 7.7L19.3 9.29C19.92 9.92 21 9.48 21 8.59Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
3
src/icons/FullscreenExit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.29 4.12L16.7 8.71L18.29 10.3C18.92 10.93 18.47 12.01 17.58 12.01H13C12.45 12.01 12 11.56 12 11.01V6.41C12 5.52 13.08 5.07 13.71 5.7L15.3 7.29L19.89 2.7C20.28 2.31 20.91 2.31 21.3 2.7C21.68 3.1 21.68 3.73 21.29 4.12ZM4.11997 21.29L8.70997 16.7L10.3 18.29C10.93 18.92 12.01 18.47 12.01 17.58V13C12.01 12.45 11.56 12 11.01 12H6.40997C5.51997 12 5.06997 13.08 5.69997 13.71L7.28997 15.3L2.69997 19.89C2.30997 20.28 2.30997 20.91 2.69997 21.3C3.09997 21.68 3.72997 21.68 4.11997 21.29Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 613 B |
@@ -1,4 +1,5 @@
|
||||
<svg width="260" height="30" viewBox="0 0 260 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<desc>Element Call (Beta)</desc>
|
||||
<circle cx="15" cy="15" r="13" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 30C23.2843 30 30 23.2843 30 15C30 6.71573 23.2843 0 15 0C6.71573 0 0 6.71573 0 15C0 23.2843 6.71573 30 15 30ZM12.2579 6.98923C12.2579 6.38376 12.7497 5.89292 13.3565 5.89292C17.4687 5.89292 20.8024 9.21967 20.8024 13.3234C20.8024 13.9289 20.3106 14.4197 19.7038 14.4197C19.0971 14.4197 18.6052 13.9289 18.6052 13.3234C18.6052 10.4306 16.2553 8.08554 13.3565 8.08554C12.7497 8.08554 12.2579 7.59471 12.2579 6.98923ZM24.1066 13.3235C24.1066 12.7181 23.6148 12.2272 23.008 12.2272C22.4013 12.2272 21.9094 12.7181 21.9094 13.3235C21.9094 16.2163 19.5595 18.5614 16.6607 18.5614C16.0539 18.5614 15.5621 19.0523 15.5621 19.6577C15.5621 20.2632 16.0539 20.754 16.6607 20.754C20.7729 20.754 24.1066 17.4273 24.1066 13.3235ZM17.7601 23.011C17.7601 23.6164 17.2682 24.1073 16.6615 24.1073C12.5492 24.1073 9.21553 20.7805 9.21553 16.6768C9.21553 16.0713 9.70739 15.5805 10.3141 15.5805C10.9209 15.5805 11.4127 16.0713 11.4127 16.6768C11.4127 19.5696 13.7627 21.9146 16.6615 21.9146C17.2682 21.9146 17.7601 22.4055 17.7601 23.011ZM5.89281 16.6769C5.89281 17.2824 6.38466 17.7732 6.9914 17.7732C7.59813 17.7732 8.08999 17.2824 8.08999 16.6769C8.08999 13.7841 10.4399 11.439 13.3388 11.439C13.9455 11.439 14.4373 10.9482 14.4373 10.3427C14.4373 9.73722 13.9455 9.24639 13.3388 9.24639C9.22647 9.24639 5.89281 12.5731 5.89281 16.6769Z" fill="#0DBD8B"/>
|
||||
<path d="M53.5406 17.258H42.8052C42.932 18.3814 43.3397 19.2782 44.0282 19.9486C44.7167 20.6009 45.6227 20.927 46.746 20.927C47.4889 20.927 48.1593 20.7459 48.7572 20.3835C49.3551 20.0211 49.7809 19.5319 50.0346 18.9159H53.296C52.8611 20.3472 52.0458 21.5068 50.8499 22.3947C49.6722 23.2644 48.2771 23.6992 46.6645 23.6992C44.5627 23.6992 42.8596 23.0016 41.555 21.6065C40.2686 20.2114 39.6254 18.4448 39.6254 16.3068C39.6254 14.2231 40.2776 12.4747 41.5822 11.0614C42.8867 9.64814 44.5718 8.94151 46.6373 8.94151C48.7029 8.94151 50.3698 9.63908 51.6381 11.0342C52.9245 12.4112 53.5677 14.1506 53.5677 16.2524L53.5406 17.258ZM46.6373 11.5778C45.6227 11.5778 44.7801 11.8767 44.1098 12.4747C43.4394 13.0726 43.0226 13.8698 42.8596 14.8663H50.3607C50.2158 13.8698 49.8172 13.0726 49.1649 12.4747C48.5126 11.8767 47.6701 11.5778 46.6373 11.5778Z" fill="white"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -33,10 +33,12 @@ limitations under the License.
|
||||
--primary-content: #ffffff;
|
||||
--secondary-content: #a9b2bc;
|
||||
--tertiary-content: #8e99a4;
|
||||
--tertiary-content-20: #8e99a433;
|
||||
--quaternary-content: #6f7882;
|
||||
--quinary-content: #394049;
|
||||
--system: #21262c;
|
||||
--background: #15191e;
|
||||
--background-85: rgba(23, 25, 28, 0.85);
|
||||
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
|
||||
}
|
||||
|
||||
|
||||
@@ -15,42 +15,55 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { AllHTMLAttributes, useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { Button } from "../button";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Avatar, Size } from "../Avatar";
|
||||
import { Button } from "../button";
|
||||
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
|
||||
import styles from "./AvatarInputField.module.css";
|
||||
|
||||
export const AvatarInputField = forwardRef(
|
||||
interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
||||
id: string;
|
||||
label: string;
|
||||
avatarUrl: string;
|
||||
displayName: string;
|
||||
onRemoveAvatar: () => void;
|
||||
}
|
||||
|
||||
export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
||||
(
|
||||
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [objUrl, setObjUrl] = useState(null);
|
||||
const [objUrl, setObjUrl] = useState<string>(null);
|
||||
|
||||
const fileInputRef = useObjectRef(ref);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(e.target.files[0]));
|
||||
const currentInput = fileInputRef.current;
|
||||
|
||||
const onChange = (e: Event) => {
|
||||
const inputEvent = e as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||
if (inputEvent.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
|
||||
setRemoved(false);
|
||||
} else {
|
||||
setObjUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
fileInputRef.current.addEventListener("change", onChange);
|
||||
currentInput.addEventListener("change", onChange);
|
||||
|
||||
return () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.removeEventListener("change", onChange);
|
||||
}
|
||||
currentInput?.removeEventListener("change", onChange);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -63,7 +76,7 @@ export const AvatarInputField = forwardRef(
|
||||
<div className={classNames(styles.avatarInputField, className)}>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
size="xl"
|
||||
size={Size.XL}
|
||||
src={removed ? null : objUrl || avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
@@ -87,7 +100,7 @@ export const AvatarInputField = forwardRef(
|
||||
variant="icon"
|
||||
onPress={onPressRemoveAvatar}
|
||||
>
|
||||
Remove
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -14,12 +14,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { ChangeEvent, FC, forwardRef, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./Input.module.css";
|
||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||
import { TranslatedError } from "../TranslatedError";
|
||||
|
||||
export function FieldRow({ children, rightAlign, className, ...rest }) {
|
||||
interface FieldRowProps {
|
||||
children: ReactNode;
|
||||
rightAlign?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FieldRow({
|
||||
children,
|
||||
rightAlign,
|
||||
className,
|
||||
}: FieldRowProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -33,11 +45,42 @@ export function FieldRow({ children, rightAlign, className, ...rest }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Field({ children, className, ...rest }) {
|
||||
interface FieldProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Field({ children, className }: FieldProps): JSX.Element {
|
||||
return <div className={classNames(styles.field, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export const InputField = forwardRef(
|
||||
interface InputFieldProps {
|
||||
label: string;
|
||||
type: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
id?: string;
|
||||
checked?: boolean;
|
||||
className?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
// this is a hack. Those variables should be part of `HTMLAttributes<HTMLInputElement> | HTMLAttributes<HTMLTextAreaElement>`
|
||||
// but extending from this union type does not work
|
||||
name?: string;
|
||||
autoComplete?: string;
|
||||
autoCorrect?: string;
|
||||
autoCapitalize?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
defaultChecked?: boolean;
|
||||
onChange?: (event: ChangeEvent) => void;
|
||||
}
|
||||
|
||||
export const InputField = forwardRef<
|
||||
HTMLInputElement | HTMLTextAreaElement,
|
||||
InputFieldProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
@@ -68,19 +111,18 @@ export const InputField = forwardRef(
|
||||
{type === "textarea" ? (
|
||||
<textarea
|
||||
id={id}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type={type}
|
||||
ref={ref as React.ForwardedRef<HTMLTextAreaElement>}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={id}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
ref={ref as React.ForwardedRef<HTMLInputElement>}
|
||||
type={type}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -99,6 +141,12 @@ export const InputField = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
export function ErrorMessage({ children }) {
|
||||
return <p className={styles.errorMessage}>{children}</p>;
|
||||
interface ErrorMessageProps {
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export const ErrorMessage: FC<ErrorMessageProps> = ({ error }) => (
|
||||
<p className={styles.errorMessage}>
|
||||
{error instanceof TranslatedError ? error.translatedMessage : error.message}
|
||||
</p>
|
||||
);
|
||||
@@ -15,16 +15,23 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { HiddenSelect, useSelect } from "@react-aria/select";
|
||||
import { AriaSelectOptions, HiddenSelect, useSelect } from "@react-aria/select";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { useSelectState } from "@react-stately/select";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Popover } from "../popover/Popover";
|
||||
import { ListBox } from "../ListBox";
|
||||
import styles from "./SelectInput.module.css";
|
||||
import classNames from "classnames";
|
||||
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
||||
|
||||
export function SelectInput(props) {
|
||||
interface Props extends AriaSelectOptions<object> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SelectInput(props: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const state = useSelectState(props);
|
||||
|
||||
const ref = useRef();
|
||||
@@ -51,7 +58,7 @@ export function SelectInput(props) {
|
||||
<span {...valueProps} className={styles.selectedItem}>
|
||||
{state.selectedItem
|
||||
? state.selectedItem.rendered
|
||||
: "Select an option"}
|
||||
: t("Select an option")}
|
||||
</span>
|
||||
<ArrowDownIcon />
|
||||
</button>
|
||||
@@ -15,22 +15,37 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import styles from "./Toggle.module.css";
|
||||
import { useToggleButton } from "@react-aria/button";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./Toggle.module.css";
|
||||
import { Field } from "./Input";
|
||||
|
||||
export function Toggle({ id, label, className, onChange, isSelected }) {
|
||||
const buttonRef = useRef();
|
||||
interface Props {
|
||||
id: string;
|
||||
label: string;
|
||||
onChange: (selected: boolean) => void;
|
||||
isSelected: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Toggle({
|
||||
id,
|
||||
label,
|
||||
className,
|
||||
onChange,
|
||||
isSelected,
|
||||
}: Props): JSX.Element {
|
||||
const buttonRef = useRef<HTMLButtonElement>();
|
||||
const toggle = useCallback(() => {
|
||||
onChange(!isSelected);
|
||||
});
|
||||
const { buttonProps } = useToggleButton(
|
||||
}, [isSelected, onChange]);
|
||||
|
||||
const buttonProps = useToggleButton(
|
||||
{ isSelected },
|
||||
{ toggle },
|
||||
{ isSelected: isSelected, setSelected: undefined, toggle },
|
||||
buttonRef
|
||||
);
|
||||
|
||||
return (
|
||||
<Field
|
||||
className={classNames(
|
||||
80
src/main.tsx
@@ -20,22 +20,44 @@ limitations under the License.
|
||||
// dependency references.
|
||||
import "matrix-js-sdk/src/browser-index";
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { createBrowserHistory } from "history";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import { init as initRageshake } from "./settings/rageshake";
|
||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
|
||||
initRageshake();
|
||||
|
||||
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
|
||||
let fatalError: Error | null = null;
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
fatalError = new Error(
|
||||
"This app cannot run in an insecure context. To fix this, access the app " +
|
||||
"via a local loopback address, or serve it over HTTPS.\n" +
|
||||
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
|
||||
);
|
||||
} else if (!navigator.mediaDevices) {
|
||||
fatalError = new Error("Your browser does not support WebRTC.");
|
||||
}
|
||||
|
||||
if (fatalError !== null) {
|
||||
root.render(fatalError.message);
|
||||
throw fatalError; // Stop the app early
|
||||
}
|
||||
|
||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||
const style = document.documentElement.style;
|
||||
style.setProperty("--accent", import.meta.env.VITE_THEME_ACCENT as string);
|
||||
@@ -61,6 +83,10 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||
"--tertiary-content",
|
||||
import.meta.env.VITE_THEME_TERTIARY_CONTENT as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--tertiary-content-20",
|
||||
import.meta.env.VITE_THEME_TERTIARY_CONTENT_20 as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--quaternary-content",
|
||||
import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string
|
||||
@@ -74,6 +100,10 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||
"--background",
|
||||
import.meta.env.VITE_THEME_BACKGROUND as string
|
||||
);
|
||||
style.setProperty(
|
||||
"--background-85",
|
||||
import.meta.env.VITE_THEME_BACKGROUND_85 as string
|
||||
);
|
||||
}
|
||||
|
||||
const history = createBrowserHistory();
|
||||
@@ -90,13 +120,37 @@ Sentry.init({
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Sentry.ErrorBoundary fallback={ErrorView}>
|
||||
<InspectorContextProvider>
|
||||
<App history={history} />
|
||||
</InspectorContextProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.addDetector({
|
||||
name: "urlFragment",
|
||||
// Look for a language code in the URL's fragment
|
||||
lookup: () => getUrlParams().lang ?? undefined,
|
||||
});
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en-GB",
|
||||
defaultNS: "app",
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
pluralSeparator: "|",
|
||||
contextSeparator: "|",
|
||||
interpolation: {
|
||||
escapeValue: false, // React has built-in XSS protections
|
||||
},
|
||||
detection: {
|
||||
// No localStorage detectors or caching here, since we don't have any way
|
||||
// of letting the user manually select a language
|
||||
order: ["urlFragment", "navigator"],
|
||||
caches: [],
|
||||
},
|
||||
});
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App history={history} />
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,46 @@
|
||||
import Olm from "@matrix-org/olm";
|
||||
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
|
||||
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
||||
import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage";
|
||||
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { loadOlm } from "./olm";
|
||||
|
||||
export const defaultHomeserver =
|
||||
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
|
||||
`${window.location.protocol}//${window.location.host}`;
|
||||
export const fallbackICEServerAllowed =
|
||||
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
|
||||
|
||||
export const defaultHomeserverHost = new URL(defaultHomeserver).host;
|
||||
|
||||
export class CryptoStoreIntegrityError extends Error {
|
||||
constructor() {
|
||||
super("Crypto store data was expected, but none was found");
|
||||
}
|
||||
}
|
||||
|
||||
const SYNC_STORE_NAME = "element-call-sync";
|
||||
// Note that the crypto store name has changed from previous versions
|
||||
// deliberately in order to force a logout for all users due to
|
||||
// https://github.com/vector-im/element-call/issues/464
|
||||
// (It's a good opportunity to make the database names consistent.)
|
||||
const CRYPTO_STORE_NAME = "element-call-crypto";
|
||||
|
||||
function waitForSync(client: MatrixClient) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const onSync = (
|
||||
@@ -41,15 +60,22 @@ function waitForSync(client: MatrixClient) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises and returns a new standalone Matrix Client.
|
||||
* If true is passed for the 'restore' parameter, a check will be made
|
||||
* to ensure that corresponding crypto data is stored and recovered.
|
||||
* If the check fails, CryptoStoreIntegrityError will be thrown.
|
||||
* @param clientOptions Object of options passed through to the client
|
||||
* @param restore Whether the session is being restored from storage
|
||||
* @returns The MatrixClient instance
|
||||
*/
|
||||
export async function initClient(
|
||||
clientOptions: ICreateClientOpts
|
||||
clientOptions: ICreateClientOpts,
|
||||
restore: boolean
|
||||
): Promise<MatrixClient> {
|
||||
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||
window.OLM_OPTIONS = {};
|
||||
await Olm.init({ locateFile: () => olmWasmPath });
|
||||
await loadOlm();
|
||||
|
||||
let indexedDB: IDBFactory;
|
||||
|
||||
try {
|
||||
indexedDB = window.indexedDB;
|
||||
} catch (e) {}
|
||||
@@ -59,30 +85,72 @@ export async function initClient(
|
||||
if (indexedDB && localStorage && !import.meta.env.DEV) {
|
||||
storeOpts.store = new IndexedDBStore({
|
||||
indexedDB: window.indexedDB,
|
||||
localStorage: window.localStorage,
|
||||
dbName: "element-call-sync",
|
||||
localStorage,
|
||||
dbName: SYNC_STORE_NAME,
|
||||
workerFactory: () => new IndexedDBWorker(),
|
||||
});
|
||||
} else if (localStorage) {
|
||||
storeOpts.store = new MemoryStore({ localStorage });
|
||||
}
|
||||
|
||||
if (localStorage) {
|
||||
storeOpts.sessionStore = new WebStorageSessionStore(localStorage);
|
||||
// Check whether we have crypto data store. If we are restoring a session
|
||||
// from storage then we will have started the crypto store and therefore
|
||||
// have generated keys for that device, so if we can't recover those keys,
|
||||
// we must not continue or we'll generate new keys and anyone who saw our
|
||||
// previous keys will not accept our new key.
|
||||
// It's worth mentioning here that if support for indexeddb or localstorage
|
||||
// appears or disappears between sessions (it happens) then the failure mode
|
||||
// here will be that we'll try a different store, not find crypto data and
|
||||
// fail to restore the session. An alternative would be to continue using
|
||||
// whatever we were using before, but that could be confusing since you could
|
||||
// enable indexeddb and but the app would still not be using it.
|
||||
if (restore) {
|
||||
if (indexedDB) {
|
||||
const cryptoStoreExists = await IndexedDBCryptoStore.exists(
|
||||
indexedDB,
|
||||
CRYPTO_STORE_NAME
|
||||
);
|
||||
if (!cryptoStoreExists) throw new CryptoStoreIntegrityError();
|
||||
} else if (localStorage) {
|
||||
if (!LocalStorageCryptoStore.exists(localStorage))
|
||||
throw new CryptoStoreIntegrityError();
|
||||
} else {
|
||||
// if we get here then we're using the memory store, which cannot
|
||||
// possibly have remembered a session, so it's an error.
|
||||
throw new CryptoStoreIntegrityError();
|
||||
}
|
||||
}
|
||||
|
||||
if (indexedDB) {
|
||||
storeOpts.cryptoStore = new IndexedDBCryptoStore(
|
||||
indexedDB,
|
||||
"matrix-js-sdk:crypto"
|
||||
CRYPTO_STORE_NAME
|
||||
);
|
||||
} else if (localStorage) {
|
||||
storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
|
||||
} else {
|
||||
storeOpts.cryptoStore = new MemoryCryptoStore();
|
||||
}
|
||||
|
||||
// XXX: we read from the URL params in RoomPage too:
|
||||
// it would be much better to read them in one place and pass
|
||||
// the values around, but we initialise the matrix client in
|
||||
// many different places so we'd have to pass it into all of
|
||||
// them.
|
||||
const { e2eEnabled } = getUrlParams();
|
||||
if (!e2eEnabled) {
|
||||
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
...storeOpts,
|
||||
...clientOptions,
|
||||
useAuthorizationHeader: true,
|
||||
// Use a relatively low timeout for API calls: this is a realtime application
|
||||
// Use a relatively low timeout for API calls: this is a realtime app
|
||||
// so we don't want API calls taking ages, we'd rather they just fail.
|
||||
localTimeoutMs: 5000,
|
||||
useE2eForGroupCall: e2eEnabled,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -155,9 +223,9 @@ export function isLocalRoomId(roomId: string): boolean {
|
||||
export async function createRoom(
|
||||
client: MatrixClient,
|
||||
name: string,
|
||||
isPtt = false
|
||||
): Promise<string> {
|
||||
const createRoomResult = await client.createRoom({
|
||||
ptt: boolean
|
||||
): Promise<[string, string]> {
|
||||
const createPromise = client.createRoom({
|
||||
visibility: Visibility.Private,
|
||||
preset: Preset.PublicChat,
|
||||
name,
|
||||
@@ -187,29 +255,50 @@ export async function createRoom(
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Creating ${isPtt ? "PTT" : "video"} group call room`);
|
||||
// Wait for the room to arrive
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onRoom = async (room: Room) => {
|
||||
if (room.roomId === (await createPromise).room_id) {
|
||||
resolve();
|
||||
cleanUp();
|
||||
}
|
||||
};
|
||||
createPromise.catch((e) => {
|
||||
reject(e);
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
const cleanUp = () => {
|
||||
client.off(ClientEvent.Room, onRoom);
|
||||
};
|
||||
client.on(ClientEvent.Room, onRoom);
|
||||
});
|
||||
|
||||
const result = await createPromise;
|
||||
|
||||
console.log(`Creating ${ptt ? "PTT" : "video"} group call room`);
|
||||
|
||||
await client.createGroupCall(
|
||||
createRoomResult.room_id,
|
||||
isPtt ? GroupCallType.Voice : GroupCallType.Video,
|
||||
isPtt,
|
||||
GroupCallIntent.Prompt
|
||||
result.room_id,
|
||||
ptt ? GroupCallType.Voice : GroupCallType.Video,
|
||||
ptt,
|
||||
GroupCallIntent.Room
|
||||
);
|
||||
|
||||
return fullAliasFromRoomName(name, client);
|
||||
return [fullAliasFromRoomName(name, client), result.room_id];
|
||||
}
|
||||
|
||||
export function getRoomUrl(roomId: string): string {
|
||||
if (roomId.startsWith("#")) {
|
||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||
export function getRoomUrl(roomIdOrAlias: string): string {
|
||||
if (roomIdOrAlias.startsWith("#")) {
|
||||
const [localPart, host] = roomIdOrAlias.replace("#", "").split(":");
|
||||
|
||||
if (host !== defaultHomeserverHost) {
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`;
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||
}
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
89
src/media-utils.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
/**
|
||||
* Finds a media device with label matching 'deviceName'
|
||||
* @param deviceName The label of the device to look for
|
||||
* @param devices The list of devices to search
|
||||
* @returns A matching media device or undefined if no matching device was found
|
||||
*/
|
||||
export async function findDeviceByName(
|
||||
deviceName: string,
|
||||
kind: MediaDeviceKind,
|
||||
devices: MediaDeviceInfo[]
|
||||
): Promise<string | undefined> {
|
||||
const deviceInfo = devices.find(
|
||||
(d) => d.kind === kind && d.label === deviceName
|
||||
);
|
||||
return deviceInfo?.deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the available audio input/output and video input devices
|
||||
* from the browser: a wrapper around mediaDevices.enumerateDevices()
|
||||
* that requests a stream and holds it while calling enumerateDevices().
|
||||
* This is because some browsers (Firefox) only return device labels when
|
||||
* the app has an active user media stream. In Chrome, this will get a
|
||||
* stream from the default camera which can mean, for example, that the
|
||||
* light for the FaceTime camera turns on briefly even if you selected
|
||||
* another camera. Once the Permissions API
|
||||
* (https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)
|
||||
* is ready for primetime, this should allow us to avoid this.
|
||||
*
|
||||
* @return The available media devices
|
||||
*/
|
||||
export async function getDevices(): Promise<MediaDeviceInfo[]> {
|
||||
// First get the devices without their labels, to learn what kinds of streams
|
||||
// we can request
|
||||
let devices: MediaDeviceInfo[];
|
||||
try {
|
||||
devices = await navigator.mediaDevices.enumerateDevices();
|
||||
} catch (error) {
|
||||
logger.warn("Unable to refresh WebRTC devices", error);
|
||||
devices = [];
|
||||
}
|
||||
|
||||
let stream: MediaStream | null = null;
|
||||
try {
|
||||
if (devices.some((d) => d.kind === "audioinput")) {
|
||||
// Holding just an audio stream will be enough to get us all device labels
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
} else if (devices.some((d) => d.kind === "videoinput")) {
|
||||
// We have to resort to a video stream
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info("Couldn't get media stream for enumerateDevices: failing");
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (stream !== null) {
|
||||
try {
|
||||
return await navigator.mediaDevices.enumerateDevices();
|
||||
} catch (error) {
|
||||
logger.warn("Unable to refresh WebRTC devices", error);
|
||||
} finally {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all else failed, continue without device labels
|
||||
return devices;
|
||||
}
|
||||
29
src/olm.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 Olm from "@matrix-org/olm";
|
||||
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
|
||||
|
||||
// https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||
window.OLM_OPTIONS = {};
|
||||
|
||||
let olmLoaded: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Loads Olm, if not already loaded.
|
||||
*/
|
||||
export const loadOlm = (): Promise<void> =>
|
||||
(olmLoaded ??= Olm.init({ locateFile: () => olmWasmPath }));
|
||||