Compare commits
533 Commits
robin/rese
...
valere/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23bcb5777c | ||
|
|
06337b9ae6 | ||
|
|
1fa2eba0c0 | ||
|
|
f99d01fc38 | ||
|
|
301cf2f4eb | ||
|
|
918d67b2ac | ||
|
|
d52656c553 | ||
|
|
0823936ee1 | ||
|
|
150cdf64c8 | ||
|
|
552e1215be | ||
|
|
185d7d1177 | ||
|
|
c572d2e779 | ||
|
|
348a6fe1af | ||
|
|
a638acde2c | ||
|
|
e06f288e93 | ||
|
|
db2004fb90 | ||
|
|
e0b9a85f02 | ||
|
|
6be7749091 | ||
|
|
5245b22d80 | ||
|
|
75a8dd21eb | ||
|
|
2c5afe82c4 | ||
|
|
58cd12b9b8 | ||
|
|
a1c7255cc6 | ||
|
|
9ca8962328 | ||
|
|
8da111d39d | ||
|
|
19daf5f5f3 | ||
|
|
5526cd74cb | ||
|
|
e7e0facaa5 | ||
|
|
1a26a85a78 | ||
|
|
4936cdfbf6 | ||
|
|
340265a838 | ||
|
|
1b3a56427f | ||
|
|
27f24ca565 | ||
|
|
a12b9ccbb4 | ||
|
|
13894aaf3a | ||
|
|
d5efba285b | ||
|
|
717c7420f9 | ||
|
|
414322e5d9 | ||
|
|
1eec7314e8 | ||
|
|
2c66e11a0a | ||
|
|
102e581c41 | ||
|
|
b0eb566a4f | ||
|
|
2dc6134606 | ||
|
|
ea17ed7253 | ||
|
|
9f4e99310b | ||
|
|
625cfa15ff | ||
|
|
95069fd3fa | ||
|
|
13636b78d9 | ||
|
|
bcbf7a90f0 | ||
|
|
2de7a20d8a | ||
|
|
a6c4fb4148 | ||
|
|
b030d304df | ||
|
|
392c51fbf6 | ||
|
|
a18700cbcd | ||
|
|
93d763f58f | ||
|
|
58d60b35fd | ||
|
|
60332dc2db | ||
|
|
a9db9c8b59 | ||
|
|
a5aba928dd | ||
|
|
0e1b61a5e8 | ||
|
|
8823be67c5 | ||
|
|
8e6eb70e5b | ||
|
|
c846ea6447 | ||
|
|
fc2384e330 | ||
|
|
2fc7f11bea | ||
|
|
cca46bd8dd | ||
|
|
1e75f9adcc | ||
|
|
4a8f5bccac | ||
|
|
4608d68cd9 | ||
|
|
0fd414358c | ||
|
|
1ab081d636 | ||
|
|
6710f4c72a | ||
|
|
de5f5198dd | ||
|
|
f9f2896a6a | ||
|
|
a500915c43 | ||
|
|
7cbb1ec1e8 | ||
|
|
39f8cb95ec | ||
|
|
4c6b960da3 | ||
|
|
85ffe68d98 | ||
|
|
2c576a7477 | ||
|
|
64c2e5911c | ||
|
|
5be3b91509 | ||
|
|
2d7e4247f3 | ||
|
|
00daf834b6 | ||
|
|
dcc3ab641f | ||
|
|
dee06a4b70 | ||
|
|
3691e7120d | ||
|
|
8778be8351 | ||
|
|
e88474452f | ||
|
|
b1d143720a | ||
|
|
5da780ed30 | ||
|
|
c96e81bfd3 | ||
|
|
e346c8c148 | ||
|
|
1a4b38cf93 | ||
|
|
5835cd423e | ||
|
|
427a8dd644 | ||
|
|
afe004c6e7 | ||
|
|
f5ea734a5c | ||
|
|
13fb46644c | ||
|
|
669bc76dd5 | ||
|
|
05e7b5a7ff | ||
|
|
18ba02c9c2 | ||
|
|
529cb8a7ec | ||
|
|
7437961195 | ||
|
|
c820ba3983 | ||
|
|
c3c0516f0d | ||
|
|
597e6782a8 | ||
|
|
f08805d3ed | ||
|
|
3ac2aa8526 | ||
|
|
91a366fa2a | ||
|
|
1fff71ace1 | ||
|
|
1820cac3f6 | ||
|
|
86fb026be8 | ||
|
|
00401ca38a | ||
|
|
84f95be48d | ||
|
|
0502f66e21 | ||
|
|
68aae4a8e3 | ||
|
|
dfaa6a33f4 | ||
|
|
e8bf817f88 | ||
|
|
6a1f7dd057 | ||
|
|
22900161d6 | ||
|
|
47c876f3df | ||
|
|
3d8639df03 | ||
|
|
879a1d4af1 | ||
|
|
71127a4e94 | ||
|
|
b00f7d5409 | ||
|
|
2819c79590 | ||
|
|
edf68d16b7 | ||
|
|
a4a0a58a72 | ||
|
|
dbdf853d55 | ||
|
|
0759f9b27d | ||
|
|
4980d8a622 | ||
|
|
d24da1859e | ||
|
|
530fbaf90a | ||
|
|
6cf020763e | ||
|
|
edd3eb8747 | ||
|
|
f99a256c86 | ||
|
|
8e21ea6d51 | ||
|
|
6b44f3b008 | ||
|
|
96e96a5e43 | ||
|
|
7777179935 | ||
|
|
78e9521f22 | ||
|
|
342dd2e7d7 | ||
|
|
8bf24895ce | ||
|
|
dddda7057f | ||
|
|
02f23e25fd | ||
|
|
b2c594d36c | ||
|
|
d9fe31039f | ||
|
|
41e152f420 | ||
|
|
ccfd32c9b2 | ||
|
|
38d78ddce4 | ||
|
|
cc870c3cc2 | ||
|
|
c8098734cd | ||
|
|
e08f16f889 | ||
|
|
386dc6c84d | ||
|
|
d46fe55a67 | ||
|
|
02f4c73759 | ||
|
|
598371b807 | ||
|
|
802ebf828d | ||
|
|
e4a54e3a19 | ||
|
|
a617a92e88 | ||
|
|
33bc78eec1 | ||
|
|
8ffb360114 | ||
|
|
55b46b3f33 | ||
|
|
6bdfd7fbd8 | ||
|
|
a10489d7df | ||
|
|
7b88420f6a | ||
|
|
cb91f1ad4f | ||
|
|
c6e8c94fd6 | ||
|
|
376a4b4e4a | ||
|
|
d9b6302bf7 | ||
|
|
35319dd6b5 | ||
|
|
9011ae4e1f | ||
|
|
9b9c08ed61 | ||
|
|
317b2dc796 | ||
|
|
0774d18f92 | ||
|
|
6e9dc34008 | ||
|
|
db5c7cf9c7 | ||
|
|
63122c7f6b | ||
|
|
4be395500f | ||
|
|
df7bd8ff2b | ||
|
|
2374a3fd33 | ||
|
|
5811794f31 | ||
|
|
e201258af3 | ||
|
|
76465d0e63 | ||
|
|
a2cf4c8139 | ||
|
|
65d358df58 | ||
|
|
32cb8541f4 | ||
|
|
62cfe09c7b | ||
|
|
8e32deaba5 | ||
|
|
8311b1619c | ||
|
|
fc449bcfbf | ||
|
|
34a8977dd1 | ||
|
|
db04cbfbfc | ||
|
|
c8d3d58940 | ||
|
|
d85cf5f929 | ||
|
|
1e32b355ce | ||
|
|
1193a22658 | ||
|
|
2541f810fa | ||
|
|
7961bb35be | ||
|
|
c599d6eae9 | ||
|
|
075a4cbe0d | ||
|
|
cd845b69d2 | ||
|
|
779f792cbf | ||
|
|
e9c43856d0 | ||
|
|
1e8b87408c | ||
|
|
1428df8567 | ||
|
|
b8acdc3cec | ||
|
|
880e07c07f | ||
|
|
07522d6704 | ||
|
|
58ed7ef93e | ||
|
|
c1694fe706 | ||
|
|
a775bcd62a | ||
|
|
7724cbf9be | ||
|
|
fe65c1f4da | ||
|
|
a91c71a8e4 | ||
|
|
e30142a43b | ||
|
|
c15551c9f5 | ||
|
|
f410a11934 | ||
|
|
3122ccf4e4 | ||
|
|
03527e9d7a | ||
|
|
36be15436a | ||
|
|
04c03b1e72 | ||
|
|
38e60208ef | ||
|
|
fb8de061f3 | ||
|
|
acd26c61ef | ||
|
|
e475f56af5 | ||
|
|
9486ed5d77 | ||
|
|
7ba4df7781 | ||
|
|
db65a5308a | ||
|
|
482e2f8f6f | ||
|
|
cd5b7cdc47 | ||
|
|
e097e58212 | ||
|
|
31892f46cd | ||
|
|
c0dccf89cb | ||
|
|
44ddb40eea | ||
|
|
5b8839b577 | ||
|
|
33bf63d8d2 | ||
|
|
a7cc11ac81 | ||
|
|
25f04030fc | ||
|
|
5d0289f3c9 | ||
|
|
3da9013426 | ||
|
|
1a1e5a9db8 | ||
|
|
87574451a9 | ||
|
|
f8a051ee11 | ||
|
|
a0d778083d | ||
|
|
8de6ddceb0 | ||
|
|
8b3b72dfb4 | ||
|
|
db59679ad4 | ||
|
|
f08ae36f9e | ||
|
|
dc789e63f2 | ||
|
|
30024877fc | ||
|
|
94085bff00 | ||
|
|
aa0f3a1126 | ||
|
|
0ed47c2588 | ||
|
|
770346676c | ||
|
|
8c1fa3fd70 | ||
|
|
217fa40ddb | ||
|
|
a733461845 | ||
|
|
6667fc54c0 | ||
|
|
c0aab96968 | ||
|
|
610032ad1d | ||
|
|
e4d14b4892 | ||
|
|
8509efb48b | ||
|
|
6dcc44b631 | ||
|
|
fce7b6d456 | ||
|
|
ef7c9a166a | ||
|
|
dba8a43495 | ||
|
|
1a3e88c19a | ||
|
|
955c22c8dc | ||
|
|
02f9d0822f | ||
|
|
e133289a7f | ||
|
|
518039f7c3 | ||
|
|
f5094be194 | ||
|
|
fd426191af | ||
|
|
f9d87ea934 | ||
|
|
77ba88b76c | ||
|
|
8615d3adb0 | ||
|
|
7971ea76cd | ||
|
|
5d992a398d | ||
|
|
1aa76925f7 | ||
|
|
4888efe2f7 | ||
|
|
879816ac6d | ||
|
|
6de8820590 | ||
|
|
415f83ace5 | ||
|
|
c9fb80f702 | ||
|
|
9bf63cce25 | ||
|
|
16947e7c39 | ||
|
|
b84c8ab82b | ||
|
|
078c67bfb2 | ||
|
|
81555c8d51 | ||
|
|
d294be8bd4 | ||
|
|
51672599ba | ||
|
|
2ade9b5c1e | ||
|
|
b0503f41ef | ||
|
|
d59e794bdf | ||
|
|
b35fcf9076 | ||
|
|
029827488c | ||
|
|
4ee67fa4e5 | ||
|
|
446a2d4080 | ||
|
|
007a50d0f1 | ||
|
|
c7b0f9783d | ||
|
|
391ca63b7e | ||
|
|
5dfa420135 | ||
|
|
ff3f727c80 | ||
|
|
f7565ef104 | ||
|
|
2f55d8e30c | ||
|
|
3145bafd5e | ||
|
|
7dd45acd29 | ||
|
|
8f561c0319 | ||
|
|
4104db3e7a | ||
|
|
d530997725 | ||
|
|
fb0fefae10 | ||
|
|
8bb79e6f33 | ||
|
|
0e0fba6575 | ||
|
|
5ea0759427 | ||
|
|
7ac378037f | ||
|
|
ff7a00ed3d | ||
|
|
e4915b5bd3 | ||
|
|
38fc34667c | ||
|
|
6873ef8dd6 | ||
|
|
6f0ffa84dc | ||
|
|
8458d198c9 | ||
|
|
c34516e871 | ||
|
|
2b76d3dd70 | ||
|
|
32bf1c30d2 | ||
|
|
586a923be3 | ||
|
|
434712ba17 | ||
|
|
b3863748dc | ||
|
|
35ed313577 | ||
|
|
7e81eca068 | ||
|
|
dbcc0e2c18 | ||
|
|
3bdb71ee40 | ||
|
|
735d63d393 | ||
|
|
31bb46485f | ||
|
|
f53558cb81 | ||
|
|
0c194617a3 | ||
|
|
7c5336fc40 | ||
|
|
2e23bbbb94 | ||
|
|
f0d753e58d | ||
|
|
aad59ea624 | ||
|
|
23ff26a988 | ||
|
|
8c1e69f15a | ||
|
|
01ede7629e | ||
|
|
ff382ec283 | ||
|
|
31355ba7af | ||
|
|
1ac9630964 | ||
|
|
db3477ea17 | ||
|
|
30eea63aae | ||
|
|
825b83dc03 | ||
|
|
f509c06cc6 | ||
|
|
c012aec909 | ||
|
|
d08a180c54 | ||
|
|
db948cb718 | ||
|
|
c03e2c4bde | ||
|
|
e47a743f1e | ||
|
|
73ccb38c11 | ||
|
|
ab4eadf58f | ||
|
|
93cb7955c9 | ||
|
|
8f841dfb59 | ||
|
|
f4d590c703 | ||
|
|
f3419f94c3 | ||
|
|
a53585c96d | ||
|
|
3b1ce22b71 | ||
|
|
9787ac3abc | ||
|
|
131bdc3522 | ||
|
|
daa931c3c4 | ||
|
|
0f8c4675cf | ||
|
|
337d09cd4d | ||
|
|
6b6b679774 | ||
|
|
3ffb118dc7 | ||
|
|
a507bcde90 | ||
|
|
0c27610119 | ||
|
|
f86c9fe0a0 | ||
|
|
3ae535bf31 | ||
|
|
5e2e94d794 | ||
|
|
5bf7361d01 | ||
|
|
0fe8d16740 | ||
|
|
f1a2002072 | ||
|
|
1af1a8ed0b | ||
|
|
0970db8297 | ||
|
|
a36e72147c | ||
|
|
7f4a5a8bab | ||
|
|
442fc1fc16 | ||
|
|
905177f0b6 | ||
|
|
31c35583fb | ||
|
|
764c6fce24 | ||
|
|
86aa459a84 | ||
|
|
32b6250cc3 | ||
|
|
4a6dbf350f | ||
|
|
29968a3eba | ||
|
|
aa092c2195 | ||
|
|
0bf5c4a835 | ||
|
|
3d47609cff | ||
|
|
1425de8726 | ||
|
|
c1aa1888f0 | ||
|
|
58ebbfeed7 | ||
|
|
02f3f1b71a | ||
|
|
f7a6e20ae4 | ||
|
|
378d59e948 | ||
|
|
a07dbe645e | ||
|
|
a596eb71a9 | ||
|
|
c8a1cae18b | ||
|
|
4f1450aaf0 | ||
|
|
8704b44b78 | ||
|
|
0ee11af3c5 | ||
|
|
13fac57b01 | ||
|
|
e0ce58b2a2 | ||
|
|
6f9adfe3e8 | ||
|
|
d47e579994 | ||
|
|
61d8f9a412 | ||
|
|
31185ae365 | ||
|
|
e07c7c864b | ||
|
|
b88bce5220 | ||
|
|
775e110ca3 | ||
|
|
97d548ee3a | ||
|
|
34724b7a8c | ||
|
|
b005f36ac7 | ||
|
|
b0587fcfb3 | ||
|
|
b8eccc6d7a | ||
|
|
34df584366 | ||
|
|
a62b0be831 | ||
|
|
01240fb59d | ||
|
|
7851b9c289 | ||
|
|
15bcaef3a5 | ||
|
|
3c831fc5e6 | ||
|
|
26efbc1389 | ||
|
|
929367c9ce | ||
|
|
2f3e0b419d | ||
|
|
ef41ef85eb | ||
|
|
d9b91778b1 | ||
|
|
2f6bae5e5e | ||
|
|
40b351d76d | ||
|
|
8bc914a70f | ||
|
|
83bfb721e7 | ||
|
|
64e27152c6 | ||
|
|
f4a5ca9e84 | ||
|
|
789f37611b | ||
|
|
931cc9ddd7 | ||
|
|
65846dcf7d | ||
|
|
aa1b690b11 | ||
|
|
e5561dbd20 | ||
|
|
e68b403808 | ||
|
|
85d91de2e7 | ||
|
|
5f8a8d4c5b | ||
|
|
d2534748eb | ||
|
|
a548c5d592 | ||
|
|
08ab38280a | ||
|
|
a732fcc604 | ||
|
|
7e19b58976 | ||
|
|
e56fb7cfc8 | ||
|
|
aabfe16021 | ||
|
|
d572977922 | ||
|
|
6e0eee3509 | ||
|
|
87186058ab | ||
|
|
fbafc9f3f6 | ||
|
|
4f944ea72e | ||
|
|
61d2da2259 | ||
|
|
0661486676 | ||
|
|
07a4244c05 | ||
|
|
03f92ed6e4 | ||
|
|
469ec63fec | ||
|
|
50caca7bd3 | ||
|
|
81caa3321f | ||
|
|
3c3fce96e7 | ||
|
|
e757f7af6b | ||
|
|
7eae5b0ffb | ||
|
|
d82b91ad4a | ||
|
|
82b41ae86c | ||
|
|
68396f8a83 | ||
|
|
ebc714b73f | ||
|
|
e4fd630f37 | ||
|
|
a52b723414 | ||
|
|
99fc7162e2 | ||
|
|
8953936d3d | ||
|
|
a1110af6d5 | ||
|
|
b8951944ab | ||
|
|
16aa4ae3a9 | ||
|
|
d585f6232e | ||
|
|
b1ca464c58 | ||
|
|
0b8e7e9cae | ||
|
|
5374342d98 | ||
|
|
0971a15c40 | ||
|
|
78c59bb41c | ||
|
|
045d861ad1 | ||
|
|
269565d317 | ||
|
|
f0403c80bc | ||
|
|
a21da9417c | ||
|
|
fa0b5210fe | ||
|
|
9f84a5cae0 | ||
|
|
4eb867436a | ||
|
|
323e088378 | ||
|
|
ab9dfc75f0 | ||
|
|
b969857583 | ||
|
|
a056a28423 | ||
|
|
435a7d0adb | ||
|
|
a1759a46de | ||
|
|
1cf11b91c1 | ||
|
|
0412629871 | ||
|
|
c11a37ca61 | ||
|
|
fb95ba278e | ||
|
|
aa00a951b6 | ||
|
|
7fd7dc3794 | ||
|
|
ed234a1ad1 | ||
|
|
d7e0abc55c | ||
|
|
e8c6d79a89 | ||
|
|
d94feaa8db | ||
|
|
6d0697cf2f | ||
|
|
5d6ec19ca0 | ||
|
|
956b7fc480 | ||
|
|
2946b307c2 | ||
|
|
c22e0cb7e2 | ||
|
|
01a2cd1410 | ||
|
|
7f4b0a33f6 | ||
|
|
35963bbc9e | ||
|
|
abf683f275 | ||
|
|
acaf69ca1b | ||
|
|
7a4c189249 | ||
|
|
7227c7b368 | ||
|
|
2012b09845 | ||
|
|
abd66f50db | ||
|
|
610e792394 | ||
|
|
7fa534d70d | ||
|
|
c8091ac111 | ||
|
|
86beaeb20f | ||
|
|
6b39d0a5fd | ||
|
|
f69c75322f | ||
|
|
f9b04ae38e | ||
|
|
53adfa4497 | ||
|
|
18a59dd7db | ||
|
|
6b8c620bbb | ||
|
|
56328108ca | ||
|
|
f78b8dd153 |
@@ -28,8 +28,6 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
||||||
"jsx-a11y/media-has-caption": "off",
|
"jsx-a11y/media-has-caption": "off",
|
||||||
// We should use the js-sdk logger, never console directly.
|
|
||||||
"no-console": ["error"],
|
|
||||||
"react/display-name": "error",
|
"react/display-name": "error",
|
||||||
// Encourage proper usage of Promises:
|
// Encourage proper usage of Promises:
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
@@ -44,8 +42,17 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
// To encourage good usage of RxJS:
|
// To encourage good usage of RxJS:
|
||||||
"rxjs/no-exposed-subjects": "error",
|
"rxjs/no-exposed-subjects": "error",
|
||||||
"rxjs/finnish": "error",
|
"rxjs/finnish": ["error", { names: { "^this$": false } }],
|
||||||
},
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["src/*/**"],
|
||||||
|
rules: {
|
||||||
|
// In application code we should use the js-sdk logger, never console directly.
|
||||||
|
"no-console": ["error"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: "detect",
|
version: "detect",
|
||||||
|
|||||||
4
.github/workflows/blocked.yaml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: Prevent blocked
|
name: Prevent blocked
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, labeled, unlabeled]
|
types: [opened, labeled, unlabeled, synchronize]
|
||||||
jobs:
|
jobs:
|
||||||
prevent-blocked:
|
prevent-blocked:
|
||||||
name: Prevent blocked
|
name: Prevent blocked
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: Add notice
|
- name: Add notice
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
10
.github/workflows/build-and-publish-docker.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check it out
|
- name: Check it out
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Log in to container registry
|
- name: Log in to container registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: ${{ inputs.docker_tags}}
|
tags: ${{ inputs.docker_tags}}
|
||||||
@@ -50,10 +50,10 @@ jobs:
|
|||||||
org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial
|
org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
23
.github/workflows/build-element-call.yaml
vendored
@@ -9,6 +9,11 @@ on:
|
|||||||
type: string # This would ideally be a `choice` type, but that isn't supported yet
|
type: string # This would ideally be a `choice` type, but that isn't supported yet
|
||||||
description: The package type to be built. Must be one of 'full' or 'embedded'
|
description: The package type to be built. Must be one of 'full' or 'embedded'
|
||||||
required: true
|
required: true
|
||||||
|
build_mode:
|
||||||
|
type: string # This would ideally be a `choice` type, but that isn't supported yet
|
||||||
|
description: The build mode for vite. Must be either 'development' or 'production'
|
||||||
|
required: false
|
||||||
|
default: production
|
||||||
secrets:
|
secrets:
|
||||||
SENTRY_ORG:
|
SENTRY_ORG:
|
||||||
required: true
|
required: true
|
||||||
@@ -27,7 +32,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
@@ -37,20 +42,8 @@ jobs:
|
|||||||
node-version-file: ".node-version"
|
node-version-file: ".node-version"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: "yarn install --immutable"
|
run: "yarn install --immutable"
|
||||||
- name: Build full version
|
- name: Build Element Call
|
||||||
if: ${{ inputs.package == 'full' }}
|
run: ${{ format('yarn run build:{0}:{1}', inputs.package, inputs.build_mode) }}
|
||||||
run: "yarn run build:full"
|
|
||||||
env:
|
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
|
||||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
|
|
||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
|
||||||
- name: Build embedded
|
|
||||||
if: ${{ inputs.package == 'embedded' }}
|
|
||||||
run: "yarn run build:embedded"
|
|
||||||
env:
|
env:
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
|||||||
2
.github/workflows/build.yaml
vendored
@@ -14,6 +14,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package: full
|
package: full
|
||||||
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
||||||
|
build_mode: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'development build') && 'development' || 'production' }}
|
||||||
secrets:
|
secrets:
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
@@ -61,6 +62,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
package: embedded
|
package: embedded
|
||||||
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
||||||
|
build_mode: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'development build') && 'development' || 'production' }}
|
||||||
secrets:
|
secrets:
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
|||||||
2
.github/workflows/changelog-label.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
- uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||||
with:
|
with:
|
||||||
REQUIRED_LABELS_ANY: "PR-Bug-Fix,PR-Documentation,PR-Task,PR-Feature,PR-Improvement,PR-Developer-Experience,dependencies"
|
REQUIRED_LABELS_ANY: "PR-Bug-Fix,PR-Documentation,PR-Task,PR-Feature,PR-Improvement,PR-Developer-Experience,dependencies,PR-Breaking-Change"
|
||||||
REQUIRED_LABELS_ANY_DESCRIPTION: "Select at least one 'PR-' label"
|
REQUIRED_LABELS_ANY_DESCRIPTION: "Select at least one 'PR-' label"
|
||||||
BANNED_LABELS: "banned"
|
BANNED_LABELS: "banned"
|
||||||
|
|||||||
2
.github/workflows/lint.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
|
|||||||
16
.github/workflows/publish-embedded-packages.yaml
vendored
@@ -44,6 +44,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
echo "TAG=latest" >> "$GITHUB_OUTPUT"
|
echo "TAG=latest" >> "$GITHUB_OUTPUT"
|
||||||
|
elif [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\-rc\.[0-9]+$ ]]; then
|
||||||
|
echo "TAG=rc" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "TAG=other" >> "$GITHUB_OUTPUT"
|
echo "TAG=other" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
@@ -83,7 +85,7 @@ jobs:
|
|||||||
run: find ${{ env.FILENAME_PREFIX }} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${{ env.FILENAME_PREFIX }}.sha256
|
run: find ${{ env.FILENAME_PREFIX }} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${{ env.FILENAME_PREFIX }}.sha256
|
||||||
- name: Upload
|
- name: Upload
|
||||||
if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }}
|
if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }}
|
||||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
${{ env.FILENAME_PREFIX }}.tar.gz
|
${{ env.FILENAME_PREFIX }}.tar.gz
|
||||||
@@ -101,7 +103,7 @@ jobs:
|
|||||||
id-token: write # required for the provenance flag on npm publish
|
id-token: write # required for the provenance flag on npm publish
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: 📥 Download built element-call artifact
|
- name: 📥 Download built element-call artifact
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
@@ -142,7 +144,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: 📥 Download built element-call artifact
|
- name: 📥 Download built element-call artifact
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
@@ -163,6 +165,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ "${{ needs.versioning.outputs.TAG }}" == "latest" ]]; then
|
if [[ "${{ needs.versioning.outputs.TAG }}" == "latest" ]]; then
|
||||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
||||||
|
elif [[ "${{ needs.versioning.outputs.TAG }}" == "rc" ]]; then
|
||||||
|
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}-SNAPSHOT" >> "$GITHUB_ENV"
|
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}-SNAPSHOT" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
@@ -195,7 +199,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
with:
|
with:
|
||||||
path: element-call
|
path: element-call
|
||||||
|
|
||||||
@@ -208,7 +212,7 @@ jobs:
|
|||||||
path: element-call/embedded/ios/Sources/dist
|
path: element-call/embedded/ios/Sources/dist
|
||||||
|
|
||||||
- name: Checkout element-call-swift
|
- name: Checkout element-call-swift
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-call-swift
|
repository: element-hq/element-call-swift
|
||||||
path: element-call-swift
|
path: element-call-swift
|
||||||
@@ -260,7 +264,7 @@ jobs:
|
|||||||
echo "iOS: ${{ needs.publish_ios.outputs.ARTIFACT_VERSION }}"
|
echo "iOS: ${{ needs.publish_ios.outputs.ARTIFACT_VERSION }}"
|
||||||
- name: Add release notes
|
- name: Add release notes
|
||||||
if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }}
|
if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }}
|
||||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2
|
||||||
with:
|
with:
|
||||||
append_body: true
|
append_body: true
|
||||||
body: |
|
body: |
|
||||||
|
|||||||
4
.github/workflows/publish.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Create Checksum
|
- name: Create Checksum
|
||||||
run: find ${{ env.FILENAME_PREFIX }} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${{ env.FILENAME_PREFIX }}.sha256
|
run: find ${{ env.FILENAME_PREFIX }} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${{ env.FILENAME_PREFIX }}.sha256
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
${{ env.FILENAME_PREFIX }}.tar.gz
|
${{ env.FILENAME_PREFIX }}.tar.gz
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Add release note
|
- name: Add release note
|
||||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2
|
||||||
with:
|
with:
|
||||||
append_body: true
|
append_body: true
|
||||||
body: |
|
body: |
|
||||||
|
|||||||
15
.github/workflows/test.yaml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Vitest
|
- name: Vitest
|
||||||
run: "yarn run test:coverage"
|
run: "yarn run test:coverage"
|
||||||
- name: Upload to codecov
|
- name: Upload to codecov
|
||||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -30,10 +30,10 @@ jobs:
|
|||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
playwright:
|
playwright:
|
||||||
name: Run end-to-end tests
|
name: Run end-to-end tests
|
||||||
timeout-minutes: 10
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
@@ -46,11 +46,12 @@ jobs:
|
|||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Run backend components
|
- name: Run backend components
|
||||||
run: |
|
run: |
|
||||||
docker compose -f playwright-backend-docker-compose.yml up -d
|
docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml pull
|
||||||
|
docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up -d
|
||||||
docker ps
|
docker ps
|
||||||
- name: Copy config file
|
|
||||||
run: cp config/config.devenv.json public/config.json
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
|
env:
|
||||||
|
USE_DOCKER: 1
|
||||||
run: yarn playwright test
|
run: yarn playwright test
|
||||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|||||||
2
.github/workflows/translations-download.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the code
|
- name: Checkout the code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
|
|||||||
2
.github/workflows/translations-upload.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the code
|
- name: Checkout the code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||||
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: localazy/upload@27e6b5c0fddf4551596b42226b1c24124335d24a # v1
|
uses: localazy/upload@27e6b5c0fddf4551596b42226b1c24124335d24a # v1
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -8,7 +8,9 @@ dist-ssr
|
|||||||
.idea/
|
.idea/
|
||||||
public/config.json
|
public/config.json
|
||||||
backend/synapse_tmp/*
|
backend/synapse_tmp/*
|
||||||
|
backend/synapse_tmp_othersite/*
|
||||||
/coverage
|
/coverage
|
||||||
|
config.json
|
||||||
|
|
||||||
# Yarn
|
# Yarn
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
@@ -27,4 +29,4 @@ yarn-error.log
|
|||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ COPY ./dist /dist
|
|||||||
WORKDIR /dist/assets
|
WORKDIR /dist/assets
|
||||||
RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json
|
RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json
|
||||||
|
|
||||||
FROM nginxinc/nginx-unprivileged:alpine
|
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||||
|
|
||||||
COPY --from=builder ./dist /app
|
COPY --from=builder ./dist /app
|
||||||
|
|
||||||
|
|||||||
29
README.md
@@ -143,7 +143,8 @@ via the `org.matrix.msc4143.rtc_foci` key, e.g.:
|
|||||||
where the format for MatrixRTC using LiveKit backend is defined in
|
where the format for MatrixRTC using LiveKit backend is defined in
|
||||||
[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
|
[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
|
||||||
In the example above Matrix clients do discover a focus of type `livekit` which
|
In the example above Matrix clients do discover a focus of type `livekit` which
|
||||||
points them to a Matrix LiveKit JWT Auth Service via `livekit_service_url`.
|
points them to a [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||||
|
via `livekit_service_url`.
|
||||||
|
|
||||||
### Backend Selection
|
### Backend Selection
|
||||||
|
|
||||||
@@ -154,9 +155,9 @@ points them to a Matrix LiveKit JWT Auth Service via `livekit_service_url`.
|
|||||||
the **first participant who joined the call** defines via the `foci_preferred`
|
the **first participant who joined the call** defines via the `foci_preferred`
|
||||||
key in their `org.matrix.msc3401.call.member` which actual MatrixRTC backend
|
key in their `org.matrix.msc3401.call.member` which actual MatrixRTC backend
|
||||||
will be used for this call.
|
will be used for this call.
|
||||||
- During the actual call join flow, the **LiveKit JWT Auth Service** provides
|
- During the actual call join flow, the **[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)**
|
||||||
the client with the **LiveKit SFU WebSocket URL** and an **access JWT token**
|
provides the client with the **LiveKit SFU WebSocket URL** and an
|
||||||
in order to exchange media via WebRTC.
|
**access JWT token** in order to exchange media via WebRTC.
|
||||||
|
|
||||||
The example below illustrates how backend selection works across **Matrix
|
The example below illustrates how backend selection works across **Matrix
|
||||||
federation**, using the setup from sites A, B, and C. It demonstrates backend
|
federation**, using the setup from sites A, B, and C. It demonstrates backend
|
||||||
@@ -205,22 +206,22 @@ See also:
|
|||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
A docker compose file `dev-backend-docker-compose.yml` is provided to start the
|
A docker compose file `dev-backend-docker-compose.yml` is provided to start the
|
||||||
whole stack of components which is required for a local development environment:
|
whole stack of components which is required for a local development environment
|
||||||
|
including federation:
|
||||||
|
|
||||||
- Minimum Synapse Setup (servername: `synapse.m.localhost`)
|
- Minimum Synapse Setup (servernameis: `synapse.m.localhost`, `synapse.othersite.m.localhost`)
|
||||||
- LiveKit Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
|
- MatrixRTC Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
|
||||||
- Minimum LiveKit SFU Setup using dev defaults for config
|
- Minimum LiveKit SFU setup using dev defaults for config
|
||||||
- Redis db for completeness
|
|
||||||
- Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS)
|
- Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS)
|
||||||
- Hostnames: `m.localhost`, `*.m.localhost`
|
- Hostnames: `m.localhost`, `*.m.localhost`, `*.othersite.m.localhost`
|
||||||
- Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted
|
- Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted
|
||||||
certificates
|
certificates
|
||||||
- Minimum TLS reverse proxy for
|
- Minimum TLS reverse proxy for
|
||||||
- Synapse homeserver: `synapse.m.localhost`
|
- Synapse homeserver: `synapse.m.localhost` and `synapse.othersite.m.localhost`
|
||||||
- MatrixRTC backend: `matrix-rtc.m.localhost`
|
- MatrixRTC backend: `matrix-rtc.m.localhost` and `matrix-rtc.othersite.m.localhost`
|
||||||
- Local Element Call development `call.m.localhost` via `yarn dev --host `
|
- Local Element Call development `call.m.localhost` via `yarn dev --host `
|
||||||
- Element Web `app.m.localhost`
|
- Element Web `app.m.localhost` and `app.othersite.m.localhost`
|
||||||
- Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT
|
- Note certificates will expire on Thr, 20 September 2035 14:27:35 CEST
|
||||||
|
|
||||||
These use a test 'secret' published in this repository, so this must be used
|
These use a test 'secret' published in this repository, so this must be used
|
||||||
only for local development and **_never be exposed to the public Internet._**
|
only for local development and **_never be exposed to the public Internet._**
|
||||||
|
|||||||
64
backend/dev_homeserver-othersite.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
server_name: "synapse.othersite.m.localhost"
|
||||||
|
public_baseurl: https://synapse.othersite.m.localhost/
|
||||||
|
|
||||||
|
pid_file: /data/homeserver.pid
|
||||||
|
|
||||||
|
listeners:
|
||||||
|
- port: 18008
|
||||||
|
tls: false
|
||||||
|
type: http
|
||||||
|
x_forwarded: true
|
||||||
|
resources:
|
||||||
|
- names: [client, federation, openid]
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
database:
|
||||||
|
name: sqlite3
|
||||||
|
args:
|
||||||
|
database: /data/homeserver.db
|
||||||
|
|
||||||
|
media_store_path: /data/media_store
|
||||||
|
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||||
|
|
||||||
|
# Due to custom TLS certificate with domains
|
||||||
|
# - m.localhost, localhost
|
||||||
|
# - *.m.localhost
|
||||||
|
# - *.othersite.m.localhost
|
||||||
|
# we disable certificate verification to allow for federation
|
||||||
|
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||||
|
federation_verify_certificates: false
|
||||||
|
ip_range_blacklist: []
|
||||||
|
trusted_key_servers:
|
||||||
|
- server_name: "synapse.m.localhost"
|
||||||
|
accept_keys_insecurely: true
|
||||||
|
|
||||||
|
experimental_features:
|
||||||
|
# MSC3266: Room summary API. Used for knocking over federation
|
||||||
|
msc3266_enabled: true
|
||||||
|
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||||
|
# correctly track the state of the room.
|
||||||
|
msc4222_enabled: true
|
||||||
|
|
||||||
|
# The maximum allowed duration by which sent events can be delayed, as
|
||||||
|
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||||
|
# duration (null), which disallows sending delayed events.
|
||||||
|
max_event_delay_duration: 24h
|
||||||
|
|
||||||
|
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||||
|
enable_registration: true
|
||||||
|
enable_registration_without_verification: true
|
||||||
|
|
||||||
|
report_stats: false
|
||||||
|
serve_server_wellknown: true
|
||||||
|
|
||||||
|
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||||
|
#
|
||||||
|
# Each ratelimiting configuration is made of two parameters:
|
||||||
|
# - per_second: number of requests a client can send per second.
|
||||||
|
# - burst_count: number of requests a client can send before being throttled.
|
||||||
|
|
||||||
|
rc_message:
|
||||||
|
# This needs to match at least the heart-beat frequency plus a bit of headroom
|
||||||
|
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
||||||
|
per_second: 0.5
|
||||||
|
burst_count: 30
|
||||||
@@ -19,8 +19,18 @@ database:
|
|||||||
|
|
||||||
media_store_path: /data/media_store
|
media_store_path: /data/media_store
|
||||||
signing_key_path: "/data/SERVERNAME.signing.key"
|
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||||
|
|
||||||
|
# Due to custom TLS certificate with domains
|
||||||
|
# - m.localhost, localhost
|
||||||
|
# - *.m.localhost
|
||||||
|
# - *.othersite.m.localhost
|
||||||
|
# we disable certificate verification to allow for federation.
|
||||||
|
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||||
|
federation_verify_certificates: false
|
||||||
|
ip_range_blacklist: []
|
||||||
trusted_key_servers:
|
trusted_key_servers:
|
||||||
- server_name: "matrix.org"
|
- server_name: "synapse.othersite.m.localhost"
|
||||||
|
accept_keys_insecurely: true
|
||||||
|
|
||||||
experimental_features:
|
experimental_features:
|
||||||
# MSC3266: Room summary API. Used for knocking over federation
|
# MSC3266: Room summary API. Used for knocking over federation
|
||||||
@@ -28,12 +38,21 @@ experimental_features:
|
|||||||
# MSC4222 needed for syncv2 state_after. This allow clients to
|
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||||
# correctly track the state of the room.
|
# correctly track the state of the room.
|
||||||
msc4222_enabled: true
|
msc4222_enabled: true
|
||||||
|
# sticky events for matrixRTC user state
|
||||||
|
msc4354_enabled: true
|
||||||
|
|
||||||
# The maximum allowed duration by which sent events can be delayed, as
|
# The maximum allowed duration by which sent events can be delayed, as
|
||||||
# per MSC4140. Must be a positive value if set. Defaults to no
|
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||||
# duration (null), which disallows sending delayed events.
|
# duration (null), which disallows sending delayed events.
|
||||||
max_event_delay_duration: 24h
|
max_event_delay_duration: 24h
|
||||||
|
|
||||||
|
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||||
|
enable_registration: true
|
||||||
|
enable_registration_without_verification: true
|
||||||
|
|
||||||
|
report_stats: false
|
||||||
|
serve_server_wellknown: true
|
||||||
|
|
||||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||||
#
|
#
|
||||||
# Each ratelimiting configuration is made of two parameters:
|
# Each ratelimiting configuration is made of two parameters:
|
||||||
@@ -45,10 +64,3 @@ rc_message:
|
|||||||
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
||||||
per_second: 0.5
|
per_second: 0.5
|
||||||
burst_count: 30
|
burst_count: 30
|
||||||
|
|
||||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
|
||||||
enable_registration: true
|
|
||||||
enable_registration_without_verification: true
|
|
||||||
|
|
||||||
report_stats: false
|
|
||||||
serve_server_wellknown: true
|
|
||||||
|
|||||||
20
backend/dev_livekit-othersite.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
port: 17880
|
||||||
|
bind_addresses:
|
||||||
|
- "0.0.0.0"
|
||||||
|
rtc:
|
||||||
|
tcp_port: 17881
|
||||||
|
port_range_start: 50300
|
||||||
|
port_range_end: 50400
|
||||||
|
use_external_ip: false
|
||||||
|
turn:
|
||||||
|
enabled: false
|
||||||
|
domain: localhost
|
||||||
|
cert_file: ""
|
||||||
|
key_file: ""
|
||||||
|
tls_port: 5349
|
||||||
|
udp_port: 443
|
||||||
|
external_tls: true
|
||||||
|
keys:
|
||||||
|
devkey: secret
|
||||||
|
room:
|
||||||
|
auto_create: false
|
||||||
@@ -6,11 +6,6 @@ rtc:
|
|||||||
port_range_start: 50100
|
port_range_start: 50100
|
||||||
port_range_end: 50200
|
port_range_end: 50200
|
||||||
use_external_ip: false
|
use_external_ip: false
|
||||||
#redis:
|
|
||||||
# address: redis:6379
|
|
||||||
# username: ""
|
|
||||||
# password: ""
|
|
||||||
# db: 0
|
|
||||||
turn:
|
turn:
|
||||||
enabled: false
|
enabled: false
|
||||||
domain: localhost
|
domain: localhost
|
||||||
@@ -21,3 +16,5 @@ turn:
|
|||||||
external_tls: true
|
external_tls: true
|
||||||
keys:
|
keys:
|
||||||
devkey: secret
|
devkey: secret
|
||||||
|
room:
|
||||||
|
auto_create: false
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Synapse reverse proxy including .well-known/matrix/client
|
# Synapse reverse proxy including .well-known/matrix/client
|
||||||
|
# domain synapse.m.localhost
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
@@ -26,14 +27,53 @@ server {
|
|||||||
# This is also required for development environment.
|
# This is also required for development environment.
|
||||||
# Reason: the lk-jwt-service uses the federation API for the openid token
|
# Reason: the lk-jwt-service uses the federation API for the openid token
|
||||||
# verification, which requires TLS
|
# verification, which requires TLS
|
||||||
location / {
|
location ~ ^(/_matrix|/_synapse/client) {
|
||||||
proxy_pass "http://homeserver:8008";
|
proxy_pass "http://homeserver:8008";
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Synapse reverse proxy including .well-known/matrix/client
|
||||||
|
# domain synapse.othersite.m.localhost
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
listen 443 ssl;
|
||||||
|
listen 8448 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
listen [::]:8448 ssl;
|
||||||
|
server_name synapse.othersite.m.localhost;
|
||||||
|
ssl_certificate /root/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /root/ssl/key.pem;
|
||||||
|
|
||||||
|
# well-known config adding rtc_foci backend
|
||||||
|
# Note well-known is currently not effective due to:
|
||||||
|
# https://spec.matrix.org/v1.12/client-server-api/#well-known-uri the spec
|
||||||
|
# says it must be at https://$server_name/... (implied port 443) Hence, we
|
||||||
|
# currently rely for local development environment on deprecated config.json
|
||||||
|
# setting for livekit_service_url
|
||||||
|
location /.well-known/matrix/client {
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
return 200 '{"m.homeserver": {"base_url": "https://synapse.othersite.m.localhost"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrix-rtc.othersite.m.localhost/livekit/jwt"}]}';
|
||||||
|
default_type application/json;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse proxy for Matrix Synapse Homeserver
|
||||||
|
# This is also required for development environment.
|
||||||
|
# Reason: the lk-jwt-service uses the federation API for the openid token
|
||||||
|
# verification, which requires TLS
|
||||||
|
location ~ ^(/_matrix|/_synapse/client) {
|
||||||
|
proxy_pass "http://homeserver-1:18008";
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $host;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
@@ -41,19 +81,23 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# MatrixRTC reverse proxy
|
# MatrixRTC reverse proxy
|
||||||
|
# domain matrix-rtc.m.localhost
|
||||||
# - MatrixRTC Authorization Service
|
# - MatrixRTC Authorization Service
|
||||||
# - LiveKit SFU websocket signaling connection
|
# - LiveKit SFU websocket signaling connection
|
||||||
|
upstream jwt-auth-services {
|
||||||
|
server auth-server:6080;
|
||||||
|
server host.docker.internal:6080;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
listen 8448 ssl;
|
|
||||||
listen [::]:8448 ssl;
|
|
||||||
server_name matrix-rtc.m.localhost;
|
server_name matrix-rtc.m.localhost;
|
||||||
ssl_certificate /root/ssl/cert.pem;
|
ssl_certificate /root/ssl/cert.pem;
|
||||||
ssl_certificate_key /root/ssl/key.pem;
|
ssl_certificate_key /root/ssl/key.pem;
|
||||||
|
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
|
||||||
location ^~ /livekit/jwt/ {
|
location ^~ /livekit/jwt/ {
|
||||||
|
|
||||||
@@ -62,8 +106,9 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# JWT Service running at port 8080
|
# JWT Service running at port 6080
|
||||||
proxy_pass http://auth-server:8080/;
|
proxy_pass http://jwt-auth-services/;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /livekit/sfu/ {
|
location ^~ /livekit/sfu/ {
|
||||||
@@ -88,6 +133,54 @@ server {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MatrixRTC reverse proxy
|
||||||
|
# domain matrix-rtc.othersite.m.localhost
|
||||||
|
# - MatrixRTC Authorization Service
|
||||||
|
# - LiveKit SFU websocket signaling connection
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name matrix-rtc.othersite.m.localhost;
|
||||||
|
ssl_certificate /root/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /root/ssl/key.pem;
|
||||||
|
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
|
||||||
|
location ^~ /livekit/jwt/ {
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# JWT Service running at port 16080
|
||||||
|
proxy_pass http://auth-service-1:16080/;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /livekit/sfu/ {
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_send_timeout 120;
|
||||||
|
proxy_read_timeout 120;
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
proxy_set_header Accept-Encoding gzip;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# LiveKit SFU websocket connection running at port 17880
|
||||||
|
proxy_pass http://livekit-sfu-1:17880/;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
# Convenience reverse proxy for the call.m.localhost domain to yarn dev --host
|
# Convenience reverse proxy for the call.m.localhost domain to yarn dev --host
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@@ -153,3 +246,36 @@ server {
|
|||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Convenience reverse proxy app.othersite.m.localhost for element web
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name app.othersite.m.localhost;
|
||||||
|
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name app.othersite.m.localhost;
|
||||||
|
ssl_certificate /root/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /root/ssl/key.pem;
|
||||||
|
|
||||||
|
|
||||||
|
location ^~ / {
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_pass http://element-web-1:18081;
|
||||||
|
proxy_ssl_verify off;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIDGjCCAgKgAwIBAgIUGdiFHhH4KL2pqBjMQHQ+PVIkSV8wDQYJKoZIhvcNAQEL
|
MIIDGjCCAgKgAwIBAgIUbSbx+1UGptOTGefqEn7Zh3yoChIwDQYJKoZIhvcNAQEL
|
||||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy
|
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA5MjIxMjI3
|
||||||
MDJaFw0zNTA1MDMxMDMyMDJaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
|
MzVaFw0zNTA5MjAxMjI3MzVaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
|
||||||
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDA2y0hjmNn1vRsVSdy
|
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHODfkrFsOkqCnXnTb
|
||||||
8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH9jdT
|
QWz3LkUtNCoVvM7wFouirRnITJYG+lFwF+zNl89Eaq+uUN4bwd8ml1ZuR9p+1azC
|
||||||
tG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeFw9fw
|
SlklD5adhCR/ErknfUWamQEf6amSs3p0NnqnhXbnDEEbQOwNaPU/aGc6aw0+I9O6
|
||||||
eRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZNWnie
|
NQ/H830GlVuKd24Bfv0mx6Imo0Hi9jxKYhqFh80nmltk2uyXefaJxuo1jXBhwLyC
|
||||||
Ui7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAveL9K
|
DW8RVj55QvkZyBUzid8yslxrlo0LHKCCjZflwJJv5f+jaubkH5c0qxVaoR4+Liyt
|
||||||
FGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BLPiQU
|
X/4viIwt3Mhj04ppudTvt973mTbjRG5haCz9y7OkT1mMWhc0xrdMFX+gjPERYS2H
|
||||||
KGKrAgMBAAGjUDBOMB0GA1UdDgQWBBQJqBjMu61c1p24txw/y+kv3D+V6DAfBgNV
|
Ru/RAgMBAAGjUDBOMB0GA1UdDgQWBBTXNfLAKVayGQda/JZLPszrpz6LVzAfBgNV
|
||||||
HSMEGDAWgBQJqBjMu61c1p24txw/y+kv3D+V6DAMBgNVHRMEBTADAQH/MA0GCSqG
|
HSMEGDAWgBTXNfLAKVayGQda/JZLPszrpz6LVzAMBgNVHRMEBTADAQH/MA0GCSqG
|
||||||
SIb3DQEBCwUAA4IBAQB8m2YfFGLugNt5vAAOvNxVqDA8c72yCVYr3CBCpmTIEY5Z
|
SIb3DQEBCwUAA4IBAQCvGfyopHHgZB+horGH6i/Xg41V+r4d0o092F1Lfr4vh86e
|
||||||
d3qVGhG9//ux6+J8ntkSwd9nV5GJyYXHukCG1VavnAWolWdNF/WAllf0jhLuz7kD
|
XMakRw92vsyk/iWOnLPNPcpVWzPcvINaCs/bahgnGSOAnrA4jjcXqymyGIy/6xc5
|
||||||
/cJnuI1By4tBsBmSz851i6HJ4t5k99Be+6GQVzi0e7zzfxTHZE4xP2J6Ox8QbPsP
|
1EeZAxehiL9E5q4LQ841HDX0gps4ZzUO1BRYQcjG9Rdts83JO2ekkfHkZdNj2eQr
|
||||||
n0m76nIp/WbWaJqzvIIjJhmUUPPv+4wN+eOArgjiGLzptM2qTtGZtd0c9nS5gvep
|
KOrr92Na1/w+EQdo/T9Rs2ES623xKEOxPqb8d/rx5Z4DdeuGx1u+3AfS76Lpo4ni
|
||||||
+mEbSUN9zkhAroZf80wf+hEvy+fJ94VbZ9QjTzTg7odZLrsXGIe8DaG63EYRQ25b
|
EJ0g1ImqdSUtiOLzeCQh6pqqb+vuFbxAyeyYSAJ49847EtFBvZCmWmPL2JICg9uq
|
||||||
W5iYBAreln5fGSt7qHsGfqwZibTEk/Lx3dydO1Kg
|
7rKW/qDfEK9GUs0GWCs3+mJkNvOOxBwtMuQrL7ZF
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA2y0hjmNn1vRs
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHODfkrFsOkqCn
|
||||||
VSdy8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH
|
XnTbQWz3LkUtNCoVvM7wFouirRnITJYG+lFwF+zNl89Eaq+uUN4bwd8ml1ZuR9p+
|
||||||
9jdTtG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeF
|
1azCSlklD5adhCR/ErknfUWamQEf6amSs3p0NnqnhXbnDEEbQOwNaPU/aGc6aw0+
|
||||||
w9fweRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZN
|
I9O6NQ/H830GlVuKd24Bfv0mx6Imo0Hi9jxKYhqFh80nmltk2uyXefaJxuo1jXBh
|
||||||
WnieUi7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAv
|
wLyCDW8RVj55QvkZyBUzid8yslxrlo0LHKCCjZflwJJv5f+jaubkH5c0qxVaoR4+
|
||||||
eL9KFGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BL
|
LiytX/4viIwt3Mhj04ppudTvt973mTbjRG5haCz9y7OkT1mMWhc0xrdMFX+gjPER
|
||||||
PiQUKGKrAgMBAAECggEAAPX2kxi5AQ7ul82SzT1KgpSXyDHLdYaUyAoYnaX9RO+B
|
YS2HRu/RAgMBAAECggEABhB9CxYAE5p9D3s9nWsJcSDUdELRQSYlOoPFLmeMkF9c
|
||||||
8ylmpyeqygs4+KQS4EMJm9jpo85Oy37bIKdG3kljU6wQcKlL5Y+ZUOo1nzpV6fid
|
dcvq7LmduMh1Q8TnoivOBxRIwbj7pZHEYfYJM0TmH82wrQzXu5KLVltm4gTkVt9b
|
||||||
hGVs6ts8VXw8KshKQ9AyccZ8L/pirUfgOffgTwfjY7/90zceAL/s98GuZWc62nkX
|
DR8vjBgYdb8HVpM17Cl2xhW62XpJIiseFRUsHc/9sf2Egc3MIpPuIleGR0budbSW
|
||||||
55joQv/OikqYfAGP/U6Bp2Zyf23DwJB09Z3B6NnZj/ZyAbDrDEHuA15LhCOcCczp
|
ybBkqEokTYTSiAztcu3G+VN0U9MsJgLMa8HApya7M48ojdrhzngVHZRUOXul9o7u
|
||||||
IU/mFEywBPHT9Tg4w4Beq78PeAETvku2UalYRLhP3RLlXr2oEbwUtINRVt2QjZ85
|
zYJWSxPHIIYp5C4pYQBAx8OttThwKK1A9lwbQ2EJx0KnTbBC6O5Gna/jENpGd1h2
|
||||||
Esps4uCqL/mgQluIebtudD9HL/YMlNPXue1mDXFxJQKBgQDgZZY4yJBcf488T1V6
|
rzK/9MONtsjln7IejP+4mDlNupS6SF3zzHPBHjqKAQKBgQDtXUIKPiVTFS45yWtK
|
||||||
HNm06b/LvVGj253pKgw14hpY1xQu3Ymgzv1GEqzhSYdzxhpmj0tMUNHxAp+YdGQu
|
XD62s3j8jfIi+22b/C30fCPtppn0cm/0zY+vovgWVUBnQXkExafRthZCuxnE8ry7
|
||||||
SZ0wcPKhw0aYVkIjDRYDC3Wn5GJhyIEYHGYMo/n4l49UzHRBPOTDzp49DkHTKBgh
|
E29S40+4z9yivAC9dz7vHZUbyIFP6VG9WyhUYo+/WqOIePyh+iBISQ9TA1DneIYz
|
||||||
XgIIazYT3CkjTIMRrkUv+qfIPQKBgQDcBGu/mqbjxs4sN3zqPS4aB21o6t6W0sXs
|
+VZ8iU5GvdybUPl2C5WN8seaoQKBgQDW3EwVN2EEkChLRJbQYN2qpjn+0vYESMJ8
|
||||||
ZP9w6RlTPQi5U2oRbftjZtYc0bbEgkMUImB1HwYPQT5pJ+MyC414xDvSc2exBr5d
|
K0sgMRtgh4+/T2Xb9b8O/dd87Fi/4oaUqWZ2E2sdsXq8P/IEo0cv6SRfHMy7GyxL
|
||||||
To6yyPIy78Tf5PHM12fpKV92nSvoz/pSjYcGxxDtKfPqu+t8mOJfjCV1lLLA+xuB
|
RM7ztwUfMC4LVWi0ZIXMrm4gRDGN2XjGvhkX6fU2lSf6azWL1K3wI3amNV2b7P7d
|
||||||
DDaE4p8dBwKBgQCdAne6A5v/HMH8UQZeCxHJpESvKiiVnnU/UEx651nID7XvlNNX
|
ItpvdkH3MQKBgQCXf29YJEQkXB9t6J3fDzND3xb4cwy5wSo7ZeBa7CTuWOhoeeX1
|
||||||
0X0mKqsMd4ZvW43ddSYan/JF0LAa3FW8jYWO/3jF9vzOWoysOdvNBZetgf/Uq5ao
|
JIJyAp0/e9goT0SThChRlFtu6gZPivJkoMnr6IOInLrg7we15fc4HPR/kCDgxTVT
|
||||||
aDZ/YbzmVCXWD7jIbPMkjs3pqrAkL0mzDzQc7+dGviWKrV6IYIfIqnn7gQKBgDCz
|
m2wJOAMxigNYZogwRfn2yRLL1BD+PBHD+H936xcX1bSJOUyPSGOC/xLhIQKBgQCb
|
||||||
vdIk/qpO+JZrFfiX4Fucp0hhLTJ/p5ZDaRPqVVPKn+K+Jy2ChfIj8mNgvK9VEloj
|
kCDd85ygyycBaAWxlZCor3WqFF/fNjbp5Aaepi9mMoBXSUs8eK7+UbelURHozEAY
|
||||||
nexvGJ1J2PHYBX+vdPp1nbRhHWPfVUY8PHQw7QP/dToGaMvqJrNDGEGeWvjnCMc7
|
fpYaw3B4rTlp9vppdTZjb+/PlXB9v+zQCl+0gTyKGj4cIpiOk4F0co51eipOw7f4
|
||||||
UtdaO1H0Rm0AegkTopB56lTTvJnhO95eALd7nrMDAoGAEPdzJtWoKafp49svhSj0
|
XUaZ0+CgxlmNq/W26iONjH+pU1YVQQA+Z6+zp/GW4QKBgQCrzYgeugxxqgJzyIRu
|
||||||
hiXQv2SPBwVUN4LZ4SOWiXUcmYYm80aNpYKLkBxYjrfqFWhE7NUHLGp8YorQWKY2
|
0njJkIg+T5gHvsQrtpzq7LVob+HBiBiT7eDOeGDXTK8F//sk969QVrDMQsTMvGW9
|
||||||
acD9AReHk/xku0ABy6jeYmSCmCxASxst5liKD+l12sk0gB0rk5MBxB4Uu1MIbQZ2
|
sG1oTqxciALTMqkJTf8+hT9Uogir0/iTbJUzTt5vPYpQOEQwQHIXMUTjZ9C6NDKT
|
||||||
aCASX3AVD2/XyC2MKkzc8Eg=
|
QlmeMCxeWyPYqoMfwKmdtDP/Iw==
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIDZzCCAk+gAwIBAgIUXizLjwkdqepX0bh0K3abeJxj68IwDQYJKoZIhvcNAQEL
|
MIIDgDCCAmigAwIBAgIUT9NYpZbrAKokSPSTE3zzsAMowvEwDQYJKoZIhvcNAQEL
|
||||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMzU5
|
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA5MjIxMjI3
|
||||||
MTFaFw0zNTA1MDMxMzU5MTFaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
|
MzVaFw0zNTA5MjAxMjI3MzVaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
|
||||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrzGSScSgaQuZdELGFYiLiYRwr
|
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK1DNwTQWmyK71Ar56NvmSMQ8s
|
||||||
LKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI39WmH95aX3d+A
|
qUY3jGqqPVORjfDUtDCrPPdCxT+ZlnsAgdonElWoWqczMrSyBRgfJlZMd4lEvt6V
|
||||||
U7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ouuN6AOdzWs8hp
|
EEiZGUvA/lG1XIVgrx1kMSHKBoJj7lCBN6r3IWmYe6CxgfZurgp+7Z22i6cGMOnQ
|
||||||
RYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFft1ZXxIZOCjDs
|
0XduX5Asup6zk5V7AE6i9eKrJsUjYmRBXtk099IitkER4TMqh6WxJmFF+eV9P/ax
|
||||||
rEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71IfieLCEzMTP1VXa
|
fxkon+bQWITwP1PLC1UOTK7lR0EcVan5aY6WMs/6RfO4Gw/dvuiVG1jCrVcaKNGT
|
||||||
tP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZrQUGnL2RtAgMB
|
PYqmQqs+MOvyIqJ9kYELRZu+6bhPWSXk2ESpSIUIPH9twfnmWrncneIJR24/AgMB
|
||||||
AAGjgaIwgZ8wHwYDVR0jBBgwFoAUCagYzLutXNaduLccP8vpL9w/legwCQYDVR0T
|
AAGjgbswgbgwHwYDVR0jBBgwFoAU1zXywClWshkHWvyWSz7M66c+i1cwCQYDVR0T
|
||||||
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMAYDVR0RBCkw
|
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwSQYDVR0RBEIw
|
||||||
J4IJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdDAdBgNVHQ4E
|
QIIJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdIIXKi5vdGhl
|
||||||
FgQUfdh1p52ZgWyZcBgBXGwKi4EnUE0wDQYJKoZIhvcNAQELBQADggEBAKrHEuB6
|
cnNpdGUubS5sb2NhbGhvc3QwHQYDVR0OBBYEFIkGX+cEJ1ISKIwuT1zzp7uHJ90e
|
||||||
33j8+EwSHw3zrvt/DRXK2BDHI1Ir9JcztSunaKAjZXVvf/dvZp0Xs1dEdJIdnv6G
|
MA0GCSqGSIb3DQEBCwUAA4IBAQBnnnfB7KmyYo16ZYUCmoqGhbM4p8npeYTh5ySb
|
||||||
iZYhBbOqDqpQZbf2h/h0kuu5yZSBUdnQXnYNxlhp2UaC/UEgw5iZT/p1rm7RjVie
|
K01YwGCnMU1qGfJnKHaRwQ2+KtVGZnpBdjmsHcOUetA3V2BirPaYowMCMtaI36LD
|
||||||
y4Dp2WytV5iZOLmLj6xDvd3DXazgJPWIRX8p8qJZbKTkwCjTr7nDIj8jjG1sVFf7
|
LnxvboSZLX0mgEYuN7HmxW4a7fSelDecTYa7xti1sNhE/w8xW7Lky046/DousyUy
|
||||||
1RJBO5/6WSnImrpDmlLUrvjiKvbxcdseDJyBOhTwdRdSk4S2M+s5tR5j2I1gXLOq
|
d9x3wJ183GGj1W2p6bR1E4sqTr/VbmoULQxnqA3GUNOxW3lRL5e8lQ6jJVRmMF4k
|
||||||
J5ioN76+SCrTY0K0WKRy9oOXWO1/X3+VYcekp+0F3SGkd5w17jylCv1XIGHAdEsQ
|
92BtMPrI/m7jwHj0f/WBLI8mdJ/O/W/NxQOG475FZePDfrg+MkeXPChPggf42/ou
|
||||||
v2z2/aMI/7sAD2Q=
|
AMm56FNB7e1l0b1Fots730RfpCPuXpiAxL4pisS0X1dMVeeM
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrzGSScSgaQuZd
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK1DNwTQWmyK71
|
||||||
ELGFYiLiYRwrLKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI3
|
Ar56NvmSMQ8sqUY3jGqqPVORjfDUtDCrPPdCxT+ZlnsAgdonElWoWqczMrSyBRgf
|
||||||
9WmH95aX3d+AU7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ou
|
JlZMd4lEvt6VEEiZGUvA/lG1XIVgrx1kMSHKBoJj7lCBN6r3IWmYe6CxgfZurgp+
|
||||||
uN6AOdzWs8hpRYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFf
|
7Z22i6cGMOnQ0XduX5Asup6zk5V7AE6i9eKrJsUjYmRBXtk099IitkER4TMqh6Wx
|
||||||
t1ZXxIZOCjDsrEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71Ifi
|
JmFF+eV9P/axfxkon+bQWITwP1PLC1UOTK7lR0EcVan5aY6WMs/6RfO4Gw/dvuiV
|
||||||
eLCEzMTP1VXatP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZr
|
G1jCrVcaKNGTPYqmQqs+MOvyIqJ9kYELRZu+6bhPWSXk2ESpSIUIPH9twfnmWrnc
|
||||||
QUGnL2RtAgMBAAECggEAJaFQii8U/KOYt9vXNoMnZvSkaeSQLLhn2V6Kciu1CtWE
|
neIJR24/AgMBAAECggEANRp6vzfDN4gKWoaV5TyYegCTNv+4rRl74cd9pjmx6Jam
|
||||||
aMTWLsFE6nk+G5xXkYcTmM3T0GghtH3u5CjyI6EcsEkeEorCZJt0wbmayDmqiekR
|
uWaUXCx1etpNqPPWcG1Z9OKLLRnk+kjgKGOqq4mObGvGreNeBot7bHOJZADtwMMI
|
||||||
LfMzOdHuTHX5+edPgMGYYG1BFyRKyYFsjH1b5zRFZhXdGQnrl5760GsVlz9D1KZQ
|
YG+Gp7StlclS1YoEHoDmezA/AcqDgTXa9KF0rdMBb1sGFJCLAuBNSJCxtVV6CQIz
|
||||||
iHcT+q1S2tmZeoUukQnADENKXUMCyTGM5FCddgNtsWnGDsTDayh7hUdvDkB+mW4G
|
X26uT0m+Wx8MQyQWA7Sqy6DQNJo++IZkvr7a3cidqBOUPs+QvnIV5JsUb2gp5tGn
|
||||||
lSp+BZuc3PCwpbD6qkXvfugWs6CUAAtXoV3ceWgxQ+TEnNlwxaG1AyugfgNUBolk
|
zk+ObeRjoFFWYAN/NK7bneRenkP40m3MSL8ZfaEuuonui7CrxM1SiQyq2N1u/Aoy
|
||||||
8xgeZt4r5QId03jsHDf7hpBAofcaCd5EMIIQYFvWoQKBgQDlbAvAzEFPTZZn2nRV
|
OE1JtNaVPbLBo6kG5al7Sj4Z0zhRt+iv93S2lZMkBQKBgQD2+FpLTqyLO1NDOFkE
|
||||||
Xagw4xjqVc1LLEKLCWq0N5rEkwn0h90Dz5N7/3NuonP/sIDsDHCbyiOYBI1Ck6Xi
|
kxU+LdLOx0OV9wASC0ApPOu1dHMG6ksByr7TWeiu6GJDgajusPB7NVPOt2cm4iWU
|
||||||
0WuB+OyKDh+xeF2mekN9G9ywPahdK5lT/TVsxXFyZlwtVv1x/6KBO4yv5URizxqU
|
xPxXPO5l87uiSvu80h5uG4Qdj8KEijHkdap2wbVkU/mm8lBKC36jyBQIlJKySyXY
|
||||||
gyAPDDxfD/KcNjkOBaodWEwQGQKBgQC/s2gPDBtQkjLwkHXchBomLww5eLlVrac1
|
zSEMfLK9jQPKz5cKKT3dVj/fAwKBgQDSPq9oks6K96MAB66o6cm214otQlnTQkPM
|
||||||
WK4UX6uSdOgrjJ375OOgMTxe8NVZdOuAKytGXRWDwgH3nVWvuZhe7dGlX3JMuSer
|
xgjtjddX+Lp9tgihGvtSfPbyy89oUDHCfKvW/AHG52e5dec5YUi6mVdHEWbk33Kt
|
||||||
e9VwDpBESrvqcR4ruL6wm8wej6BXyjH0wD3FHb0S5HfuBDxTn+4bDwrbRzOUMNgy
|
BoQuxeK3XseIDlD/JD9Dd7KfUyO5w2jtYLfNdqez41O4qj2N52m1KwJYTwMsc8Kq
|
||||||
lSppuflxdQKBgQDiZcIfazFT8evn5nMAvuC4BZNTxIJHmZC9JfjPiUPIkpWzYtOe
|
izVgkC5hFQKBgBFAc/5CtqbbNAvECePZ6mf3h3xOSxhUsrqP8qFu0gBQ7CAVibvM
|
||||||
7BvNtKOT3Op9uw8uYYRKqKqBXJSNy6ha8XCXHS9HeXKbLn20SFkLQBCDNwVLlDfF
|
T9wvsaNWNFcG3age0A2rQfl0sk3zCjEEOaRWa0jP59GEb2VXQCzs2yO9gRcFGEsf
|
||||||
40zyXtF6JDr4XyzSb4NM5pgKCER5AYloXxGm59s3sEQpFXUuOjbKqJS/GQKBgAoI
|
NRMqoOMrQos47gbeGrCSL2QSDNVLjo9AdQiMRWgcS6GFMsXQ77NgbQHFAoGBAI4a
|
||||||
c7vF4HAZFr1sch62cz/oWnVvkhOf4Q5zs7ixQSOLJtOQqnwSgK9TpFs7s47ZBbJR
|
YGTGFWRITJvQlXUFz5kNxg8hMaVgvILDt3UY0dxb+XDOgLajjgsK+77Pkrhmu7tA
|
||||||
kBRAru2Ua9Hv1Bo8VnMxczV6h1roneDlvEf/GyHX33nnrbKQGrrXjJlU3wl5NaAf
|
mMUOQAU4kxr/XfGil43H5v3Z/Tnk7ZWVOfKDPeHC5gpH4ucQkNIBLXISt6rvMRSA
|
||||||
p5v3cHvapUQ5yIZ/6lBUOzc6xMJOxCHxmKSr7Rg5AoGAbEE4lt6Xh2dnBPJ81eNI
|
srrk4CTuGcBPEJvBNemF0Gfvv61j8MdkoAdMbIyhAoGAfGR6yZLBmRMsW5PKmcpT
|
||||||
IDrw/3ITY53qAY4Bx88CByIFuu8CEUdUZprh98jSl6ic1tMinZfUhRMwABLrUD51
|
nq2oSeUpmtGZra6pWz/3XU7AgrCLcx1DmqEjm4w7y5NQJmxyMZqqdTJILCjr3Srt
|
||||||
DGst8iGLPD9u83iMcUHI/L+p7AbxrKLvWXZrF5UZm440c9mSWqfhPaTBosPtNDsG
|
+2F0NqQL6Li+xQGibAvDj0Jxyol38RvFC0J/w2vQmuF0hTuH95yknSd7FPXK+DPG
|
||||||
LfETwH1flKXMTXd2xA9RTE4=
|
qYgXLjun9dht6kx9vGJ69wI=
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
@@ -34,5 +34,6 @@ subjectAltName = @alt_names
|
|||||||
DNS.1 = localhost
|
DNS.1 = localhost
|
||||||
DNS.2 = m.localhost
|
DNS.2 = m.localhost
|
||||||
DNS.3 = *.m.localhost
|
DNS.3 = *.m.localhost
|
||||||
|
DNS.4 = *.othersite.m.localhost
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|||||||
53
backend/ew.test.othersite.config.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"default_server_config": {
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://synapse.othersite.m.localhost",
|
||||||
|
"server_name": "synapse.othersite.m.localhost"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable_custom_urls": false,
|
||||||
|
"disable_guests": false,
|
||||||
|
"disable_login_language_selector": false,
|
||||||
|
"disable_3pid_login": false,
|
||||||
|
"force_verification": false,
|
||||||
|
"brand": "Element",
|
||||||
|
"integrations_ui_url": "https://scalar.vector.im/",
|
||||||
|
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||||
|
"integrations_widgets_urls": [
|
||||||
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
|
"https://scalar.vector.im/api",
|
||||||
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
|
],
|
||||||
|
"default_widget_container_height": 280,
|
||||||
|
"default_country_code": "GB",
|
||||||
|
"show_labs_settings": false,
|
||||||
|
"features": {
|
||||||
|
"feature_element_call_video_rooms": true,
|
||||||
|
"feature_video_rooms": true,
|
||||||
|
"feature_group_calls": true,
|
||||||
|
"feature_release_announcement": false
|
||||||
|
},
|
||||||
|
"default_federate": true,
|
||||||
|
"default_theme": "light",
|
||||||
|
"room_directory": {
|
||||||
|
"servers": ["matrix.org"]
|
||||||
|
},
|
||||||
|
"enable_presence_by_hs_url": {
|
||||||
|
"https://matrix.org": false,
|
||||||
|
"https://matrix-client.matrix.org": false
|
||||||
|
},
|
||||||
|
"setting_defaults": {
|
||||||
|
"breadcrumbs": true,
|
||||||
|
"feature_group_calls": true
|
||||||
|
},
|
||||||
|
"jitsi": {
|
||||||
|
"preferred_domain": "meet.element.io"
|
||||||
|
},
|
||||||
|
"element_call": {
|
||||||
|
"participant_limit": 8,
|
||||||
|
"brand": "Element Call"
|
||||||
|
},
|
||||||
|
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||||
|
}
|
||||||
81
backend/playwright_homeserver-othersite.yaml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
server_name: "synapse.othersite.m.localhost"
|
||||||
|
public_baseurl: https://synapse.othersite.m.localhost/
|
||||||
|
|
||||||
|
pid_file: /data/homeserver.pid
|
||||||
|
|
||||||
|
listeners:
|
||||||
|
- port: 18008
|
||||||
|
tls: false
|
||||||
|
type: http
|
||||||
|
x_forwarded: true
|
||||||
|
resources:
|
||||||
|
- names: [client, federation, openid]
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
database:
|
||||||
|
name: sqlite3
|
||||||
|
args:
|
||||||
|
database: /data/homeserver.db
|
||||||
|
|
||||||
|
media_store_path: /data/media_store
|
||||||
|
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||||
|
|
||||||
|
# Due to custom TLS certificate with domains
|
||||||
|
# - m.localhost, localhost
|
||||||
|
# - *.m.localhost
|
||||||
|
# - *.othersite.m.localhost
|
||||||
|
# we disable certificate verification to allow for federation.
|
||||||
|
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||||
|
federation_verify_certificates: false
|
||||||
|
ip_range_blacklist: []
|
||||||
|
trusted_key_servers:
|
||||||
|
- server_name: "synapse.m.localhost"
|
||||||
|
accept_keys_insecurely: true
|
||||||
|
|
||||||
|
experimental_features:
|
||||||
|
# MSC3266: Room summary API. Used for knocking over federation
|
||||||
|
msc3266_enabled: true
|
||||||
|
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||||
|
# correctly track the state of the room.
|
||||||
|
msc4222_enabled: true
|
||||||
|
|
||||||
|
# The maximum allowed duration by which sent events can be delayed, as
|
||||||
|
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||||
|
# duration (null), which disallows sending delayed events.
|
||||||
|
max_event_delay_duration: 24h
|
||||||
|
|
||||||
|
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||||
|
enable_registration: true
|
||||||
|
enable_registration_without_verification: true
|
||||||
|
|
||||||
|
report_stats: false
|
||||||
|
serve_server_wellknown: true
|
||||||
|
|
||||||
|
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||||
|
#
|
||||||
|
# Each ratelimiting configuration is made of two parameters:
|
||||||
|
# - per_second: number of requests a client can send per second.
|
||||||
|
# - burst_count: number of requests a client can send before being throttled.
|
||||||
|
|
||||||
|
rc_message:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
|
rc_delayed_event_mgmt:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
|
rc_login:
|
||||||
|
address:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
account:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
failed_attempts:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
|
rc_registration:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
@@ -19,8 +19,18 @@ database:
|
|||||||
|
|
||||||
media_store_path: /data/media_store
|
media_store_path: /data/media_store
|
||||||
signing_key_path: "/data/SERVERNAME.signing.key"
|
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||||
|
|
||||||
|
# Due to custom TLS certificate with domains
|
||||||
|
# - m.localhost, localhost
|
||||||
|
# - *.m.localhost
|
||||||
|
# - *.othersite.m.localhost
|
||||||
|
# we disable certificate verification to allow for federation.
|
||||||
|
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||||
|
federation_verify_certificates: false
|
||||||
|
ip_range_blacklist: []
|
||||||
trusted_key_servers:
|
trusted_key_servers:
|
||||||
- server_name: "matrix.org"
|
- server_name: "synapse.othersite.m.localhost"
|
||||||
|
accept_keys_insecurely: true
|
||||||
|
|
||||||
experimental_features:
|
experimental_features:
|
||||||
# MSC3266: Room summary API. Used for knocking over federation
|
# MSC3266: Room summary API. Used for knocking over federation
|
||||||
@@ -34,6 +44,13 @@ experimental_features:
|
|||||||
# duration (null), which disallows sending delayed events.
|
# duration (null), which disallows sending delayed events.
|
||||||
max_event_delay_duration: 24h
|
max_event_delay_duration: 24h
|
||||||
|
|
||||||
|
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||||
|
enable_registration: true
|
||||||
|
enable_registration_without_verification: true
|
||||||
|
|
||||||
|
report_stats: false
|
||||||
|
serve_server_wellknown: true
|
||||||
|
|
||||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||||
#
|
#
|
||||||
# Each ratelimiting configuration is made of two parameters:
|
# Each ratelimiting configuration is made of two parameters:
|
||||||
@@ -44,6 +61,10 @@ rc_message:
|
|||||||
per_second: 10000
|
per_second: 10000
|
||||||
burst_count: 10000
|
burst_count: 10000
|
||||||
|
|
||||||
|
rc_delayed_event_mgmt:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
rc_login:
|
rc_login:
|
||||||
address:
|
address:
|
||||||
per_second: 10000
|
per_second: 10000
|
||||||
@@ -58,10 +79,3 @@ rc_login:
|
|||||||
rc_registration:
|
rc_registration:
|
||||||
per_second: 10000
|
per_second: 10000
|
||||||
burst_count: 10000
|
burst_count: 10000
|
||||||
|
|
||||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
|
||||||
enable_registration: true
|
|
||||||
enable_registration_without_verification: true
|
|
||||||
|
|
||||||
report_stats: false
|
|
||||||
serve_server_wellknown: true
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
bind 0.0.0.0
|
|
||||||
protected-mode yes
|
|
||||||
port 6379
|
|
||||||
timeout 0
|
|
||||||
tcp-keepalive 300
|
|
||||||
@@ -8,5 +8,12 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"feature_use_device_session_member_events": true
|
"feature_use_device_session_member_events": true
|
||||||
},
|
},
|
||||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf"
|
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||||
|
"matrix_rtc_session": {
|
||||||
|
"wait_for_key_rotation_ms": 3000,
|
||||||
|
"membership_event_expiry_ms": 180000000,
|
||||||
|
"delayed_leave_event_delay_ms": 18000,
|
||||||
|
"delayed_leave_event_restart_ms": 4000,
|
||||||
|
"network_error_retry_ms": 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,12 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"feature_use_device_session_member_events": true
|
"feature_use_device_session_member_events": true
|
||||||
},
|
},
|
||||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf"
|
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||||
|
"matrix_rtc_session": {
|
||||||
|
"wait_for_key_rotation_ms": 3000,
|
||||||
|
"membership_event_expiry_ms": 180000000,
|
||||||
|
"delayed_leave_event_delay_ms": 18000,
|
||||||
|
"delayed_leave_event_restart_ms": 4000,
|
||||||
|
"network_error_retry_ms": 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
"server_name": "call-unstable.ems.host"
|
"server_name": "call-unstable.ems.host"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||||
|
"matrix_rtc_session": {
|
||||||
|
"wait_for_key_rotation_ms": 3000,
|
||||||
|
"membership_event_expiry_ms": 180000000,
|
||||||
|
"delayed_leave_event_delay_ms": 18000,
|
||||||
|
"delayed_leave_event_restart_ms": 4000,
|
||||||
|
"network_error_retry_ms": 100
|
||||||
|
},
|
||||||
"posthog": {
|
"posthog": {
|
||||||
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
|
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
|
||||||
"api_host": "https://posthog-element-call.element.io"
|
"api_host": "https://posthog-element-call.element.io"
|
||||||
|
|||||||
@@ -4,26 +4,51 @@ networks:
|
|||||||
services:
|
services:
|
||||||
auth-service:
|
auth-service:
|
||||||
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||||
|
pull_policy: always
|
||||||
hostname: auth-server
|
hostname: auth-server
|
||||||
environment:
|
environment:
|
||||||
- LIVEKIT_JWT_PORT=8080
|
- LIVEKIT_JWT_PORT=6080
|
||||||
- LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu
|
- LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu
|
||||||
- LIVEKIT_KEY=devkey
|
- LIVEKIT_KEY=devkey
|
||||||
- LIVEKIT_SECRET=secret
|
- LIVEKIT_SECRET=secret
|
||||||
# If the configured homeserver runs on localhost, it'll probably be using
|
# If the configured homeserver runs on localhost, it'll probably be using
|
||||||
# a self-signed certificate
|
# a self-signed certificate
|
||||||
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
|
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
|
||||||
|
- LIVEKIT_FULL_ACCESS_HOMESERVERS=*
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
ports:
|
ports:
|
||||||
# HOST_PORT:CONTAINER_PORT
|
# HOST_PORT:CONTAINER_PORT
|
||||||
- 8080:8080
|
- 6080:6080
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
|
auth-service-1:
|
||||||
|
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||||
|
pull_policy: always
|
||||||
|
hostname: auth-server-1
|
||||||
|
environment:
|
||||||
|
- LIVEKIT_JWT_PORT=16080
|
||||||
|
- LIVEKIT_URL=wss://matrix-rtc.othersite.m.localhost/livekit/sfu
|
||||||
|
- LIVEKIT_KEY=devkey
|
||||||
|
- LIVEKIT_SECRET=secret
|
||||||
|
# If the configured homeserver runs on localhost, it'll probably be using
|
||||||
|
# a self-signed certificate
|
||||||
|
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
|
||||||
|
- LIVEKIT_FULL_ACCESS_HOMESERVERS=*
|
||||||
|
deploy:
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
ports:
|
||||||
|
# HOST_PORT:CONTAINER_PORT
|
||||||
|
- 16080:16080
|
||||||
networks:
|
networks:
|
||||||
- ecbackend
|
- ecbackend
|
||||||
|
|
||||||
livekit:
|
livekit:
|
||||||
image: livekit/livekit-server:latest
|
image: livekit/livekit-server:latest
|
||||||
|
pull_policy: always
|
||||||
hostname: livekit-sfu
|
hostname: livekit-sfu
|
||||||
command: --dev --config /etc/livekit.yaml
|
command: --dev --config /etc/livekit.yaml
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -41,20 +66,30 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- ecbackend
|
- ecbackend
|
||||||
|
|
||||||
redis:
|
livekit-1:
|
||||||
image: redis:6-alpine
|
image: livekit/livekit-server:latest
|
||||||
command: redis-server /etc/redis.conf
|
pull_policy: always
|
||||||
|
hostname: livekit-sfu-1
|
||||||
|
command: --dev --config /etc/livekit.yaml
|
||||||
|
restart: unless-stopped
|
||||||
|
# The SFU seems to work far more reliably when we let it share the host
|
||||||
|
# network rather than opening specific ports (but why?? we're not missing
|
||||||
|
# any…)
|
||||||
ports:
|
ports:
|
||||||
# HOST_PORT:CONTAINER_PORT
|
# HOST_PORT:CONTAINER_PORT
|
||||||
- 6379:6379
|
- 17880:17880/tcp
|
||||||
|
- 17881:17881/tcp
|
||||||
|
- 17882:17882/tcp
|
||||||
|
- 50300-50400:50300-50400/udp
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/redis.conf:/etc/redis.conf:Z
|
- ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z
|
||||||
networks:
|
networks:
|
||||||
- ecbackend
|
- ecbackend
|
||||||
|
|
||||||
synapse:
|
synapse:
|
||||||
hostname: homeserver
|
hostname: homeserver
|
||||||
image: docker.io/matrixdotorg/synapse:latest
|
image: docker.io/matrixdotorg/synapse:latest
|
||||||
|
pull_policy: always
|
||||||
environment:
|
environment:
|
||||||
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
|
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
|
||||||
# Needed for rootless podman-compose such that the uid/gid mapping does
|
# Needed for rootless podman-compose such that the uid/gid mapping does
|
||||||
@@ -69,6 +104,24 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- ecbackend
|
- ecbackend
|
||||||
|
|
||||||
|
synapse-1:
|
||||||
|
hostname: homeserver-1
|
||||||
|
image: docker.io/matrixdotorg/synapse:latest
|
||||||
|
pull_policy: always
|
||||||
|
environment:
|
||||||
|
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
|
||||||
|
# Needed for rootless podman-compose such that the uid/gid mapping does
|
||||||
|
# fit local user uid. If the container runs as root (uid 0) it is fine as
|
||||||
|
# it actually maps to your non-root user on the host (e.g. 1000).
|
||||||
|
# Otherwise uid mapping will not match your non-root user.
|
||||||
|
- UID=0
|
||||||
|
- GID=0
|
||||||
|
volumes:
|
||||||
|
- ./backend/synapse_tmp_othersite:/data:Z
|
||||||
|
- ./backend/dev_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
element-web:
|
element-web:
|
||||||
image: ghcr.io/element-hq/element-web:develop
|
image: ghcr.io/element-hq/element-web:develop
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
@@ -81,10 +134,24 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- ecbackend
|
- ecbackend
|
||||||
|
|
||||||
|
element-web-1:
|
||||||
|
image: ghcr.io/element-hq/element-web:develop
|
||||||
|
pull_policy: always
|
||||||
|
volumes:
|
||||||
|
- ./backend/ew.test.othersite.config.json:/app/config.json:Z
|
||||||
|
environment:
|
||||||
|
ELEMENT_WEB_PORT: 18081
|
||||||
|
ports:
|
||||||
|
# HOST_PORT:CONTAINER_PORT
|
||||||
|
- "18081:18081"
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
# see backend/dev_tls_setup for how to generate the tls certs
|
# see backend/dev_tls_setup for how to generate the tls certs
|
||||||
hostname: synapse.m.localhost
|
hostname: synapse.m.localhost
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
|
pull_policy: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
|
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
|
||||||
- ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z
|
- ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z
|
||||||
@@ -100,4 +167,9 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- synapse
|
- synapse
|
||||||
networks:
|
networks:
|
||||||
- ecbackend
|
ecbackend:
|
||||||
|
aliases:
|
||||||
|
- synapse.m.localhost
|
||||||
|
- synapse.othersite.m.localhost
|
||||||
|
- matrix-rtc.m.localhost
|
||||||
|
- matrix-rtc.othersite.m.localhost
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
@@ -1,7 +1,31 @@
|
|||||||
# Global JS controls
|
# Global JS controls
|
||||||
|
|
||||||
A few aspects of Element Call's interface can be controlled through a global API on the `window`:
|
A few aspects of Element Call's interface can be controlled through a global API on the `window`.
|
||||||
|
|
||||||
|
## Picture-in-picture
|
||||||
|
|
||||||
- `controls.canEnterPip(): boolean` Determines whether it's possible to enter picture-in-picture mode.
|
- `controls.canEnterPip(): boolean` Determines whether it's possible to enter picture-in-picture mode.
|
||||||
- `controls.enablePip(): void` Puts the call interface into picture-in-picture mode. Throws if not in a call.
|
- `controls.enablePip(): void` Puts the call interface into picture-in-picture mode. Throws if not in a call.
|
||||||
- `controls.disablePip(): void` Takes the call interface out of picture-in-picture mode, restoring it to its natural display mode. Throws if not in a call.
|
- `controls.disablePip(): void` Takes the call interface out of picture-in-picture mode, restoring it to its natural display mode. Throws if not in a call.
|
||||||
|
|
||||||
|
## Audio devices
|
||||||
|
|
||||||
|
On mobile platforms (iOS, Android), web views do not reliably support selecting audio output devices such as the main speaker, earpiece, or headset. To address this limitation, the following functions allow the hosting application (e.g., Element Web, Element X) to manage audio devices via exposed JavaScript interfaces. These functions must be enabled using the URL parameter `controlledAudioDevices` to take effect.
|
||||||
|
|
||||||
|
- `controls.setAvailableAudioDevices(devices: { id: string, name: string, forEarpiece?: boolean, isEarpiece?: boolean isSpeaker?: boolean, isExternalHeadset?, boolean; }[]): void` Sets the list of available audio outputs. `forEarpiece` is used on iOS only.
|
||||||
|
It flags the device that should be used if the user selects earpiece mode. This should be the main stereo loudspeaker of the device.
|
||||||
|
- `controls.onAudioDeviceSelect: ((id: string) => void) | undefined` Callback called whenever the user or application selects a new audio output.
|
||||||
|
- `controls.setAudioDevice(id: string): void` Sets the selected audio device in Element Call's menu. This should be used if the OS decides to automatically switch to Bluetooth, for example.
|
||||||
|
- `controls.setAudioEnabled(enabled: boolean)` Enables/disables all audio output from the application. Output is enabled by default.
|
||||||
|
- `controls.onAudioPlaybackStarted: ((id: string) => void) | undefined`: This will be called the first time we start
|
||||||
|
playing audio in the webview. It can be helpful to do device setup on the native app when the webviews audio is ready.
|
||||||
|
In particular android is using it to setup the output channel so that the call volume can
|
||||||
|
be controlled by the hardware volume rocker.
|
||||||
|
|
||||||
|
## Element Call button delegation
|
||||||
|
|
||||||
|
Callbacks for buttons in EC that are handled by the native application
|
||||||
|
|
||||||
|
- `showNativeAudioDevicePicker: (() => void) | undefined`. Callback called whenever the user presses the output button in the settings menu.
|
||||||
|
This button is only shown on iOS. (`/iPad|iPhone|iPod|Mac/.test(navigator.userAgent)`)
|
||||||
|
- `onBackButtonPressed: (() => void) | undefined`. Callback when the webview detects a tab on the header's back button.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 929 KiB After Width: | Height: | Size: 941 KiB |
|
Before Width: | Height: | Size: 886 KiB After Width: | Height: | Size: 908 KiB |
@@ -64,7 +64,7 @@ rc_delayed_event_mgmt:
|
|||||||
```
|
```
|
||||||
|
|
||||||
As a prerequisite for the
|
As a prerequisite for the
|
||||||
[Matrix LiveKit JWT auth service](https://github.com/element-hq/lk-jwt-service)
|
[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||||
make sure that your Synapse server has either a `federation` or `openid`
|
make sure that your Synapse server has either a `federation` or `openid`
|
||||||
[listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners).
|
[listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners).
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ required for each site deployment.
|
|||||||
|
|
||||||
As depicted above in the `example.com` site deployment, Element Call requires a
|
As depicted above in the `example.com` site deployment, Element Call requires a
|
||||||
[Livekit SFU](https://github.com/livekit/livekit) alongside a
|
[Livekit SFU](https://github.com/livekit/livekit) alongside a
|
||||||
[Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service)
|
[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||||
to implement
|
to implement
|
||||||
[MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
|
[MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ the example above, this results in:
|
|||||||
| Service | Endpoint | Example |
|
| Service | Endpoint | Example |
|
||||||
| -------- | ------- | ------- |
|
| -------- | ------- | ------- |
|
||||||
| [Livekit SFU](https://github.com/livekit/livekit) WebSocket signalling connection | `/livekit/sfu` | `matrix-rtc.example.com/livekit/sfu` |
|
| [Livekit SFU](https://github.com/livekit/livekit) WebSocket signalling connection | `/livekit/sfu` | `matrix-rtc.example.com/livekit/sfu` |
|
||||||
| [Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
|
| [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
|
||||||
|
|
||||||
Using Nginx, you can achieve this by:
|
Using Nginx, you can achieve this by:
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# JWT Service running at port 8080
|
# MatrixRTC Authorization Service running at port 8080
|
||||||
proxy_pass http://localhost:8080/;
|
proxy_pass http://localhost:8080/;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +126,32 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or Using Caddy, you can achieve this by:
|
||||||
|
|
||||||
|
```caddy configuration file
|
||||||
|
# Route for lk-jwt-service with livekit/jwt prefix
|
||||||
|
@jwt_service path /livekit/jwt/sfu/get /livekit/jwt/healthz
|
||||||
|
handle @jwt_service {
|
||||||
|
uri strip_prefix /livekit/jwt
|
||||||
|
reverse_proxy http://[::1]:8080 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Forwarded-Server {host}
|
||||||
|
header_up X-Real-IP {remote_addr}
|
||||||
|
header_up X-Forwarded-For {remote_addr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default route for livekit
|
||||||
|
handle {
|
||||||
|
reverse_proxy http://localhost:7880 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Forwarded-Server {host}
|
||||||
|
header_up X-Real-IP {remote_addr}
|
||||||
|
header_up X-Forwarded-For {remote_addr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### MatrixRTC backend announcement
|
#### MatrixRTC backend announcement
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@@ -145,10 +171,6 @@ server {
|
|||||||
{
|
{
|
||||||
"type": "livekit",
|
"type": "livekit",
|
||||||
"livekit_service_url": "https://matrix-rtc-2.example.com/livekit/jwt"
|
"livekit_service_url": "https://matrix-rtc-2.example.com/livekit/jwt"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "nextgen_new_foci_type",
|
|
||||||
"props_for_nextgen_foci": "val"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -218,7 +240,7 @@ server {
|
|||||||
There are currently two different config files. `.env` holds variables that are
|
There are currently two different config files. `.env` holds variables that are
|
||||||
used at build time, while `public/config.json` holds variables that are used at
|
used at build time, while `public/config.json` holds variables that are used at
|
||||||
runtime. Documentation and default values for `public/config.json` can be found
|
runtime. Documentation and default values for `public/config.json` can be found
|
||||||
in [ConfigOptions.ts](src/config/ConfigOptions.ts).
|
in [ConfigOptions.ts](../src/config/ConfigOptions.ts).
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Please note configuring MatrixRTC backend via `config.json` of
|
> Please note configuring MatrixRTC backend via `config.json` of
|
||||||
@@ -255,12 +277,17 @@ self-hosters and developers working with Element Call.
|
|||||||
|
|
||||||
- [How to resolve stuck MatrixRTC calls](https://sspaeth.de/2025/02/how-to-resolve-stuck-matrixrtc-calls/)
|
- [How to resolve stuck MatrixRTC calls](https://sspaeth.de/2025/02/how-to-resolve-stuck-matrixrtc-calls/)
|
||||||
|
|
||||||
## 🛠️ How-Tos & Tutorials
|
## 📝 How-Tos & Tutorials
|
||||||
|
|
||||||
- [MatrixRTC aka Element-call setup (Geek warning)](https://sspaeth.de/2024/11/sfu/)
|
- [MatrixRTC aka Element-call setup (Geek warning)](https://sspaeth.de/2024/11/sfu/)
|
||||||
- [MatrixRTC with Synology Container Manager (Docker)](https://ztfr.de/matrixrtc-with-synology-container-manager-docker/)
|
- [MatrixRTC with Synology Container Manager (Docker)](https://ztfr.de/matrixrtc-with-synology-container-manager-docker/)
|
||||||
- [Encrypted & Scalable Video Calls: How to deploy an Element Call backend with Synapse Using Docker-Compose](https://willlewis.co.uk/blog/posts/deploy-element-call-backend-with-synapse-and-docker-compose/)
|
- [Encrypted & Scalable Video Calls: How to deploy an Element Call backend with Synapse Using Docker-Compose](https://willlewis.co.uk/blog/posts/deploy-element-call-backend-with-synapse-and-docker-compose/)
|
||||||
- [Element Call einrichten: Verschlüsselte Videoanrufe mit Element X und Matrix Synapse](https://www.cleveradmin.de/blog/2025/04/matrixrtc-element-call-backend-einrichten/)
|
- [Element Call einrichten: Verschlüsselte Videoanrufe mit Element X und Matrix Synapse](https://www.cleveradmin.de/blog/2025/04/matrixrtc-element-call-backend-einrichten/)
|
||||||
|
- [MatrixRTC Back-End for Synapse with Docker Compose and Traefik](https://forge.avontech.net/kstro1/matrixrtc-docker-traefik/)
|
||||||
|
|
||||||
|
## 🛠️ Tools
|
||||||
|
|
||||||
|
- [A Matrix server sanity tester including tests for proper MatrixRTC setup](https://codeberg.org/spaetz/testmatrix)
|
||||||
|
|
||||||
## 🤝 Want to Contribute?
|
## 🤝 Want to Contribute?
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
|||||||
|
|
||||||
| Name | Values | Required for widget | Required for SPA | Description |
|
| Name | Values | Required for widget | Required for SPA | Description |
|
||||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `intent` | `start_call`, `join_existing`, `start_call_dm`, `join_existing_dm. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. |
|
||||||
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
||||||
| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
||||||
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
||||||
@@ -56,18 +57,21 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
|||||||
| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. |
|
| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. |
|
||||||
| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. |
|
| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. |
|
||||||
| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. |
|
| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. |
|
||||||
| `hideHeader` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the room header when in a call. |
|
| `header` | `none`, `standard` or `app_bar` | No, defaults to `standard` | No, defaults to `standard` | The style of headers to show. `standard` is the default arrangement, `none` hides the header entirely, and `app_bar` produces a header with a back button like you might see in mobile apps. The callback for the back button is `window.controls.onBackButtonPressed`. |
|
||||||
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
||||||
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
||||||
| `intent` | `start_call` or `join_existing` | No, defaults to `start_call` | No, defaults to `start_call` | The intent of the user with respect to the call. e.g. if they clicked a Start Call button, this would be `start_call`. If it was a Join Call button, it would be `join_existing`. |
|
|
||||||
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
||||||
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
||||||
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
||||||
|
| `controlledAudioDevices` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the [global JS controls for audio devices](./controls.md#audio-devices) should be enabled, allowing the list of audio devices to be controlled by the app hosting Element Call. |
|
||||||
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
|
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
|
||||||
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
||||||
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
||||||
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
||||||
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
||||||
|
| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. |
|
||||||
|
| `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. |
|
||||||
|
| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. |
|
||||||
|
|
||||||
### Widget-only parameters
|
### Widget-only parameters
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||||
|
|
||||||
[versions]
|
[versions]
|
||||||
android_gradle_plugin = "8.10.0"
|
android_gradle_plugin = "8.11.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
|
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
||||||
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.31.0" }
|
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.34.0" }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import com.vanniktech.maven.publish.SonatypeHost
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.maven.publish)
|
alias(libs.plugins.maven.publish)
|
||||||
@@ -27,7 +25,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mavenPublishing {
|
mavenPublishing {
|
||||||
publishToMavenCentral(SonatypeHost.S01, automaticRelease = true)
|
publishToMavenCentral(automaticRelease = true)
|
||||||
|
|
||||||
signAllPublications()
|
signAllPublications()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default {
|
import type { UserConfig } from "i18next-parser";
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
keySeparator: ".",
|
keySeparator: ".",
|
||||||
namespaceSeparator: false,
|
namespaceSeparator: false,
|
||||||
contextSeparator: "|",
|
contextSeparator: "|",
|
||||||
@@ -26,3 +28,5 @@ export default {
|
|||||||
input: ["src/**/*.{ts,tsx}"],
|
input: ["src/**/*.{ts,tsx}"],
|
||||||
sort: true,
|
sort: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
11
knip.ts
@@ -1,8 +1,15 @@
|
|||||||
import { KnipConfig } from "knip";
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type KnipConfig } from "knip";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
vite: {
|
vite: {
|
||||||
config: ["vite.config.js", "vite-embedded.config.js"],
|
config: ["vite.config.ts", "vite-embedded.config.ts"],
|
||||||
},
|
},
|
||||||
entry: ["src/main.tsx", "i18next-parser.config.ts"],
|
entry: ["src/main.tsx", "i18next-parser.config.ts"],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"developer_mode": {
|
"developer_mode": {
|
||||||
|
"always_show_iphone_earpiece": "Zobrazit možnost sluchátek pro iPhone na všech platformách",
|
||||||
"crypto_version": "Kryptografická verze: {{version}}",
|
"crypto_version": "Kryptografická verze: {{version}}",
|
||||||
"debug_tile_layout_label": "Ladění rozložení dlaždic",
|
"debug_tile_layout_label": "Ladění rozložení dlaždic",
|
||||||
"device_id": "ID zařízení: {{id}}",
|
"device_id": "ID zařízení: {{id}}",
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Volání není podporováno",
|
"call_is_not_supported": "Volání není podporováno",
|
||||||
"call_not_found": "Volání nebylo nalezeno",
|
"call_not_found": "Volání nebylo nalezeno",
|
||||||
"call_not_found_description": "<0>Zdá se, že tento odkaz nepatří k žádnému existujícímu volání. Zkontrolujte, zda máte správný odkaz, nebo <1>vytvořte nový</1>.</0>",
|
"call_not_found_description": "<0>Zdá se, že tento odkaz nepatří k žádnému existujícímu hovoru. Zkontrolujte, zda máte správný odkaz, nebo<2> vytvořte nový</2>.</0>",
|
||||||
"connection_lost": "Spojení ztraceno",
|
"connection_lost": "Spojení ztraceno",
|
||||||
"connection_lost_description": "Hovor byl přerušen.",
|
"connection_lost_description": "Hovor byl přerušen.",
|
||||||
"e2ee_unsupported": "Nekompatibilní prohlížeč",
|
"e2ee_unsupported": "Nekompatibilní prohlížeč",
|
||||||
@@ -104,6 +105,11 @@
|
|||||||
"knock_reject_heading": "Přístup odepřen",
|
"knock_reject_heading": "Přístup odepřen",
|
||||||
"reason": "Důvod"
|
"reason": "Důvod"
|
||||||
},
|
},
|
||||||
|
"handset": {
|
||||||
|
"overlay_back_button": "Zpět do režimu reproduktoru",
|
||||||
|
"overlay_description": "Funguje pouze při používání aplikace",
|
||||||
|
"overlay_title": "Režim sluchátka"
|
||||||
|
},
|
||||||
"hangup_button_label": "Ukončit hovor",
|
"hangup_button_label": "Ukončit hovor",
|
||||||
"header_label": "Domov Element Call",
|
"header_label": "Domov Element Call",
|
||||||
"header_participants_label": "Účastníci",
|
"header_participants_label": "Účastníci",
|
||||||
@@ -173,8 +179,11 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Fotoaparát",
|
"camera": "Fotoaparát",
|
||||||
"camera_numbered": "Fotoaparát {{n}}",
|
"camera_numbered": "Fotoaparát {{n}}",
|
||||||
|
"change_device_button": "Změnit zvukové zařízení",
|
||||||
"default": "Výchozí",
|
"default": "Výchozí",
|
||||||
"default_named": "Výchozí <2> ({{name}}) </2>",
|
"default_named": "Výchozí <2> ({{name}}) </2>",
|
||||||
|
"handset": "Sluchátko",
|
||||||
|
"loudspeaker": "Reproduktor",
|
||||||
"microphone": "Mikrofon",
|
"microphone": "Mikrofon",
|
||||||
"microphone_numbered": "Mikrofon {{n}}",
|
"microphone_numbered": "Mikrofon {{n}}",
|
||||||
"speaker": "Reproduktor",
|
"speaker": "Reproduktor",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"developer_mode": {
|
"developer_mode": {
|
||||||
|
"always_show_iphone_earpiece": "Vis mulighed for iPhone-høretelefon på alle platforme",
|
||||||
"crypto_version": "Krypto-version: {{version}}",
|
"crypto_version": "Krypto-version: {{version}}",
|
||||||
"debug_tile_layout_label": "Fejlfinding af fliselayout",
|
"debug_tile_layout_label": "Fejlfinding af fliselayout",
|
||||||
"device_id": "Enheds-id: {{id}}",
|
"device_id": "Enheds-id: {{id}}",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"livekit_server_info": "LiveKit Serverinfo",
|
"livekit_server_info": "LiveKit Serverinfo",
|
||||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||||
"matrix_id": "Matrix ID: {{id}}",
|
"matrix_id": "Matrix ID: {{id}}",
|
||||||
|
"mute_all_audio": "Slå al lyd fra (deltagere, reaktioner, deltagelseslyde)",
|
||||||
"show_connection_stats": "Vis forbindelsesstatistik",
|
"show_connection_stats": "Vis forbindelsesstatistik",
|
||||||
"show_non_member_tiles": "Vis fliser for medier fra ikke-medlemmer",
|
"show_non_member_tiles": "Vis fliser for medier fra ikke-medlemmer",
|
||||||
"url_params": "URL-parametre",
|
"url_params": "URL-parametre",
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Opkald er ikke understøttet",
|
"call_is_not_supported": "Opkald er ikke understøttet",
|
||||||
"call_not_found": "Opkald ikke fundet",
|
"call_not_found": "Opkald ikke fundet",
|
||||||
"call_not_found_description": "<0>Det link ser ikke ud til at høre til et eksisterende opkald. Tjek at du har det rigtige link, eller<1> opret et nyt</1>.</0>",
|
"call_not_found_description": "<0>Det link ser ikke ud til at høre til et eksisterende opkald. Tjek at du har det rigtige link, eller <2> opret et nyt</2>.</0>",
|
||||||
"connection_lost": "Forbindelsen gik tabt",
|
"connection_lost": "Forbindelsen gik tabt",
|
||||||
"connection_lost_description": "Du blev afbrudt fra opkaldet.",
|
"connection_lost_description": "Du blev afbrudt fra opkaldet.",
|
||||||
"e2ee_unsupported": "Inkompatibel browser",
|
"e2ee_unsupported": "Inkompatibel browser",
|
||||||
@@ -92,6 +94,8 @@
|
|||||||
"matrix_rtc_focus_missing": "Serveren er ikke konfigureret til at arbejde med {{brand}}{{domain}}. Kontakt venligst din serveradministrator (domæne:{{domain}}, fejlkode: {{ errorCode }}).",
|
"matrix_rtc_focus_missing": "Serveren er ikke konfigureret til at arbejde med {{brand}}{{domain}}. Kontakt venligst din serveradministrator (domæne:{{domain}}, fejlkode: {{ errorCode }}).",
|
||||||
"open_elsewhere": "Åbnet i en anden fane",
|
"open_elsewhere": "Åbnet i en anden fane",
|
||||||
"open_elsewhere_description": "{{brand}} er blevet åbnet i en anden fane. Hvis det ikke lyder rigtigt, kan du prøve at genindlæse siden.",
|
"open_elsewhere_description": "{{brand}} er blevet åbnet i en anden fane. Hvis det ikke lyder rigtigt, kan du prøve at genindlæse siden.",
|
||||||
|
"room_creation_restricted": "Kunne ikke oprette opkald",
|
||||||
|
"room_creation_restricted_description": "Oprettelse af opkald er muligvis begrænset til autoriserede brugere. Prøv igen senere, eller kontakt din serveradministrator, hvis problemet fortsætter.",
|
||||||
"unexpected_ec_error": "Der opstod en uventet fejl (<0>Fejlkode:</0> <1> {{ errorCode }}</1>). Kontakt venligst din serveradministrator."
|
"unexpected_ec_error": "Der opstod en uventet fejl (<0>Fejlkode:</0> <1> {{ errorCode }}</1>). Kontakt venligst din serveradministrator."
|
||||||
},
|
},
|
||||||
"group_call_loader": {
|
"group_call_loader": {
|
||||||
@@ -103,6 +107,11 @@
|
|||||||
"knock_reject_heading": "Adgang nægtet",
|
"knock_reject_heading": "Adgang nægtet",
|
||||||
"reason": "Årsag: {{reason}}"
|
"reason": "Årsag: {{reason}}"
|
||||||
},
|
},
|
||||||
|
"handset": {
|
||||||
|
"overlay_back_button": "Tilbage til højttalertilstand",
|
||||||
|
"overlay_description": "Virker kun, når du bruger appen",
|
||||||
|
"overlay_title": "Telefon-højtaler"
|
||||||
|
},
|
||||||
"hangup_button_label": "Afslut opkald",
|
"hangup_button_label": "Afslut opkald",
|
||||||
"header_label": "Element Ring hjem",
|
"header_label": "Element Ring hjem",
|
||||||
"header_participants_label": "Deltagere",
|
"header_participants_label": "Deltagere",
|
||||||
@@ -164,12 +173,18 @@
|
|||||||
"effect_volume_description": "Juster den lydstyrke som reaktioner og håndsoprækninger afspilles med.",
|
"effect_volume_description": "Juster den lydstyrke som reaktioner og håndsoprækninger afspilles med.",
|
||||||
"effect_volume_label": "Lydstyrke for lydeffekter"
|
"effect_volume_label": "Lydstyrke for lydeffekter"
|
||||||
},
|
},
|
||||||
|
"background_blur_header": "Baggrund",
|
||||||
|
"background_blur_label": "Gør videoens baggrund sløret",
|
||||||
|
"blur_not_supported_by_browser": "(Baggrundssløring understøttes ikke af denne enhed.)",
|
||||||
"developer_tab_title": "Udvikler",
|
"developer_tab_title": "Udvikler",
|
||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
"camera_numbered": "Kamera {{n}}",
|
"camera_numbered": "Kamera {{n}}",
|
||||||
|
"change_device_button": "Skift lydenhed",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"default_named": "Standard <2>({{name}})</2>",
|
"default_named": "Standard <2>({{name}})</2>",
|
||||||
|
"handset": "Telefon",
|
||||||
|
"loudspeaker": "Højttaler",
|
||||||
"microphone": "Mikrofon",
|
"microphone": "Mikrofon",
|
||||||
"microphone_numbered": "Mikrofon {{n}}",
|
"microphone_numbered": "Mikrofon {{n}}",
|
||||||
"speaker": "Højttaler",
|
"speaker": "Højttaler",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"reaction": "Reaktion",
|
"reaction": "Reaktion",
|
||||||
"reactions": "Reaktionen",
|
"reactions": "Reaktionen",
|
||||||
|
"reconnecting": "Verbindung wird wiederhergestellt...",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"unencrypted": "Nicht verschlüsselt",
|
"unencrypted": "Nicht verschlüsselt",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Anrufe werden nicht unterstützt",
|
"call_is_not_supported": "Anrufe werden nicht unterstützt",
|
||||||
"call_not_found": "Anruf nicht gefunden",
|
"call_not_found": "Anruf nicht gefunden",
|
||||||
"call_not_found_description": "<0>Dieser Link scheint zu keinem bestehenden Anruf zu gehören. Vergewissern Sie sich, dass Sie den richtigen Link haben, oder <1> erstellen Sie einen neuen</1>. </0>",
|
"call_not_found_description": "<0>Dieser Link scheint zu keinem bestehenden Anruf zu gehören. Es sollte geprüft werden, ob der Link korrekt ist, oder <2>ein neuer erstellt werden</2>.</0>",
|
||||||
"connection_lost": "Verbindung verloren",
|
"connection_lost": "Verbindung verloren",
|
||||||
"connection_lost_description": "Ihre Verbindung zum Anruf wurde unterbrochen.",
|
"connection_lost_description": "Ihre Verbindung zum Anruf wurde unterbrochen.",
|
||||||
"e2ee_unsupported": "Inkompatibler Browser",
|
"e2ee_unsupported": "Inkompatibler Browser",
|
||||||
@@ -94,6 +95,8 @@
|
|||||||
"matrix_rtc_focus_missing": "Der Server ist nicht für die Verwendung mit {{brand}} konfiguriert. Bitte den Serveradministrator kontaktieren (Domain: {{domain}}, Fehlercode: {{ errorCode }}).",
|
"matrix_rtc_focus_missing": "Der Server ist nicht für die Verwendung mit {{brand}} konfiguriert. Bitte den Serveradministrator kontaktieren (Domain: {{domain}}, Fehlercode: {{ errorCode }}).",
|
||||||
"open_elsewhere": "In einem anderen Tab geöffnet",
|
"open_elsewhere": "In einem anderen Tab geöffnet",
|
||||||
"open_elsewhere_description": "{{brand}} wurde in einem anderen Tab geöffnet. Wenn das nicht richtig klingt, versuchen Sie, die Seite neu zu laden.",
|
"open_elsewhere_description": "{{brand}} wurde in einem anderen Tab geöffnet. Wenn das nicht richtig klingt, versuchen Sie, die Seite neu zu laden.",
|
||||||
|
"room_creation_restricted": "Anruf konnte nicht erstellt werden",
|
||||||
|
"room_creation_restricted_description": "Das Erstellen von Anrufen ist nur für autorisierte Nutzer möglich. Versuche es später erneut oder kontaktiere deinen Serveradministrator, falls das Problem weiterhin besteht.",
|
||||||
"unexpected_ec_error": "Ein unerwarteter Fehler ist aufgetreten (<0>Fehlercode: </0> <1>{{ errorCode }}</1>). Bitte den Serveradministrator kontaktieren."
|
"unexpected_ec_error": "Ein unerwarteter Fehler ist aufgetreten (<0>Fehlercode: </0> <1>{{ errorCode }}</1>). Bitte den Serveradministrator kontaktieren."
|
||||||
},
|
},
|
||||||
"group_call_loader": {
|
"group_call_loader": {
|
||||||
@@ -105,6 +108,11 @@
|
|||||||
"knock_reject_heading": "Zugriff verweigert",
|
"knock_reject_heading": "Zugriff verweigert",
|
||||||
"reason": "Grund: {{reason}}"
|
"reason": "Grund: {{reason}}"
|
||||||
},
|
},
|
||||||
|
"handset": {
|
||||||
|
"overlay_back_button": "Zurück zum Lautsprechermodus",
|
||||||
|
"overlay_description": "Nur wenn App im Vordergrund nutzbar",
|
||||||
|
"overlay_title": "Ohrhörer Modus"
|
||||||
|
},
|
||||||
"hangup_button_label": "Anruf beenden",
|
"hangup_button_label": "Anruf beenden",
|
||||||
"header_label": "Element Call-Startseite",
|
"header_label": "Element Call-Startseite",
|
||||||
"header_participants_label": "Teilnehmende",
|
"header_participants_label": "Teilnehmende",
|
||||||
@@ -173,9 +181,11 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
"camera_numbered": "Kamera {{n}}",
|
"camera_numbered": "Kamera {{n}}",
|
||||||
|
"change_device_button": "Audiogerät wechseln",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"default_named": "Standard<2> ({{name}} )</2>",
|
"default_named": "Standard<2> ({{name}} )</2>",
|
||||||
"earpiece": "Ohrhörer",
|
"handset": "Ohrhörer",
|
||||||
|
"loudspeaker": "Lautsprecher",
|
||||||
"microphone": "Mikrofon",
|
"microphone": "Mikrofon",
|
||||||
"microphone_numbered": "Mikrofon{{n}}",
|
"microphone_numbered": "Mikrofon{{n}}",
|
||||||
"speaker": "Lautsprecher",
|
"speaker": "Lautsprecher",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"reaction": "Reaction",
|
"reaction": "Reaction",
|
||||||
"reactions": "Reactions",
|
"reactions": "Reactions",
|
||||||
|
"reconnecting": "Reconnecting…",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"unencrypted": "Not encrypted",
|
"unencrypted": "Not encrypted",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
@@ -71,18 +72,20 @@
|
|||||||
"livekit_server_info": "LiveKit Server Info",
|
"livekit_server_info": "LiveKit Server Info",
|
||||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||||
"matrix_id": "Matrix ID: {{id}}",
|
"matrix_id": "Matrix ID: {{id}}",
|
||||||
|
"multi_sfu": "Multi-SFU media transport",
|
||||||
"mute_all_audio": "Mute all audio (participants, reactions, join sounds)",
|
"mute_all_audio": "Mute all audio (participants, reactions, join sounds)",
|
||||||
|
"prefer_sticky_events": {
|
||||||
|
"description": "Improves reliability of calls (requires homeserver support)",
|
||||||
|
"label": "Prefer sticky events"
|
||||||
|
},
|
||||||
"show_connection_stats": "Show connection statistics",
|
"show_connection_stats": "Show connection statistics",
|
||||||
"show_non_member_tiles": "Show tiles for non-member media",
|
"url_params": "URL parameters"
|
||||||
"url_params": "URL parameters",
|
|
||||||
"use_new_membership_manager": "Use the new implementation of the call MembershipManager",
|
|
||||||
"use_to_device_key_transport": "Use to device key transport. This will fallback to room key transport when another call member sent a room key"
|
|
||||||
},
|
},
|
||||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Call is not supported",
|
"call_is_not_supported": "Call is not supported",
|
||||||
"call_not_found": "Call not found",
|
"call_not_found": "Call not found",
|
||||||
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one</1>.</0>",
|
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <2>create a new one</2>.</0>",
|
||||||
"connection_lost": "Connection lost",
|
"connection_lost": "Connection lost",
|
||||||
"connection_lost_description": "You were disconnected from the call.",
|
"connection_lost_description": "You were disconnected from the call.",
|
||||||
"e2ee_unsupported": "Incompatible browser",
|
"e2ee_unsupported": "Incompatible browser",
|
||||||
@@ -91,9 +94,11 @@
|
|||||||
"generic_description": "Submitting debug logs will help us track down the problem.",
|
"generic_description": "Submitting debug logs will help us track down the problem.",
|
||||||
"insufficient_capacity": "Insufficient capacity",
|
"insufficient_capacity": "Insufficient capacity",
|
||||||
"insufficient_capacity_description": "The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.",
|
"insufficient_capacity_description": "The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.",
|
||||||
"matrix_rtc_focus_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
|
"matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
|
||||||
"open_elsewhere": "Opened in another tab",
|
"open_elsewhere": "Opened in another tab",
|
||||||
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
|
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
|
||||||
|
"room_creation_restricted": "Failed to create call",
|
||||||
|
"room_creation_restricted_description": "Call creation might be restricted to authorized users only. Try again later, or contact your server admin if the problem persists.",
|
||||||
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server admin."
|
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server admin."
|
||||||
},
|
},
|
||||||
"group_call_loader": {
|
"group_call_loader": {
|
||||||
@@ -105,6 +110,11 @@
|
|||||||
"knock_reject_heading": "Access denied",
|
"knock_reject_heading": "Access denied",
|
||||||
"reason": "Reason: {{reason}}"
|
"reason": "Reason: {{reason}}"
|
||||||
},
|
},
|
||||||
|
"handset": {
|
||||||
|
"overlay_back_button": "Back to Speaker Mode",
|
||||||
|
"overlay_description": "Only works while using app",
|
||||||
|
"overlay_title": "Handset Mode"
|
||||||
|
},
|
||||||
"hangup_button_label": "End call",
|
"hangup_button_label": "End call",
|
||||||
"header_label": "Element Call Home",
|
"header_label": "Element Call Home",
|
||||||
"header_participants_label": "Participants",
|
"header_participants_label": "Participants",
|
||||||
@@ -173,9 +183,11 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"camera_numbered": "Camera {{n}}",
|
"camera_numbered": "Camera {{n}}",
|
||||||
|
"change_device_button": "Change audio device",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"default_named": "Default <2>({{name}})</2>",
|
"default_named": "Default <2>({{name}})</2>",
|
||||||
"earpiece": "Earpiece",
|
"handset": "Handset",
|
||||||
|
"loudspeaker": "Loudspeaker",
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
"microphone_numbered": "Microphone {{n}}",
|
"microphone_numbered": "Microphone {{n}}",
|
||||||
"speaker": "Speaker",
|
"speaker": "Speaker",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"developer_mode": {
|
"developer_mode": {
|
||||||
|
"always_show_iphone_earpiece": "Näita iPhone'i kuulari valikut kõikidel platvormidel",
|
||||||
"crypto_version": "Krüptoteekide versioon: {{version}}",
|
"crypto_version": "Krüptoteekide versioon: {{version}}",
|
||||||
"debug_tile_layout_label": "Meediapaanide paigutus",
|
"debug_tile_layout_label": "Meediapaanide paigutus",
|
||||||
"device_id": "Seadme tunnus: {{id}}",
|
"device_id": "Seadme tunnus: {{id}}",
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Kõne pole toetatud",
|
"call_is_not_supported": "Kõne pole toetatud",
|
||||||
"call_not_found": "Kõnet ei leidu",
|
"call_not_found": "Kõnet ei leidu",
|
||||||
"call_not_found_description": "<0>See link ei tundu olema seotud ühegi olemasoleva kõnega. Kontrolli, et sul on õige link või <1>loo uus</1>.</0>",
|
"call_not_found_description": "<0>See link ei tundu olema seotud ühegi olemasoleva kõnega. Kontrolli, et sul on õige link või <2>loo uus</2>.</0>",
|
||||||
"connection_lost": "Ühendus on katkenud",
|
"connection_lost": "Ühendus on katkenud",
|
||||||
"connection_lost_description": "Sinu ühendus selle kõnega on katkenud.",
|
"connection_lost_description": "Sinu ühendus selle kõnega on katkenud.",
|
||||||
"e2ee_unsupported": "Mitteühilduv brauser",
|
"e2ee_unsupported": "Mitteühilduv brauser",
|
||||||
@@ -93,6 +94,8 @@
|
|||||||
"matrix_rtc_focus_missing": "See server pole seadistatud töötama rakendusega {{brand}}. Palun võta ühendust serveri halduriga (domeen: {{domain}}, veakood: {{ errorCode }}).",
|
"matrix_rtc_focus_missing": "See server pole seadistatud töötama rakendusega {{brand}}. Palun võta ühendust serveri halduriga (domeen: {{domain}}, veakood: {{ errorCode }}).",
|
||||||
"open_elsewhere": "Avatud teisel vahekaardil",
|
"open_elsewhere": "Avatud teisel vahekaardil",
|
||||||
"open_elsewhere_description": "{{brand}} on avatud teisel vahekaardil. Kui see ei tundu olema õige, proovi selle lehe uuesti laadimist.",
|
"open_elsewhere_description": "{{brand}} on avatud teisel vahekaardil. Kui see ei tundu olema õige, proovi selle lehe uuesti laadimist.",
|
||||||
|
"room_creation_restricted": "Kõne loomine ei õnnestunud",
|
||||||
|
"room_creation_restricted_description": "Kõne loomine võib olla lubatud ainult volitatud kasutajatele. Proovi hiljem uuesti või probleemi püsimisel võta ühendust oma serveri haldajaga.",
|
||||||
"unexpected_ec_error": "Tekkis ootamatu viga (<0>Veakood:</0> <1>{{ errorCode }}</1>). Palun võta ühendust serveri haldajaga."
|
"unexpected_ec_error": "Tekkis ootamatu viga (<0>Veakood:</0> <1>{{ errorCode }}</1>). Palun võta ühendust serveri haldajaga."
|
||||||
},
|
},
|
||||||
"group_call_loader": {
|
"group_call_loader": {
|
||||||
@@ -104,6 +107,10 @@
|
|||||||
"knock_reject_heading": "Liitumine pole lubatud",
|
"knock_reject_heading": "Liitumine pole lubatud",
|
||||||
"reason": "Põhjus"
|
"reason": "Põhjus"
|
||||||
},
|
},
|
||||||
|
"handset": {
|
||||||
|
"overlay_back_button": "Tagasi esineja vaatesse",
|
||||||
|
"overlay_description": "See toimib vaid rakenduse kasutamise ajal"
|
||||||
|
},
|
||||||
"hangup_button_label": "Lõpeta kõne",
|
"hangup_button_label": "Lõpeta kõne",
|
||||||
"header_label": "Avaleht: Element Call",
|
"header_label": "Avaleht: Element Call",
|
||||||
"header_participants_label": "Osalejad",
|
"header_participants_label": "Osalejad",
|
||||||
@@ -172,8 +179,10 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Kaamera",
|
"camera": "Kaamera",
|
||||||
"camera_numbered": "Kaamera {{n}}",
|
"camera_numbered": "Kaamera {{n}}",
|
||||||
|
"change_device_button": "Muuda heliseadet",
|
||||||
"default": "Vaikimisi",
|
"default": "Vaikimisi",
|
||||||
"default_named": "Vaikimisi <2>({{name}})</2>",
|
"default_named": "Vaikimisi <2>({{name}})</2>",
|
||||||
|
"loudspeaker": "Valjuhääldi",
|
||||||
"microphone": "Mikrofon",
|
"microphone": "Mikrofon",
|
||||||
"microphone_numbered": "Mikrofon {{n}}",
|
"microphone_numbered": "Mikrofon {{n}}",
|
||||||
"speaker": "Kõlar",
|
"speaker": "Kõlar",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"video": "Видео"
|
"video": "Видео"
|
||||||
},
|
},
|
||||||
"developer_mode": {
|
"developer_mode": {
|
||||||
|
"always_show_iphone_earpiece": "Показать опцию наушников для iPhone на всех платформах",
|
||||||
"crypto_version": "Версия криптографии: {{version}}",
|
"crypto_version": "Версия криптографии: {{version}}",
|
||||||
"debug_tile_layout_label": "Отладка расположения плиток",
|
"debug_tile_layout_label": "Отладка расположения плиток",
|
||||||
"device_id": "Идентификатор устройства: {{id}}",
|
"device_id": "Идентификатор устройства: {{id}}",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"livekit_server_info": "Информация о сервере LiveKit",
|
"livekit_server_info": "Информация о сервере LiveKit",
|
||||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||||
"matrix_id": "Matrix ID: {{id}}",
|
"matrix_id": "Matrix ID: {{id}}",
|
||||||
|
"mute_all_audio": "Отключить все звуки (участников, реакции, звуки присоединения)",
|
||||||
"show_connection_stats": "Показать статистику подключений",
|
"show_connection_stats": "Показать статистику подключений",
|
||||||
"show_non_member_tiles": "Показать плитки для медиафайлов, не являющихся участниками",
|
"show_non_member_tiles": "Показать плитки для медиафайлов, не являющихся участниками",
|
||||||
"url_params": "Параметры URL-адреса",
|
"url_params": "Параметры URL-адреса",
|
||||||
@@ -172,6 +174,7 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Камера",
|
"camera": "Камера",
|
||||||
"camera_numbered": "Камера {{n}}",
|
"camera_numbered": "Камера {{n}}",
|
||||||
|
"change_device_button": "Изменить аудиоустройство",
|
||||||
"default": "По умолчанию",
|
"default": "По умолчанию",
|
||||||
"default_named": "По умолчанию <2>({{name}})</2>",
|
"default_named": "По умолчанию <2>({{name}})</2>",
|
||||||
"microphone": "Микрофон",
|
"microphone": "Микрофон",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"developer_mode": {
|
"developer_mode": {
|
||||||
|
"always_show_iphone_earpiece": "Zobraziť možnosť slúchadla iPhone na všetkých platformách",
|
||||||
"crypto_version": "Krypto verzia: {{version}}",
|
"crypto_version": "Krypto verzia: {{version}}",
|
||||||
"debug_tile_layout_label": "Ladenie rozloženia dlaždíc",
|
"debug_tile_layout_label": "Ladenie rozloženia dlaždíc",
|
||||||
"device_id": "ID zariadenia: {{id}}",
|
"device_id": "ID zariadenia: {{id}}",
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Hovor nie je podporovaný",
|
"call_is_not_supported": "Hovor nie je podporovaný",
|
||||||
"call_not_found": "Hovor nebol nájdený",
|
"call_not_found": "Hovor nebol nájdený",
|
||||||
"call_not_found_description": "<0>Zdá sa, že tento odkaz nepatrí k žiadnemu existujúcemu hovoru. Skontrolujte, či máte správny odkaz, alebo <1> vytvorte nový</1>. </0>",
|
"call_not_found_description": "<0>Zdá sa, že tento odkaz nepatrí k žiadnemu existujúcemu hovoru. Skontrolujte, či máte správny odkaz, alebo <2>vytvorte nový</2>.</0>",
|
||||||
"connection_lost": "Strata spojenia",
|
"connection_lost": "Strata spojenia",
|
||||||
"connection_lost_description": "Boli ste odpojení od hovoru.",
|
"connection_lost_description": "Boli ste odpojení od hovoru.",
|
||||||
"e2ee_unsupported": "Nekompatibilný prehliadač",
|
"e2ee_unsupported": "Nekompatibilný prehliadač",
|
||||||
@@ -173,8 +174,10 @@
|
|||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
"camera_numbered": "Kamera {{n}}",
|
"camera_numbered": "Kamera {{n}}",
|
||||||
|
"change_device_button": "Zmeniť zvukové zariadenie",
|
||||||
"default": "Predvolené",
|
"default": "Predvolené",
|
||||||
"default_named": "Predvolené <2>({{name}})</2>",
|
"default_named": "Predvolené <2>({{name}})</2>",
|
||||||
|
"loudspeaker": "Reproduktor",
|
||||||
"microphone": "Mikrofón",
|
"microphone": "Mikrofón",
|
||||||
"microphone_numbered": "Mikrofón {{n}}",
|
"microphone_numbered": "Mikrofón {{n}}",
|
||||||
"speaker": "Reproduktor",
|
"speaker": "Reproduktor",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"developer_mode": {
|
"developer_mode": {
|
||||||
|
"always_show_iphone_earpiece": "Visa iPhone-hörsnäckealternativ på alla plattformar",
|
||||||
"crypto_version": "Kryptoversion: {{version}}",
|
"crypto_version": "Kryptoversion: {{version}}",
|
||||||
"debug_tile_layout_label": "Felsök panelarrangemang",
|
"debug_tile_layout_label": "Felsök panelarrangemang",
|
||||||
"device_id": "Enhets-ID: {{id}}",
|
"device_id": "Enhets-ID: {{id}}",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"livekit_server_info": "LiveKit-serverinfo",
|
"livekit_server_info": "LiveKit-serverinfo",
|
||||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||||
"matrix_id": "Matrix-ID: {{id}}",
|
"matrix_id": "Matrix-ID: {{id}}",
|
||||||
|
"mute_all_audio": "Tysta allt ljud (deltagare, reaktioner, anslutningsljud)",
|
||||||
"show_connection_stats": "Visa anslutningsstatistik",
|
"show_connection_stats": "Visa anslutningsstatistik",
|
||||||
"show_non_member_tiles": "Visa paneler för media som inte är medlemmar",
|
"show_non_member_tiles": "Visa paneler för media som inte är medlemmar",
|
||||||
"url_params": "URL-parametrar",
|
"url_params": "URL-parametrar",
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Call stöds inte",
|
"call_is_not_supported": "Call stöds inte",
|
||||||
"call_not_found": "Samtal hittades inte",
|
"call_not_found": "Samtal hittades inte",
|
||||||
"call_not_found_description": "<0>Den länken verkar inte tillhöra något befintligt samtal. Kontrollera att du har rätt länk, eller <1>skapa en ny</1>.</0>",
|
"call_not_found_description": "<0>Den länken verkar inte tillhöra något befintligt samtal. Kontrollera att du har rätt länk, eller <2>skapa en ny</2>.</0>",
|
||||||
"connection_lost": "Anslutning förlorad",
|
"connection_lost": "Anslutning förlorad",
|
||||||
"connection_lost_description": "Du kopplades bort från samtalet.",
|
"connection_lost_description": "Du kopplades bort från samtalet.",
|
||||||
"e2ee_unsupported": "Inkompatibel webbläsare",
|
"e2ee_unsupported": "Inkompatibel webbläsare",
|
||||||
@@ -92,6 +94,8 @@
|
|||||||
"matrix_rtc_focus_missing": "Servern är inte konfigurerad för att fungera med {{brand}}. Vänligen kontakta serveradministratören (Domän: {{domain}}, Felkod: {{ errorCode }}).",
|
"matrix_rtc_focus_missing": "Servern är inte konfigurerad för att fungera med {{brand}}. Vänligen kontakta serveradministratören (Domän: {{domain}}, Felkod: {{ errorCode }}).",
|
||||||
"open_elsewhere": "Öppnades i en annan flik",
|
"open_elsewhere": "Öppnades i en annan flik",
|
||||||
"open_elsewhere_description": "{{brand}} har öppnats i en annan flik. Om det inte låter rätt, pröva att ladda om sidan.",
|
"open_elsewhere_description": "{{brand}} har öppnats i en annan flik. Om det inte låter rätt, pröva att ladda om sidan.",
|
||||||
|
"room_creation_restricted": "Misslyckades att skapa samtal",
|
||||||
|
"room_creation_restricted_description": "Samtalsskapande kan vara begränsat till endast behöriga användare. Försök igen senare eller kontakta serveradministratören om problemet kvarstår.",
|
||||||
"unexpected_ec_error": "Ett oväntat fel inträffade (<0>Felkod:</0> <1>{{ errorCode }}</1>). Kontakta din serveradministratör."
|
"unexpected_ec_error": "Ett oväntat fel inträffade (<0>Felkod:</0> <1>{{ errorCode }}</1>). Kontakta din serveradministratör."
|
||||||
},
|
},
|
||||||
"group_call_loader": {
|
"group_call_loader": {
|
||||||
@@ -103,6 +107,11 @@
|
|||||||
"knock_reject_heading": "Inte tillåten att gå med",
|
"knock_reject_heading": "Inte tillåten att gå med",
|
||||||
"reason": "Anledning"
|
"reason": "Anledning"
|
||||||
},
|
},
|
||||||
|
"handset": {
|
||||||
|
"overlay_back_button": "Tillbaka till högtalarläge",
|
||||||
|
"overlay_description": "Fungerar bara när appen används",
|
||||||
|
"overlay_title": "Telefonläge"
|
||||||
|
},
|
||||||
"hangup_button_label": "Avsluta samtal",
|
"hangup_button_label": "Avsluta samtal",
|
||||||
"header_label": "Element Call Hem",
|
"header_label": "Element Call Hem",
|
||||||
"header_participants_label": "Deltagare",
|
"header_participants_label": "Deltagare",
|
||||||
@@ -164,12 +173,18 @@
|
|||||||
"effect_volume_description": "Justera volymen vid vilken reaktioner och handuppräckningseffekter spelas",
|
"effect_volume_description": "Justera volymen vid vilken reaktioner och handuppräckningseffekter spelas",
|
||||||
"effect_volume_label": "Ljudeffektsvolym"
|
"effect_volume_label": "Ljudeffektsvolym"
|
||||||
},
|
},
|
||||||
|
"background_blur_header": "Bakgrund",
|
||||||
|
"background_blur_label": "Gör bakgrunden i videon suddig",
|
||||||
|
"blur_not_supported_by_browser": "(Bakgrundssuddighet stöds inte av den här enheten.)",
|
||||||
"developer_tab_title": "Utvecklare",
|
"developer_tab_title": "Utvecklare",
|
||||||
"devices": {
|
"devices": {
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
"camera_numbered": "Kamera {{n}}",
|
"camera_numbered": "Kamera {{n}}",
|
||||||
|
"change_device_button": "Byt ljudenhet",
|
||||||
"default": "Förval",
|
"default": "Förval",
|
||||||
"default_named": "Förval <2>({{name}})</2>",
|
"default_named": "Förval <2>({{name}})</2>",
|
||||||
|
"handset": "Telefonlur",
|
||||||
|
"loudspeaker": "Högtalare",
|
||||||
"microphone": "Mikrofon",
|
"microphone": "Mikrofon",
|
||||||
"microphone_numbered": "Mikrofon {{n}}",
|
"microphone_numbered": "Mikrofon {{n}}",
|
||||||
"speaker": "Högtalare",
|
"speaker": "Högtalare",
|
||||||
|
|||||||
46
package.json
@@ -8,7 +8,11 @@
|
|||||||
"dev:embedded": "vite --config vite-embedded.config.js",
|
"dev:embedded": "vite --config vite-embedded.config.js",
|
||||||
"build": "yarn build:full",
|
"build": "yarn build:full",
|
||||||
"build:full": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
|
"build:full": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
|
||||||
|
"build:full:production": "yarn build:full",
|
||||||
|
"build:full:development": "yarn build:full --mode development",
|
||||||
"build:embedded": "yarn build:full --config vite-embedded.config.js",
|
"build:embedded": "yarn build:full --config vite-embedded.config.js",
|
||||||
|
"build:embedded:production": "yarn build:embedded",
|
||||||
|
"build:embedded:development": "yarn build:embedded --mode development",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"prettier:check": "prettier -c .",
|
"prettier:check": "prettier -c .",
|
||||||
"prettier:format": "prettier -w .",
|
"prettier:format": "prettier -w .",
|
||||||
@@ -22,6 +26,7 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"backend": "docker-compose -f dev-backend-docker-compose.yml up",
|
"backend": "docker-compose -f dev-backend-docker-compose.yml up",
|
||||||
|
"backend-playwright": "docker-compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up",
|
||||||
"test:playwright": "playwright test",
|
"test:playwright": "playwright test",
|
||||||
"test:playwright:open": "yarn test:playwright --ui",
|
"test:playwright:open": "yarn test:playwright --ui",
|
||||||
"links:enable": "mv .links.disabled.yaml .links.yaml & touch .links.yaml",
|
"links:enable": "mv .links.disabled.yaml .links.yaml & touch .links.yaml",
|
||||||
@@ -39,21 +44,21 @@
|
|||||||
"@formatjs/intl-segmenter": "^11.7.3",
|
"@formatjs/intl-segmenter": "^11.7.3",
|
||||||
"@livekit/components-core": "^0.12.0",
|
"@livekit/components-core": "^0.12.0",
|
||||||
"@livekit/components-react": "^2.0.0",
|
"@livekit/components-react": "^2.0.0",
|
||||||
"@livekit/protocol": "^1.38.0",
|
"@livekit/protocol": "^1.42.2",
|
||||||
"@livekit/track-processors": "^0.5.5",
|
"@livekit/track-processors": "^0.5.5",
|
||||||
"@mediapipe/tasks-vision": "^0.10.18",
|
"@mediapipe/tasks-vision": "^0.10.18",
|
||||||
"@opentelemetry/api": "^1.4.0",
|
"@opentelemetry/api": "^1.4.0",
|
||||||
"@opentelemetry/core": "^1.25.1",
|
"@opentelemetry/core": "^2.0.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
||||||
"@opentelemetry/resources": "^1.25.1",
|
"@opentelemetry/resources": "^2.0.0",
|
||||||
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||||
"@opentelemetry/semantic-conventions": "^1.25.1",
|
"@opentelemetry/semantic-conventions": "^1.25.1",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.56.1",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||||
"@react-spring/web": "^9.4.4",
|
"@react-spring/web": "^10.0.0",
|
||||||
"@sentry/react": "^8.0.0",
|
"@sentry/react": "^8.0.0",
|
||||||
"@sentry/vite-plugin": "^3.0.0",
|
"@sentry/vite-plugin": "^3.0.0",
|
||||||
"@stylistic/eslint-plugin": "^3.0.0",
|
"@stylistic/eslint-plugin": "^3.0.0",
|
||||||
@@ -69,40 +74,42 @@
|
|||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/pako": "^2.0.3",
|
"@types/pako": "^2.0.3",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@types/sdp-transform": "^2.4.5",
|
"@types/sdp-transform": "^2.4.5",
|
||||||
"@types/uuid": "10",
|
"@types/uuid": "10",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||||
"@typescript-eslint/parser": "^8.31.0",
|
"@typescript-eslint/parser": "^8.31.0",
|
||||||
"@use-gesture/react": "^10.2.11",
|
"@use-gesture/react": "^10.2.11",
|
||||||
"@vector-im/compound-design-tokens": "^3.0.0",
|
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||||
"@vector-im/compound-web": "^7.2.0",
|
"@vector-im/compound-web": "^8.0.0",
|
||||||
"@vitejs/plugin-react": "^4.0.1",
|
"@vitejs/plugin-react": "^4.0.1",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"eslint": "^8.14.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-deprecate": "^0.8.2",
|
"eslint-plugin-deprecate": "^0.8.2",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-matrix-org": "^2.0.0",
|
"eslint-plugin-matrix-org": "2.1.0",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-rxjs": "^5.0.3",
|
"eslint-plugin-rxjs": "^5.0.3",
|
||||||
"eslint-plugin-unicorn": "^56.0.0",
|
"eslint-plugin-unicorn": "^56.0.0",
|
||||||
|
"fetch-mock": "11.1.5",
|
||||||
"global-jsdom": "^26.0.0",
|
"global-jsdom": "^26.0.0",
|
||||||
"i18next": "^24.0.0",
|
"i18next": "^24.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-parser": "^9.1.0",
|
"i18next-parser": "^9.1.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"knip": "^5.27.2",
|
"knip": "^5.27.2",
|
||||||
"livekit-client": "^2.11.3",
|
"livekit-client": "^2.13.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"loglevel": "^1.9.1",
|
"loglevel": "^1.9.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21",
|
||||||
"matrix-widget-api": "^1.13.0",
|
"matrix-widget-api": "^1.13.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"observable-hooks": "^4.2.3",
|
"observable-hooks": "^4.2.3",
|
||||||
@@ -112,11 +119,10 @@
|
|||||||
"posthog-js": "1.160.3",
|
"posthog-js": "1.160.3",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "18",
|
"react": "19",
|
||||||
"react-dom": "18",
|
"react-dom": "19",
|
||||||
"react-i18next": "^15.0.0",
|
"react-i18next": "^15.0.0",
|
||||||
"react-router-dom": "^7.0.0",
|
"react-router-dom": "^7.0.0",
|
||||||
"react-use-clipboard": "^1.0.7",
|
|
||||||
"react-use-measure": "^2.1.1",
|
"react-use-measure": "^2.1.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
@@ -124,8 +130,8 @@
|
|||||||
"typescript-eslint-language-service": "^5.0.5",
|
"typescript-eslint-language-service": "^5.0.5",
|
||||||
"unique-names-generator": "^4.6.0",
|
"unique-names-generator": "^4.6.0",
|
||||||
"vaul": "^1.0.0",
|
"vaul": "^1.0.0",
|
||||||
"vite": "^6.0.0",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-generate-file": "^0.2.0",
|
"vite-plugin-generate-file": "^0.3.0",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"vite-plugin-svgr": "^4.0.0",
|
"vite-plugin-svgr": "^4.0.0",
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ services:
|
|||||||
synapse:
|
synapse:
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
|
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
|
||||||
|
synapse-1:
|
||||||
|
volumes:
|
||||||
|
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const baseURL = process.env.USE_DOCKER
|
||||||
|
? "http://localhost:8080"
|
||||||
|
: "https://localhost:3000";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +29,7 @@ export default defineConfig({
|
|||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
baseURL: "https://localhost:3000",
|
baseURL,
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
@@ -73,9 +77,13 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: {
|
||||||
command: "yarn dev",
|
command: "./scripts/playwright-webserver-command.sh",
|
||||||
url: "https://localhost:3000",
|
url: baseURL,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
|
gracefulShutdown: {
|
||||||
|
signal: "SIGTERM",
|
||||||
|
timeout: 500,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ test("Start a new call then leave and show the feedback screen", async ({
|
|||||||
// The tooltip with the name should be visible
|
// The tooltip with the name should be visible
|
||||||
await expect(page.getByTestId("name_tag")).toContainText("John Doe");
|
await expect(page.getByTestId("name_tag")).toContainText("John Doe");
|
||||||
|
|
||||||
|
// Resize the window to resemble a small mobile phone
|
||||||
|
await page.setViewportSize({ width: 350, height: 660 });
|
||||||
|
// We should still be able to send reactions at this screen size
|
||||||
|
await expect(page.getByRole("button", { name: "Reactions" })).toBeVisible();
|
||||||
|
|
||||||
// leave the call
|
// leave the call
|
||||||
await page.getByTestId("incall_leave").click();
|
await page.getByTestId("incall_leave").click();
|
||||||
await expect(page.getByRole("heading")).toContainText(
|
await expect(page.getByRole("heading")).toContainText(
|
||||||
|
|||||||
@@ -72,3 +72,56 @@ test("Should automatically retry non fatal JWT errors", async ({
|
|||||||
await hasRetriedPromise;
|
await hasRetriedPromise;
|
||||||
await expect(page.getByTestId("video").first()).toBeVisible();
|
await expect(page.getByTestId("video").first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Should show error screen if call creation is restricted", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// We need the socket connection to fail, but this cannot be done by using the websocket route.
|
||||||
|
// Instead, we will trick the app by returning a bad URL for the SFU that will not be reachable an error out.
|
||||||
|
await page.route(
|
||||||
|
"**/matrix-rtc.m.localhost/livekit/jwt/sfu/get",
|
||||||
|
async (route) =>
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: "wss://badurltotricktest/livekit/sfu",
|
||||||
|
jwt: "FAKE",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then if the socket connection fails, livekit will try to validate the token!
|
||||||
|
// Livekit will not auto_create anymore and will return a 404 error.
|
||||||
|
await page.route(
|
||||||
|
"**/badurltotricktest/livekit/sfu/rtc/validate?**",
|
||||||
|
async (route) =>
|
||||||
|
await route.fulfill({
|
||||||
|
status: 404,
|
||||||
|
contentType: "text/plain",
|
||||||
|
body: "requested room does not exist",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.pause();
|
||||||
|
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("HelloCall");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("John Doe");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
|
||||||
|
await page.pause();
|
||||||
|
// Should fail
|
||||||
|
await expect(page.getByText("Failed to create call")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByText(
|
||||||
|
/Call creation might be restricted to authorized users only/,
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Page, test, expect, type JSHandle } from "@playwright/test";
|
import {
|
||||||
|
type Browser,
|
||||||
|
type Page,
|
||||||
|
test,
|
||||||
|
expect,
|
||||||
|
type JSHandle,
|
||||||
|
} from "@playwright/test";
|
||||||
|
|
||||||
import type { MatrixClient } from "matrix-js-sdk";
|
import type { MatrixClient } from "matrix-js-sdk";
|
||||||
|
|
||||||
@@ -30,8 +36,8 @@ const PASSWORD = "foobarbaz1!";
|
|||||||
const CONFIG_JSON = {
|
const CONFIG_JSON = {
|
||||||
default_server_config: {
|
default_server_config: {
|
||||||
"m.homeserver": {
|
"m.homeserver": {
|
||||||
base_url: "http://synapse.localhost:8008",
|
base_url: "https://synapse.m.localhost",
|
||||||
server_name: "synapse.localhost",
|
server_name: "synapse.m.localhost",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -63,15 +69,72 @@ const CONFIG_JSON = {
|
|||||||
* Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`.
|
* Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`.
|
||||||
* @param page
|
* @param page
|
||||||
*/
|
*/
|
||||||
async function setDevToolElementCallDevUrl(page: Page): Promise<void> {
|
const setDevToolElementCallDevUrl = process.env.USE_DOCKER
|
||||||
await page.evaluate(() => {
|
? async (page: Page): Promise<void> => {
|
||||||
window.mxSettingsStore.setValue(
|
await page.evaluate(() => {
|
||||||
"Developer.elementCallUrl",
|
window.mxSettingsStore.setValue(
|
||||||
null,
|
"Developer.elementCallUrl",
|
||||||
"device",
|
null,
|
||||||
"https://localhost:3000/room",
|
"device",
|
||||||
);
|
"http://localhost:8080/room",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: async (page: Page): Promise<void> => {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.mxSettingsStore.setValue(
|
||||||
|
"Developer.elementCallUrl",
|
||||||
|
null,
|
||||||
|
"device",
|
||||||
|
"https://localhost:3000/room",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new user and returns page, clientHandle and mxId.
|
||||||
|
*/
|
||||||
|
async function registerUser(
|
||||||
|
browser: Browser,
|
||||||
|
username: string,
|
||||||
|
): Promise<{ page: Page; clientHandle: JSHandle<MatrixClient>; mxId: string }> {
|
||||||
|
const userContext = await browser.newContext({
|
||||||
|
reducedMotion: "reduce",
|
||||||
});
|
});
|
||||||
|
const page = await userContext.newPage();
|
||||||
|
await page.goto("http://localhost:8081/#/welcome");
|
||||||
|
await page.getByRole("link", { name: "Create Account" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "Password", exact: true })
|
||||||
|
.fill(PASSWORD);
|
||||||
|
await page.getByRole("textbox", { name: "Confirm password" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Confirm password" }).fill(PASSWORD);
|
||||||
|
await page.getByRole("button", { name: "Register" }).click();
|
||||||
|
const continueButton = page.getByRole("button", { name: "Continue" });
|
||||||
|
try {
|
||||||
|
await expect(continueButton).toBeVisible({ timeout: 5000 });
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "Password", exact: true })
|
||||||
|
.fill(PASSWORD);
|
||||||
|
await continueButton.click();
|
||||||
|
} catch {
|
||||||
|
// continueButton not visible, continue as normal
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: `Welcome ${username}` }),
|
||||||
|
).toBeVisible();
|
||||||
|
await setDevToolElementCallDevUrl(page);
|
||||||
|
|
||||||
|
const clientHandle = await page.evaluateHandle(() =>
|
||||||
|
window.mxMatrixClientPeg.get(),
|
||||||
|
);
|
||||||
|
const mxId = (await clientHandle.evaluate(
|
||||||
|
(cli: MatrixClient) => cli.getUserId(),
|
||||||
|
clientHandle,
|
||||||
|
))!;
|
||||||
|
|
||||||
|
return { page, clientHandle, mxId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgetTest = test.extend<MyFixtures>({
|
export const widgetTest = test.extend<MyFixtures>({
|
||||||
@@ -83,65 +146,21 @@ export const widgetTest = test.extend<MyFixtures>({
|
|||||||
const userA = `brooks_${Date.now()}`;
|
const userA = `brooks_${Date.now()}`;
|
||||||
const userB = `whistler_${Date.now()}`;
|
const userB = `whistler_${Date.now()}`;
|
||||||
|
|
||||||
const user1Context = await browser.newContext({
|
// Register users
|
||||||
reducedMotion: "reduce",
|
const {
|
||||||
});
|
page: ewPage1,
|
||||||
const ewPage1 = await user1Context.newPage();
|
clientHandle: brooksClientHandle,
|
||||||
// Register the first user
|
mxId: brooksMxId,
|
||||||
await ewPage1.goto("http://localhost:8081/#/welcome");
|
} = await registerUser(browser, userA);
|
||||||
await ewPage1.getByRole("link", { name: "Create Account" }).click();
|
const {
|
||||||
await ewPage1.getByRole("textbox", { name: "Username" }).fill(userA);
|
page: ewPage2,
|
||||||
await ewPage1
|
clientHandle: whistlerClientHandle,
|
||||||
.getByRole("textbox", { name: "Password", exact: true })
|
mxId: whistlerMxId,
|
||||||
.fill(PASSWORD);
|
} = await registerUser(browser, userB);
|
||||||
await ewPage1.getByRole("textbox", { name: "Confirm password" }).click();
|
|
||||||
await ewPage1
|
|
||||||
.getByRole("textbox", { name: "Confirm password" })
|
|
||||||
.fill(PASSWORD);
|
|
||||||
await ewPage1.getByRole("button", { name: "Register" }).click();
|
|
||||||
await expect(
|
|
||||||
ewPage1.getByRole("heading", { name: `Welcome ${userA}` }),
|
|
||||||
).toBeVisible();
|
|
||||||
await setDevToolElementCallDevUrl(ewPage1);
|
|
||||||
|
|
||||||
const brooksClientHandle = await ewPage1.evaluateHandle(() =>
|
|
||||||
window.mxMatrixClientPeg.get(),
|
|
||||||
);
|
|
||||||
const brooksMxId = (await brooksClientHandle.evaluate((cli) => {
|
|
||||||
return cli.getUserId();
|
|
||||||
}, brooksClientHandle))!;
|
|
||||||
|
|
||||||
const user2Context = await browser.newContext({
|
|
||||||
reducedMotion: "reduce",
|
|
||||||
});
|
|
||||||
const ewPage2 = await user2Context.newPage();
|
|
||||||
// Register the second user
|
|
||||||
await ewPage2.goto("http://localhost:8081/#/welcome");
|
|
||||||
await ewPage2.getByRole("link", { name: "Create Account" }).click();
|
|
||||||
await ewPage2.getByRole("textbox", { name: "Username" }).fill(userB);
|
|
||||||
await ewPage2
|
|
||||||
.getByRole("textbox", { name: "Password", exact: true })
|
|
||||||
.fill(PASSWORD);
|
|
||||||
await ewPage2.getByRole("textbox", { name: "Confirm password" }).click();
|
|
||||||
await ewPage2
|
|
||||||
.getByRole("textbox", { name: "Confirm password" })
|
|
||||||
.fill(PASSWORD);
|
|
||||||
await ewPage2.getByRole("button", { name: "Register" }).click();
|
|
||||||
await expect(
|
|
||||||
ewPage2.getByRole("heading", { name: `Welcome ${userB}` }),
|
|
||||||
).toBeVisible();
|
|
||||||
await setDevToolElementCallDevUrl(ewPage2);
|
|
||||||
|
|
||||||
const whistlerClientHandle = await ewPage2.evaluateHandle(() =>
|
|
||||||
window.mxMatrixClientPeg.get(),
|
|
||||||
);
|
|
||||||
const whistlerMxId = (await whistlerClientHandle.evaluate((cli) => {
|
|
||||||
return cli.getUserId();
|
|
||||||
}, whistlerClientHandle))!;
|
|
||||||
|
|
||||||
// Invite the second user
|
// Invite the second user
|
||||||
await ewPage1.getByRole("button", { name: "Add room" }).click();
|
await ewPage1.getByRole("button", { name: "Add", exact: true }).click();
|
||||||
await ewPage1.getByText("New room").click();
|
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
|
||||||
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
|
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
|
||||||
await ewPage1.getByRole("button", { name: "Create room" }).click();
|
await ewPage1.getByRole("button", { name: "Create room" }).click();
|
||||||
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
|
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
|
||||||
@@ -165,9 +184,9 @@ export const widgetTest = test.extend<MyFixtures>({
|
|||||||
|
|
||||||
// Accept the invite
|
// Accept the invite
|
||||||
await expect(
|
await expect(
|
||||||
ewPage2.getByRole("treeitem", { name: "Welcome Room" }),
|
ewPage2.getByRole("option", { name: "Welcome Room" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await ewPage2.getByRole("treeitem", { name: "Welcome Room" }).click();
|
await ewPage2.getByRole("option", { name: "Welcome Room" }).click();
|
||||||
await ewPage2.getByRole("button", { name: "Accept" }).click();
|
await ewPage2.getByRole("button", { name: "Accept" }).click();
|
||||||
await expect(
|
await expect(
|
||||||
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
|
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
|
||||||
|
|||||||
60
playwright/reconnect.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
// Skip test for Firefox, due to page.keyboard.press("Tab") not reliable on headless mode
|
||||||
|
test.skip(
|
||||||
|
({ browserName }) => browserName === "firefox",
|
||||||
|
'This test is not working on firefox, page.keyboard.press("Tab") not reliable in headless mode',
|
||||||
|
);
|
||||||
|
|
||||||
|
test("can only interact with header and footer while reconnecting", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("Test call");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("Test user");
|
||||||
|
// If we do not call fastForward here, we end up with Date.now() returning an actual timestamp
|
||||||
|
// but once we call `await page.clock.fastForward(20000);` later this will reset Date.now() to 0
|
||||||
|
// and we will never get into probablyDisconnected state?
|
||||||
|
await page.clock.fastForward(10);
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
await expect(page.locator("video")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
|
||||||
|
// The media tile for the local user should become visible
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
await expect(page.getByTestId("name_tag")).toContainText("Test user");
|
||||||
|
|
||||||
|
// Now disconnect from the internet
|
||||||
|
await page.route("https://synapse.m.localhost/**/*", async (route) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
await page.clock.fastForward(20000);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("dialog", { name: "Reconnecting…" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Tab order should jump directly from header to footer, skipping media tiles
|
||||||
|
await page.getByRole("button", { name: "Mute microphone" }).focus();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "Mute microphone" }),
|
||||||
|
).toBeFocused();
|
||||||
|
await page.keyboard.press("Tab");
|
||||||
|
await expect(page.getByRole("button", { name: "Stop video" })).toBeFocused();
|
||||||
|
// Most critically, we should be able to press the hangup button
|
||||||
|
await page.getByRole("button", { name: "End call" }).click();
|
||||||
|
});
|
||||||
75
playwright/restricted-sfu.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { sleep } from "matrix-js-sdk/lib/utils.js";
|
||||||
|
|
||||||
|
test("Should request JWT token before starting the call", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
let sfGetTimestamp = 0;
|
||||||
|
let sendStateEventTimestamp = 0;
|
||||||
|
await page.route(
|
||||||
|
"**/matrix-rtc.m.localhost/livekit/jwt/sfu/get",
|
||||||
|
async (route) => {
|
||||||
|
await sleep(2000); // Simulate very slow request
|
||||||
|
await route.continue();
|
||||||
|
sfGetTimestamp = Date.now();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.route(
|
||||||
|
"**/state/org.matrix.msc3401.call.member/**",
|
||||||
|
async (route) => {
|
||||||
|
await route.continue();
|
||||||
|
sendStateEventTimestamp = Date.now();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("HelloCall");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("John Doe");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
await page.waitForTimeout(4000);
|
||||||
|
// Ensure that the call is connected
|
||||||
|
await page
|
||||||
|
.locator("div")
|
||||||
|
.filter({ hasText: /^HelloCall$/ })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
expect(sfGetTimestamp).toBeGreaterThan(0);
|
||||||
|
expect(sendStateEventTimestamp).toBeGreaterThan(0);
|
||||||
|
expect(sfGetTimestamp).toBeLessThan(sendStateEventTimestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Error when pre-warming the focus are caught by the ErrorBoundary", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.route("**/openid/request_token", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 418, // Simulate an error not retryable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("HelloCall");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("John Doe");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
|
||||||
|
// Should fail
|
||||||
|
await expect(page.getByText("Something went wrong")).toBeVisible();
|
||||||
|
});
|
||||||
104
playwright/sfu-reconnect-bug.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("When creator left, avoid reconnect to the same SFU", async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
// Use reduce motion to disable animations that are making the tests a bit flaky
|
||||||
|
const creatorContext = await browser.newContext({ reducedMotion: "reduce" });
|
||||||
|
const creatorPage = await creatorContext.newPage();
|
||||||
|
|
||||||
|
await creatorPage.goto("/");
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ARRANGE: The first user creates a call as guest, join it, then click the invite button to copy the invite link
|
||||||
|
// ========
|
||||||
|
await creatorPage.getByTestId("home_callName").click();
|
||||||
|
await creatorPage.getByTestId("home_callName").fill("Welcome");
|
||||||
|
await creatorPage.getByTestId("home_displayName").click();
|
||||||
|
await creatorPage.getByTestId("home_displayName").fill("Inviter");
|
||||||
|
await creatorPage.getByTestId("home_go").click();
|
||||||
|
await expect(creatorPage.locator("video")).toBeVisible();
|
||||||
|
|
||||||
|
// join
|
||||||
|
await creatorPage.getByTestId("lobby_joinCall").click();
|
||||||
|
// Spotlight mode to make checking the test visually clearer
|
||||||
|
await creatorPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
// Get the invite link
|
||||||
|
await creatorPage.getByRole("button", { name: "Invite" }).click();
|
||||||
|
await expect(
|
||||||
|
creatorPage.getByRole("heading", { name: "Invite to this call" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(creatorPage.getByRole("img", { name: "QR Code" })).toBeVisible();
|
||||||
|
await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible();
|
||||||
|
await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible();
|
||||||
|
await creatorPage.getByTestId("modal_inviteLink").click();
|
||||||
|
|
||||||
|
const inviteLink = (await creatorPage.evaluate(
|
||||||
|
"navigator.clipboard.readText()",
|
||||||
|
)) as string;
|
||||||
|
expect(inviteLink).toContain("room/#/");
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ACT: The other user use the invite link to join the call as a guest
|
||||||
|
// ========
|
||||||
|
const guestB = await browser.newContext({
|
||||||
|
reducedMotion: "reduce",
|
||||||
|
});
|
||||||
|
const guestBPage = await guestB.newPage();
|
||||||
|
|
||||||
|
await guestBPage.goto(inviteLink);
|
||||||
|
await guestBPage.getByTestId("joincall_displayName").fill("Invitee");
|
||||||
|
await expect(guestBPage.getByTestId("joincall_joincall")).toBeVisible();
|
||||||
|
await guestBPage.getByTestId("joincall_joincall").click();
|
||||||
|
await guestBPage.getByTestId("lobby_joinCall").click();
|
||||||
|
await guestBPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ACT: add a third user to the call to reproduce the bug
|
||||||
|
// ========
|
||||||
|
const guestC = await browser.newContext({
|
||||||
|
reducedMotion: "reduce",
|
||||||
|
});
|
||||||
|
const guestCPage = await guestC.newPage();
|
||||||
|
let sfuGetCallCount = 0;
|
||||||
|
await guestCPage.route("**/livekit/jwt/sfu/get", async (route) => {
|
||||||
|
sfuGetCallCount++;
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
// Track WebSocket connections
|
||||||
|
let wsConnectionCount = 0;
|
||||||
|
await guestCPage.routeWebSocket("**", (ws) => {
|
||||||
|
// For some reason the interception is not working with the **
|
||||||
|
if (ws.url().includes("livekit/sfu/rtc")) {
|
||||||
|
wsConnectionCount++;
|
||||||
|
}
|
||||||
|
ws.connectToServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
await guestCPage.goto(inviteLink);
|
||||||
|
await guestCPage.getByTestId("joincall_displayName").fill("Invitee");
|
||||||
|
await expect(guestCPage.getByTestId("joincall_joincall")).toBeVisible();
|
||||||
|
await guestCPage.getByTestId("joincall_joincall").click();
|
||||||
|
await guestCPage.getByTestId("lobby_joinCall").click();
|
||||||
|
await guestCPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
await guestCPage.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// the creator leaves the call
|
||||||
|
await creatorPage.getByTestId("incall_leave").click();
|
||||||
|
|
||||||
|
await guestCPage.waitForTimeout(2000);
|
||||||
|
// https://github.com/element-hq/element-call/issues/3344
|
||||||
|
// The app used to request a new jwt token then to reconnect to the SFU
|
||||||
|
expect(wsConnectionCount).toBe(1);
|
||||||
|
expect(sfuGetCallCount).toBe(2 /* the first one is for the warmup */);
|
||||||
|
});
|
||||||
@@ -9,11 +9,14 @@ import { expect, test } from "@playwright/test";
|
|||||||
|
|
||||||
import { widgetTest } from "../fixtures/widget-user.ts";
|
import { widgetTest } from "../fixtures/widget-user.ts";
|
||||||
|
|
||||||
|
// Skip test, including Fixtures
|
||||||
|
widgetTest.skip(
|
||||||
|
({ browserName }) => browserName === "firefox",
|
||||||
|
"This test is not working on firefox, after hangup brooks is locked in a strange state with a blank widget",
|
||||||
|
);
|
||||||
|
|
||||||
widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
||||||
test.skip(
|
test.slow(); // Triples the timeout
|
||||||
browserName === "firefox",
|
|
||||||
"This test is not working on firefox, after hangup brooks is locked in a strange state with a blank widget",
|
|
||||||
);
|
|
||||||
|
|
||||||
const { brooks, whistler } = asWidget;
|
const { brooks, whistler } = asWidget;
|
||||||
|
|
||||||
@@ -46,7 +49,10 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
|||||||
|
|
||||||
// Check the join indicator on the room list
|
// Check the join indicator on the room list
|
||||||
await expect(
|
await expect(
|
||||||
brooks.page.locator("div").filter({ hasText: /^Joined • 1$/ }),
|
brooks.page
|
||||||
|
.locator('iframe[title="Element Call"]')
|
||||||
|
.contentFrame()
|
||||||
|
.getByRole("button", { name: "End call" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Join from the other side
|
// Join from the other side
|
||||||
@@ -56,26 +62,28 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await whistler.page.getByRole("button", { name: "Join" }).click();
|
await whistler.page.getByRole("button", { name: "Join" }).click();
|
||||||
|
|
||||||
await expect(
|
// Currently disabled due to recent Element Web is bypassing Lobby
|
||||||
whistler.page
|
// await expect(
|
||||||
.locator('iframe[title="Element Call"]')
|
// whistler.page
|
||||||
.contentFrame()
|
// .locator('iframe[title="Element Call"]')
|
||||||
.getByTestId("lobby_joinCall"),
|
// .contentFrame()
|
||||||
).toBeVisible();
|
// .getByTestId("lobby_joinCall"),
|
||||||
|
// ).toBeVisible();
|
||||||
|
//
|
||||||
|
// await whistler.page
|
||||||
|
// .locator('iframe[title="Element Call"]')
|
||||||
|
// .contentFrame()
|
||||||
|
// .getByTestId("lobby_joinCall")
|
||||||
|
// .click();
|
||||||
|
|
||||||
await whistler.page
|
// Currrenty disabled due to recent Element Web not indicating the number of participants
|
||||||
.locator('iframe[title="Element Call"]')
|
// await expect(
|
||||||
.contentFrame()
|
// whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||||
.getByTestId("lobby_joinCall")
|
// ).toBeVisible();
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
// await expect(
|
||||||
whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
// brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||||
).toBeVisible();
|
// ).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
|
||||||
brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Whistler leaves
|
// Whistler leaves
|
||||||
await whistler.page.waitForTimeout(1000);
|
await whistler.page.waitForTimeout(1000);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"semanticCommits": "disabled",
|
"semanticCommits": "disabled",
|
||||||
"ignoreDeps": ["posthog-js"],
|
"ignoreDeps": ["posthog-js", "eslint-plugin-matrix-org"],
|
||||||
"vulnerabilityAlerts": {
|
"vulnerabilityAlerts": {
|
||||||
"schedule": ["at any time"],
|
"schedule": ["at any time"],
|
||||||
"prHourlyLimit": 0,
|
"prHourlyLimit": 0,
|
||||||
|
|||||||
10
scripts/playwright-webserver-command.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ -n "$USE_DOCKER" ]; then
|
||||||
|
set -ex
|
||||||
|
yarn build
|
||||||
|
docker build -t element-call:testing .
|
||||||
|
exec docker run --rm --name element-call-testing -p 8080:8080 -v ./config/config.devenv.json:/app/config.json:ro,Z element-call:testing
|
||||||
|
else
|
||||||
|
cp config/config.devenv.json public/config.json
|
||||||
|
exec yarn dev
|
||||||
|
fi
|
||||||
82
src/App.tsx
@@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type FC, type JSX, Suspense, useEffect, useState } from "react";
|
import {
|
||||||
|
type FC,
|
||||||
|
type JSX,
|
||||||
|
Suspense,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
@@ -17,12 +24,14 @@ import { RegisterPage } from "./auth/RegisterPage";
|
|||||||
import { RoomPage } from "./room/RoomPage";
|
import { RoomPage } from "./room/RoomPage";
|
||||||
import { ClientProvider } from "./ClientContext";
|
import { ClientProvider } from "./ClientContext";
|
||||||
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
||||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
|
||||||
import { Initializer } from "./initializer";
|
import { Initializer } from "./initializer";
|
||||||
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
|
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
import { useTheme } from "./useTheme";
|
import { useTheme } from "./useTheme";
|
||||||
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
||||||
|
import { type AppViewModel } from "./state/AppViewModel";
|
||||||
|
import { MediaDevicesContext } from "./MediaDevicesContext";
|
||||||
|
import { getUrlParams, HeaderStyle } from "./UrlParams";
|
||||||
|
import { AppBar } from "./AppBar";
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
||||||
|
|
||||||
@@ -50,7 +59,11 @@ const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
|||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const App: FC = () => {
|
interface Props {
|
||||||
|
vm: AppViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App: FC<Props> = ({ vm }) => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Initializer.init()
|
Initializer.init()
|
||||||
@@ -62,41 +75,42 @@ export const App: FC = () => {
|
|||||||
.catch(logger.error);
|
.catch(logger.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Since we are outside the router component, we cannot use useUrlParams here
|
||||||
|
const { header } = useMemo(getUrlParams, []);
|
||||||
|
|
||||||
|
const content = loaded ? (
|
||||||
|
<ClientProvider>
|
||||||
|
<MediaDevicesContext value={vm.mediaDevices}>
|
||||||
|
<ProcessorProvider>
|
||||||
|
<Sentry.ErrorBoundary
|
||||||
|
fallback={(error) => <ErrorPage error={error} widget={widget} />}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<SentryRoute path="/" element={<HomePage />} />
|
||||||
|
<SentryRoute path="/login" element={<LoginPage />} />
|
||||||
|
<SentryRoute path="/register" element={<RegisterPage />} />
|
||||||
|
<SentryRoute path="*" element={<RoomPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
</ProcessorProvider>
|
||||||
|
</MediaDevicesContext>
|
||||||
|
</ClientProvider>
|
||||||
|
) : (
|
||||||
|
<LoadingPage />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<BackgroundProvider>
|
<BackgroundProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{loaded ? (
|
<Suspense fallback={null}>
|
||||||
<Suspense fallback={null}>
|
{header === HeaderStyle.AppBar ? (
|
||||||
<ClientProvider>
|
<AppBar>{content}</AppBar>
|
||||||
<MediaDevicesProvider>
|
) : (
|
||||||
<ProcessorProvider>
|
content
|
||||||
<Sentry.ErrorBoundary
|
)}
|
||||||
fallback={(error) => (
|
</Suspense>
|
||||||
<ErrorPage error={error} widget={widget} />
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DisconnectedBanner />
|
|
||||||
<Routes>
|
|
||||||
<SentryRoute path="/" element={<HomePage />} />
|
|
||||||
<SentryRoute path="/login" element={<LoginPage />} />
|
|
||||||
<SentryRoute
|
|
||||||
path="/register"
|
|
||||||
element={<RegisterPage />}
|
|
||||||
/>
|
|
||||||
<SentryRoute path="*" element={<RoomPage />} />
|
|
||||||
</Routes>
|
|
||||||
</Sentry.ErrorBoundary>
|
|
||||||
</ProcessorProvider>
|
|
||||||
</MediaDevicesProvider>
|
|
||||||
</ClientProvider>
|
|
||||||
</Suspense>
|
|
||||||
) : (
|
|
||||||
<LoadingPage />
|
|
||||||
)}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</BackgroundProvider>
|
</BackgroundProvider>
|
||||||
|
|||||||
23
src/AppBar.module.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.bar {
|
||||||
|
block-size: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > header {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline: 0;
|
||||||
|
inset-block-start: 0;
|
||||||
|
block-size: 64px;
|
||||||
|
z-index: var(--call-view-header-footer-layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar svg path {
|
||||||
|
fill: var(--cpd-color-icon-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > header > h1 {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
25
src/AppBar.test.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { AppBar } from "./AppBar";
|
||||||
|
|
||||||
|
describe("AppBar", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<AppBar>
|
||||||
|
<p>This is the content.</p>
|
||||||
|
</AppBar>
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
134
src/AppBar.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type FC,
|
||||||
|
type MouseEvent,
|
||||||
|
type ReactNode,
|
||||||
|
use,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Heading, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||||
|
import { CollapseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Header, LeftNav, RightNav } from "./Header";
|
||||||
|
import { platform } from "./Platform";
|
||||||
|
import styles from "./AppBar.module.css";
|
||||||
|
|
||||||
|
interface AppBarContext {
|
||||||
|
setTitle: (value: string) => void;
|
||||||
|
setSecondaryButton: (value: ReactNode) => void;
|
||||||
|
setHidden: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppBarContext = createContext<AppBarContext | null>(null);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "top app bar" featuring a back button, title and possibly a secondary
|
||||||
|
* button, similar to what you might see in mobile apps.
|
||||||
|
*/
|
||||||
|
export const AppBar: FC<Props> = ({ children }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const onBackClick = useCallback((e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.controls.onBackButtonPressed?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [hidden, setHidden] = useState<boolean>(false);
|
||||||
|
const [secondaryButton, setSecondaryButton] = useState<ReactNode>(null);
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({ setTitle, setSecondaryButton, setHidden }),
|
||||||
|
[setTitle, setHidden, setSecondaryButton],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{ display: hidden ? "none" : "block" }}
|
||||||
|
className={styles.bar}
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
// App bar is mainly seen in the call view, which has its own
|
||||||
|
// 'reconnecting' toast
|
||||||
|
disconnectedBanner={false}
|
||||||
|
>
|
||||||
|
<LeftNav>
|
||||||
|
<Tooltip label={t("common.back")}>
|
||||||
|
<IconButton onClick={onBackClick}>
|
||||||
|
<CollapseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</LeftNav>
|
||||||
|
{title && (
|
||||||
|
<Heading
|
||||||
|
type="body"
|
||||||
|
size="lg"
|
||||||
|
weight={platform === "android" ? "medium" : "semibold"}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
)}
|
||||||
|
<RightNav>{secondaryButton}</RightNav>
|
||||||
|
</Header>
|
||||||
|
</div>
|
||||||
|
<AppBarContext value={context}>{children}</AppBarContext>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook which sets the title to be shown in the app bar, if present. It is
|
||||||
|
* an error to call this hook from multiple sites in the same component tree.
|
||||||
|
*/
|
||||||
|
export function useAppBarTitle(title: string): void {
|
||||||
|
const setTitle = use(AppBarContext)?.setTitle;
|
||||||
|
useEffect(() => {
|
||||||
|
if (setTitle !== undefined) {
|
||||||
|
setTitle(title);
|
||||||
|
return (): void => setTitle("");
|
||||||
|
}
|
||||||
|
}, [title, setTitle]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook which sets the title to be shown in the app bar, if present. It is
|
||||||
|
* an error to call this hook from multiple sites in the same component tree.
|
||||||
|
*/
|
||||||
|
export function useAppBarHidden(hidden: boolean): void {
|
||||||
|
const setHidden = use(AppBarContext)?.setHidden;
|
||||||
|
useEffect(() => {
|
||||||
|
if (setHidden !== undefined) {
|
||||||
|
setHidden(hidden);
|
||||||
|
return (): void => setHidden(false);
|
||||||
|
}
|
||||||
|
}, [setHidden, hidden]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook which sets the secondary button to be shown in the app bar, if
|
||||||
|
* present. It is an error to call this hook from multiple sites in the same
|
||||||
|
* component tree.
|
||||||
|
*/
|
||||||
|
export function useAppBarSecondaryButton(button: ReactNode): void {
|
||||||
|
const setSecondaryButton = use(AppBarContext)?.setSecondaryButton;
|
||||||
|
useEffect(() => {
|
||||||
|
if (setSecondaryButton !== undefined) {
|
||||||
|
setSecondaryButton(button);
|
||||||
|
return (): void => setSecondaryButton("");
|
||||||
|
}
|
||||||
|
}, [button, setSecondaryButton]);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
use,
|
||||||
useRef,
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
type JSX,
|
type JSX,
|
||||||
@@ -69,8 +69,7 @@ const ClientContext = createContext<ClientState | undefined>(undefined);
|
|||||||
|
|
||||||
export const ClientContextProvider = ClientContext.Provider;
|
export const ClientContextProvider = ClientContext.Provider;
|
||||||
|
|
||||||
export const useClientState = (): ClientState | undefined =>
|
export const useClientState = (): ClientState | undefined => use(ClientContext);
|
||||||
useContext(ClientContext);
|
|
||||||
|
|
||||||
export function useClient(): {
|
export function useClient(): {
|
||||||
client?: MatrixClient;
|
client?: MatrixClient;
|
||||||
@@ -350,9 +349,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
|
return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ClientContext value={state}>{children}</ClientContext>;
|
||||||
<ClientContext.Provider value={state}>{children}</ClientContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitResult = {
|
export type InitResult = {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
.error > h1 {
|
.error > h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error > p {
|
.error > p {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const ErrorView: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<BigIcon className={styles.icon}>
|
<BigIcon className={styles.icon}>
|
||||||
<Icon />
|
<Icon aria-hidden />
|
||||||
</BigIcon>
|
</BigIcon>
|
||||||
<Heading as="h1" weight="semibold" size="md">
|
<Heading as="h1" weight="semibold" size="md">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import classNames from "classnames";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||||
import styles from "./FullScreenView.module.css";
|
import styles from "./FullScreenView.module.css";
|
||||||
@@ -28,10 +28,10 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { hideHeader } = useUrlParams();
|
const { header } = useUrlParams();
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.page, className)}>
|
<div className={classNames(styles.page, className)}>
|
||||||
{!hideHeader && (
|
{header === "standard" && (
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<HeaderLogo />
|
<HeaderLogo />
|
||||||
@@ -67,7 +67,7 @@ export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => {
|
|||||||
) : (
|
) : (
|
||||||
<ErrorView
|
<ErrorView
|
||||||
widget={widget}
|
widget={widget}
|
||||||
Icon={ErrorIcon}
|
Icon={ErrorSolidIcon}
|
||||||
title={t("error.generic")}
|
title={t("error.generic")}
|
||||||
rageshake
|
rageshake
|
||||||
fatal
|
fatal
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import { type Ref, type FC, type HTMLAttributes, type ReactNode } from "react";
|
||||||
type FC,
|
|
||||||
type HTMLAttributes,
|
|
||||||
type ReactNode,
|
|
||||||
forwardRef,
|
|
||||||
} from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Heading, Text } from "@vector-im/compound-web";
|
import { Heading, Text } from "@vector-im/compound-web";
|
||||||
@@ -22,15 +17,29 @@ import Logo from "./icons/Logo.svg?react";
|
|||||||
import { Avatar, Size } from "./Avatar";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { EncryptionLock } from "./room/EncryptionLock";
|
import { EncryptionLock } from "./room/EncryptionLock";
|
||||||
import { useMediaQuery } from "./useMediaQuery";
|
import { useMediaQuery } from "./useMediaQuery";
|
||||||
|
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||||
|
|
||||||
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Whether the header should display an informational banner whenever the
|
||||||
|
* client is disconnected from the homeserver.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
disconnectedBanner?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header = forwardRef<HTMLElement, HeaderProps>(
|
export const Header: FC<HeaderProps> = ({
|
||||||
({ children, className, ...rest }, ref) => {
|
ref,
|
||||||
return (
|
children,
|
||||||
|
className,
|
||||||
|
disconnectedBanner = true,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<header
|
<header
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(styles.header, className)}
|
className={classNames(styles.header, className)}
|
||||||
@@ -38,9 +47,10 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</header>
|
</header>
|
||||||
);
|
{disconnectedBanner && <DisconnectedBanner />}
|
||||||
},
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Header.displayName = "Header";
|
Header.displayName = "Header";
|
||||||
|
|
||||||
|
|||||||
48
src/MediaDevicesContext.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, use, useMemo } from "react";
|
||||||
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
|
||||||
|
import { type MediaDevices } from "./state/MediaDevices";
|
||||||
|
|
||||||
|
export const MediaDevicesContext = createContext<MediaDevices | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useMediaDevices(): MediaDevices {
|
||||||
|
const mediaDevices = use(MediaDevicesContext);
|
||||||
|
if (mediaDevices === undefined)
|
||||||
|
throw new Error(
|
||||||
|
"useMediaDevices must be used within a MediaDevices context provider",
|
||||||
|
);
|
||||||
|
return mediaDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience hook to get the audio node configuration for the earpiece.
|
||||||
|
* It will check the `useAsEarpiece` of the `audioOutput` device and return
|
||||||
|
* the appropriate pan and volume values.
|
||||||
|
*
|
||||||
|
* @returns pan and volume values for the earpiece audio node configuration.
|
||||||
|
*/
|
||||||
|
export const useEarpieceAudioConfig = (): {
|
||||||
|
pan: number;
|
||||||
|
volume: number;
|
||||||
|
} => {
|
||||||
|
const devices = useMediaDevices();
|
||||||
|
const audioOutput = useObservableEagerState(devices.audioOutput.selected$);
|
||||||
|
const isVirtualEarpiece = audioOutput?.virtualEarpiece ?? false;
|
||||||
|
return {
|
||||||
|
// We use only the right speaker (pan = 1) for the earpiece.
|
||||||
|
// This mimics the behavior of the native earpiece speaker (only the top speaker on an iPhone)
|
||||||
|
pan: useMemo(() => (isVirtualEarpiece ? 1 : 0), [isVirtualEarpiece]),
|
||||||
|
// We also do lower the volume by a factor of 10 to optimize for the usecase where
|
||||||
|
// a user is holding the phone to their ear.
|
||||||
|
volume: useMemo(() => (isVirtualEarpiece ? 0.1 : 1), [isVirtualEarpiece]),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -35,6 +35,8 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
.bg.animate[data-state="closed"] {
|
.bg.animate[data-state="closed"] {
|
||||||
animation: fade-out 130ms;
|
animation: fade-out 130ms;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|||||||
@@ -19,10 +19,26 @@ import mediaViewStyles from "../src/tile/MediaView.module.css";
|
|||||||
interface Props {
|
interface Props {
|
||||||
audio?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
audio?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
||||||
video?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
video?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
||||||
|
focusUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractDomain = (url: string): string => {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
return parsedUrl.hostname; // Returns "kdk.cpm"
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid URL:", error);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// This is only used in developer mode for debugging purposes, so we don't need full localization
|
// This is only used in developer mode for debugging purposes, so we don't need full localization
|
||||||
export const RTCConnectionStats: FC<Props> = ({ audio, video, ...rest }) => {
|
export const RTCConnectionStats: FC<Props> = ({
|
||||||
|
audio,
|
||||||
|
video,
|
||||||
|
focusUrl,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [modalContents, setModalContents] = useState<
|
const [modalContents, setModalContents] = useState<
|
||||||
"video" | "audio" | "none"
|
"video" | "audio" | "none"
|
||||||
@@ -55,6 +71,13 @@ export const RTCConnectionStats: FC<Props> = ({ audio, video, ...rest }) => {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
{focusUrl && (
|
||||||
|
<div>
|
||||||
|
<Text as="span" size="xs" title="focusURL">
|
||||||
|
{extractDomain(focusUrl)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{audio && (
|
{audio && (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ interface Props {
|
|||||||
* A supporting icon to display within the toast.
|
* A supporting icon to display within the toast.
|
||||||
*/
|
*/
|
||||||
Icon?: ComponentType<SVGAttributes<SVGElement>>;
|
Icon?: ComponentType<SVGAttributes<SVGElement>>;
|
||||||
|
/**
|
||||||
|
* Whether the toast should be modal, making it fill the screen (by portalling
|
||||||
|
* it into the root of the document) and trap focus until dismissed.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
modal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +62,7 @@ export const Toast: FC<Props> = ({
|
|||||||
autoDismiss,
|
autoDismiss,
|
||||||
children,
|
children,
|
||||||
Icon,
|
Icon,
|
||||||
|
modal = true,
|
||||||
}) => {
|
}) => {
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
@@ -71,29 +78,33 @@ export const Toast: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [open, autoDismiss, onDismiss]);
|
}, [open, autoDismiss, onDismiss]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<DialogOverlay
|
||||||
|
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||||
|
/>
|
||||||
|
<DialogContent aria-describedby={undefined} asChild>
|
||||||
|
<DialogClose
|
||||||
|
className={classNames(
|
||||||
|
overlayStyles.overlay,
|
||||||
|
overlayStyles.animate,
|
||||||
|
styles.toast,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogTitle asChild>
|
||||||
|
<Text as="h3" size="sm" weight="semibold">
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
</DialogTitle>
|
||||||
|
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
<DialogRoot open={open} onOpenChange={onOpenChange} modal={modal}>
|
||||||
<DialogPortal>
|
{modal ? <DialogPortal>{content}</DialogPortal> : content}
|
||||||
<DialogOverlay
|
|
||||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
|
||||||
/>
|
|
||||||
<DialogContent aria-describedby={undefined} asChild>
|
|
||||||
<DialogClose
|
|
||||||
className={classNames(
|
|
||||||
overlayStyles.overlay,
|
|
||||||
overlayStyles.animate,
|
|
||||||
styles.toast,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogTitle asChild>
|
|
||||||
<Text as="h3" size="sm" weight="semibold">
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
</DialogTitle>
|
|
||||||
{Icon && <Icon width={20} height={20} aria-hidden />}
|
|
||||||
</DialogClose>
|
|
||||||
</DialogContent>
|
|
||||||
</DialogPortal>
|
|
||||||
</DialogRoot>
|
</DialogRoot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
|
import * as PlatformMod from "../src/Platform";
|
||||||
import {
|
import {
|
||||||
getRoomIdentifierFromUrl,
|
getRoomIdentifierFromUrl,
|
||||||
|
computeUrlParams,
|
||||||
|
HeaderStyle,
|
||||||
getUrlParams,
|
getUrlParams,
|
||||||
UserIntent,
|
|
||||||
} from "../src/UrlParams";
|
} from "../src/UrlParams";
|
||||||
|
|
||||||
const ROOM_NAME = "roomNameHere";
|
const ROOM_NAME = "roomNameHere";
|
||||||
@@ -82,6 +85,16 @@ describe("UrlParams", () => {
|
|||||||
getRoomIdentifierFromUrl("", `?roomId=${ROOM_ID}`, "").roomId,
|
getRoomIdentifierFromUrl("", `?roomId=${ROOM_ID}`, "").roomId,
|
||||||
).toBe(ROOM_ID);
|
).toBe(ROOM_ID);
|
||||||
});
|
});
|
||||||
|
it("(roomId with unprintable characters)", () => {
|
||||||
|
const invisibleChar = "\u2066";
|
||||||
|
expect(
|
||||||
|
getRoomIdentifierFromUrl(
|
||||||
|
"",
|
||||||
|
`?roomId=${invisibleChar}${ROOM_ID}${invisibleChar}`,
|
||||||
|
"",
|
||||||
|
).roomId,
|
||||||
|
).toBe(ROOM_ID);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores room alias", () => {
|
it("ignores room alias", () => {
|
||||||
@@ -93,16 +106,16 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("preload", () => {
|
describe("preload", () => {
|
||||||
it("defaults to false", () => {
|
it("defaults to false", () => {
|
||||||
expect(getUrlParams().preload).toBe(false);
|
expect(computeUrlParams().preload).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignored in SPA mode", () => {
|
it("ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?preload=true").preload).toBe(false);
|
expect(computeUrlParams("?preload=true").preload).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respected in widget mode", () => {
|
it("respected in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).preload,
|
).preload,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
@@ -111,19 +124,20 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("returnToLobby", () => {
|
describe("returnToLobby", () => {
|
||||||
it("is false in SPA mode", () => {
|
it("is false in SPA mode", () => {
|
||||||
expect(getUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
expect(computeUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to false in widget mode", () => {
|
it("defaults to false in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo")
|
computeUrlParams(
|
||||||
.returnToLobby,
|
"?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
|
).returnToLobby,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respected in widget mode", () => {
|
it("respected in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).returnToLobby,
|
).returnToLobby,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
@@ -132,12 +146,12 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("userId", () => {
|
describe("userId", () => {
|
||||||
it("is ignored in SPA mode", () => {
|
it("is ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?userId=asd").userId).toBe(null);
|
expect(computeUrlParams("?userId=asd").userId).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in widget mode", () => {
|
it("is parsed in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).userId,
|
).userId,
|
||||||
).toBe("asd");
|
).toBe("asd");
|
||||||
@@ -146,12 +160,12 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("deviceId", () => {
|
describe("deviceId", () => {
|
||||||
it("is ignored in SPA mode", () => {
|
it("is ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?deviceId=asd").deviceId).toBe(null);
|
expect(computeUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in widget mode", () => {
|
it("is parsed in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).deviceId,
|
).deviceId,
|
||||||
).toBe("asd");
|
).toBe("asd");
|
||||||
@@ -160,12 +174,12 @@ describe("UrlParams", () => {
|
|||||||
|
|
||||||
describe("baseUrl", () => {
|
describe("baseUrl", () => {
|
||||||
it("is ignored in SPA mode", () => {
|
it("is ignored in SPA mode", () => {
|
||||||
expect(getUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
expect(computeUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in widget mode", () => {
|
it("is parsed in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).baseUrl,
|
).baseUrl,
|
||||||
).toBe("asd");
|
).toBe("asd");
|
||||||
@@ -175,72 +189,179 @@ describe("UrlParams", () => {
|
|||||||
describe("viaServers", () => {
|
describe("viaServers", () => {
|
||||||
it("is ignored in widget mode", () => {
|
it("is ignored in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).viaServers,
|
).viaServers,
|
||||||
).toBe(null);
|
).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in SPA mode", () => {
|
it("is parsed in SPA mode", () => {
|
||||||
expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
expect(computeUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("homeserver", () => {
|
describe("homeserver", () => {
|
||||||
it("is ignored in widget mode", () => {
|
it("is ignored in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).homeserver,
|
).homeserver,
|
||||||
).toBe(null);
|
).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is parsed in SPA mode", () => {
|
it("is parsed in SPA mode", () => {
|
||||||
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
expect(computeUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("intent", () => {
|
describe("intent", () => {
|
||||||
it("defaults to unknown", () => {
|
const noIntentDefaults = {
|
||||||
expect(getUrlParams().intent).toBe(UserIntent.Unknown);
|
confineToRoom: false,
|
||||||
|
appPrompt: true,
|
||||||
|
preload: false,
|
||||||
|
header: HeaderStyle.Standard,
|
||||||
|
showControls: true,
|
||||||
|
hideScreensharing: false,
|
||||||
|
allowIceFallback: false,
|
||||||
|
perParticipantE2EE: false,
|
||||||
|
controlledAudioDevices: false,
|
||||||
|
skipLobby: false,
|
||||||
|
returnToLobby: false,
|
||||||
|
sendNotificationType: undefined,
|
||||||
|
};
|
||||||
|
const startNewCallDefaults = (platform: string): object => ({
|
||||||
|
confineToRoom: true,
|
||||||
|
appPrompt: false,
|
||||||
|
preload: false,
|
||||||
|
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||||
|
showControls: true,
|
||||||
|
hideScreensharing: false,
|
||||||
|
allowIceFallback: true,
|
||||||
|
perParticipantE2EE: true,
|
||||||
|
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||||
|
skipLobby: true,
|
||||||
|
returnToLobby: false,
|
||||||
|
sendNotificationType: platform === "desktop" ? "notification" : "ring",
|
||||||
|
});
|
||||||
|
const joinExistingCallDefaults = (platform: string): object => ({
|
||||||
|
confineToRoom: true,
|
||||||
|
appPrompt: false,
|
||||||
|
preload: false,
|
||||||
|
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||||
|
showControls: true,
|
||||||
|
hideScreensharing: false,
|
||||||
|
allowIceFallback: true,
|
||||||
|
perParticipantE2EE: true,
|
||||||
|
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||||
|
skipLobby: false,
|
||||||
|
returnToLobby: false,
|
||||||
|
sendNotificationType: "notification",
|
||||||
|
defaultAudioEnabled: true,
|
||||||
|
defaultVideoEnabled: true,
|
||||||
|
});
|
||||||
|
it("use no-intent-defaults with unknown intent", () => {
|
||||||
|
expect(computeUrlParams()).toMatchObject(noIntentDefaults);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores intent if it is not a valid value", () => {
|
it("ignores intent if it is not a valid value", () => {
|
||||||
expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown);
|
expect(computeUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts start_call", () => {
|
it("accepts start_call", () => {
|
||||||
expect(getUrlParams("?intent=start_call").intent).toBe(
|
expect(
|
||||||
UserIntent.StartNewCall,
|
computeUrlParams(
|
||||||
);
|
"?intent=start_call&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject({ ...startNewCallDefaults("desktop"), skipLobby: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts start_call_dm mobile", () => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("android");
|
||||||
|
onTestFinished(() => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("desktop");
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=start_call_dm&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject(startNewCallDefaults("android"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts start_call_dm mobile and prioritizes overwritten params", () => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("android");
|
||||||
|
onTestFinished(() => {
|
||||||
|
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("desktop");
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
computeUrlParams(
|
||||||
|
"?intent=start_call_dm&widgetId=1234&parentUrl=parent.org&sendNotificationType=notification",
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
...startNewCallDefaults("android"),
|
||||||
|
sendNotificationType: "notification",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts join_existing", () => {
|
it("accepts join_existing", () => {
|
||||||
expect(getUrlParams("?intent=join_existing").intent).toBe(
|
expect(
|
||||||
UserIntent.JoinExistingCall,
|
computeUrlParams(
|
||||||
);
|
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
||||||
|
),
|
||||||
|
).toMatchObject(joinExistingCallDefaults("desktop"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("skipLobby", () => {
|
describe("skipLobby", () => {
|
||||||
it("defaults to false", () => {
|
it("defaults to false", () => {
|
||||||
expect(getUrlParams().skipLobby).toBe(false);
|
expect(computeUrlParams().skipLobby).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to false if intent is start_call in SPA mode", () => {
|
it("defaults to false if intent is start_call in SPA mode", () => {
|
||||||
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
|
expect(computeUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to true if intent is start_call in widget mode", () => {
|
it("defaults to false if intent is start_call in widget mode", () => {
|
||||||
expect(
|
expect(
|
||||||
getUrlParams(
|
computeUrlParams(
|
||||||
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||||
).skipLobby,
|
).skipLobby,
|
||||||
).toBe(true);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("default to false if intent is join_existing", () => {
|
it("default to false if intent is join_existing", () => {
|
||||||
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
expect(computeUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("header", () => {
|
||||||
|
it("uses header if provided", () => {
|
||||||
|
expect(computeUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||||
|
"app_bar",
|
||||||
|
);
|
||||||
|
expect(computeUrlParams("?header=none&hideHeader=false").header).toBe(
|
||||||
|
"none",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("getUrlParams", () => {
|
||||||
|
it("uses cached values", () => {
|
||||||
|
const spy = vi.spyOn(logger, "info");
|
||||||
|
// call get once
|
||||||
|
const params = getUrlParams("?header=app_bar&hideHeader=true", "");
|
||||||
|
// call get twice
|
||||||
|
expect(getUrlParams("?header=app_bar&hideHeader=true", "")).toBe(params);
|
||||||
|
// expect compute to only be called once
|
||||||
|
// it will only log when it is computing the values
|
||||||
|
expect(spy).toHaveBeenCalledExactlyOnceWith(
|
||||||
|
"UrlParams: final set of url params\n",
|
||||||
|
"intent:",
|
||||||
|
"unknown",
|
||||||
|
"\nproperties:",
|
||||||
|
expect.any(Object),
|
||||||
|
"configuration:",
|
||||||
|
expect.any(Object),
|
||||||
|
"intentAndPlatformDerivedConfiguration:",
|
||||||
|
{},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
459
src/UrlParams.ts
@@ -8,10 +8,16 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
import {
|
||||||
|
type RTCCallIntent,
|
||||||
|
type RTCNotificationType,
|
||||||
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
import { pickBy } from "lodash-es";
|
||||||
|
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||||
import { E2eeType } from "./e2ee/e2eeType";
|
import { E2eeType } from "./e2ee/e2eeType";
|
||||||
|
import { platform } from "./Platform";
|
||||||
|
|
||||||
interface RoomIdentifier {
|
interface RoomIdentifier {
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
@@ -22,15 +28,25 @@ interface RoomIdentifier {
|
|||||||
export enum UserIntent {
|
export enum UserIntent {
|
||||||
StartNewCall = "start_call",
|
StartNewCall = "start_call",
|
||||||
JoinExistingCall = "join_existing",
|
JoinExistingCall = "join_existing",
|
||||||
|
StartNewCallDM = "start_call_dm",
|
||||||
|
StartNewCallDMVoice = "start_call_dm_voice",
|
||||||
|
JoinExistingCallDM = "join_existing_dm",
|
||||||
|
JoinExistingCallDMVoice = "join_existing_dm_voice",
|
||||||
Unknown = "unknown",
|
Unknown = "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you need to add a new flag to this interface, prefer a name that describes
|
export enum HeaderStyle {
|
||||||
// a specific behavior (such as 'confineToRoom'), rather than one that describes
|
None = "none",
|
||||||
// the situations that call for this behavior ('isEmbedded'). This makes it
|
Standard = "standard",
|
||||||
// clearer what each flag means, and helps us avoid coupling Element Call's
|
AppBar = "app_bar",
|
||||||
// behavior to the needs of specific consumers.
|
}
|
||||||
export interface UrlParams {
|
|
||||||
|
/**
|
||||||
|
* The UrlProperties are used to pass required data to the widget.
|
||||||
|
* Those are different in different rooms, users, devices. They do not configure the behavior of the
|
||||||
|
* widget but provide the required data to the widget.
|
||||||
|
*/
|
||||||
|
export interface UrlProperties {
|
||||||
// Widget api related params
|
// Widget api related params
|
||||||
widgetId: string | null;
|
widgetId: string | null;
|
||||||
parentUrl: string | null;
|
parentUrl: string | null;
|
||||||
@@ -42,42 +58,11 @@ export interface UrlParams {
|
|||||||
* is also not validated, where it is in useRoomIdentifier().
|
* is also not validated, where it is in useRoomIdentifier().
|
||||||
*/
|
*/
|
||||||
roomId: string | null;
|
roomId: string | null;
|
||||||
/**
|
|
||||||
* Whether the app should keep the user confined to the current call/room.
|
|
||||||
*/
|
|
||||||
confineToRoom: boolean;
|
|
||||||
/**
|
|
||||||
* Whether upon entering a room, the user should be prompted to launch the
|
|
||||||
* native mobile app. (Affects only Android and iOS.)
|
|
||||||
*
|
|
||||||
* The app prompt must also be enabled in the config for this to take effect.
|
|
||||||
*/
|
|
||||||
appPrompt: 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 the controls should be shown. For screen recording no controls can be desired.
|
|
||||||
*/
|
|
||||||
showControls: boolean;
|
|
||||||
/**
|
|
||||||
* Whether to hide the screen-sharing button.
|
|
||||||
*/
|
|
||||||
hideScreensharing: boolean;
|
|
||||||
/**
|
|
||||||
* Whether to use end-to-end encryption.
|
|
||||||
*/
|
|
||||||
e2eEnabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* The user's ID (only used in matryoshka mode).
|
* The user's ID (only used in matryoshka mode).
|
||||||
*/
|
*/
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The display name to use for auto-registration.
|
* The display name to use for auto-registration.
|
||||||
*/
|
*/
|
||||||
@@ -115,18 +100,106 @@ export interface UrlParams {
|
|||||||
*/
|
*/
|
||||||
posthogApiKey: string | null;
|
posthogApiKey: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the app is allowed to use fallback STUN servers for ICE in case the
|
* Whether to use end-to-end encryption.
|
||||||
* user's homeserver doesn't provide any.
|
|
||||||
*/
|
*/
|
||||||
allowIceFallback: boolean;
|
e2eEnabled: boolean;
|
||||||
/**
|
/**
|
||||||
* E2EE password
|
* E2EE password
|
||||||
*/
|
*/
|
||||||
password: string | null;
|
password: string | null;
|
||||||
|
/** This defines the homeserver that is going to be used when joining a room.
|
||||||
|
* It has to be set to a non default value for links to rooms
|
||||||
|
* that are not on the default homeserver,
|
||||||
|
* that is in use for the current user.
|
||||||
|
*/
|
||||||
|
viaServers: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether we the app should use per participant keys for E2EE.
|
* This defines the homeserver that is going to be used when registering
|
||||||
|
* a new (guest) user.
|
||||||
|
* This can be user to configure a non default guest user server when
|
||||||
|
* creating a spa link.
|
||||||
|
*/
|
||||||
|
homeserver: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
||||||
|
*/
|
||||||
|
rageshakeSubmitUrl: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Sentry DSN. This is only used in the embedded package of Element Call.
|
||||||
|
*/
|
||||||
|
sentryDsn: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Sentry environment. This is only used in the embedded package of Element Call.
|
||||||
|
*/
|
||||||
|
sentryEnvironment: string | null;
|
||||||
|
/**
|
||||||
|
* The theme to use for element call.
|
||||||
|
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||||
|
*/
|
||||||
|
theme: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the app, which can be set via URL parameters.
|
||||||
|
* Those property are different to the UrlProperties, since they are all optional
|
||||||
|
* and configure the behavior of the app. Their value is the same if EC is used in
|
||||||
|
* the same context but with different accounts/users.
|
||||||
|
*
|
||||||
|
* Their defaults can be controlled by the `intent` property.
|
||||||
|
*/
|
||||||
|
export interface UrlConfiguration {
|
||||||
|
/**
|
||||||
|
* Whether the app should keep the user confined to the current call/room.
|
||||||
|
*/
|
||||||
|
confineToRoom: boolean;
|
||||||
|
/**
|
||||||
|
* Whether upon entering a room, the user should be prompted to launch the
|
||||||
|
* native mobile app. (Affects only Android and iOS.)
|
||||||
|
*
|
||||||
|
* The app prompt must also be enabled in the config for this to take effect.
|
||||||
|
*/
|
||||||
|
appPrompt: 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;
|
||||||
|
/**
|
||||||
|
* The style of headers to show. "standard" is the default arrangement, "none"
|
||||||
|
* hides the header entirely, and "app_bar" produces a header with a back
|
||||||
|
* button like you might see in mobile apps. The callback for the back button
|
||||||
|
* is window.controls.onBackButtonPressed.
|
||||||
|
*/
|
||||||
|
header: HeaderStyle;
|
||||||
|
/**
|
||||||
|
* Whether the controls should be shown. For screen recording no controls can be desired.
|
||||||
|
*/
|
||||||
|
showControls: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to hide the screen-sharing button.
|
||||||
|
*/
|
||||||
|
hideScreensharing: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the app is allowed to use fallback STUN servers for ICE in case the
|
||||||
|
* user's homeserver doesn't provide any.
|
||||||
|
*/
|
||||||
|
allowIceFallback: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the app should use per participant keys for E2EE.
|
||||||
*/
|
*/
|
||||||
perParticipantE2EE: boolean;
|
perParticipantE2EE: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the global JS controls for audio output devices should be enabled,
|
||||||
|
* allowing the list of output devices to be controlled by the app hosting
|
||||||
|
* Element Call.
|
||||||
|
*/
|
||||||
|
controlledAudioDevices: boolean;
|
||||||
/**
|
/**
|
||||||
* Setting this flag skips the lobby and brings you in the call directly.
|
* Setting this flag skips the lobby and brings you in the call directly.
|
||||||
* In the widget this can be combined with preload to pass the device settings
|
* In the widget this can be combined with preload to pass the device settings
|
||||||
@@ -139,45 +212,47 @@ export interface UrlParams {
|
|||||||
*/
|
*/
|
||||||
returnToLobby: boolean;
|
returnToLobby: boolean;
|
||||||
/**
|
/**
|
||||||
* The theme to use for element call.
|
* Whether and what type of notification EC should send, when the user joins the call.
|
||||||
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
|
||||||
*/
|
*/
|
||||||
theme: string | null;
|
sendNotificationType?: RTCNotificationType;
|
||||||
/** This defines the homeserver that is going to be used when joining a room.
|
|
||||||
* It has to be set to a non default value for links to rooms
|
|
||||||
* that are not on the default homeserver,
|
|
||||||
* that is in use for the current user.
|
|
||||||
*/
|
|
||||||
viaServers: string | null;
|
|
||||||
/**
|
/**
|
||||||
* This defines the homeserver that is going to be used when registering
|
* Whether the app should automatically leave the call when there
|
||||||
* a new (guest) user.
|
* is no one left in the call.
|
||||||
* This can be user to configure a non default guest user server when
|
* This is one part to make the call matrixRTC session behave like a telephone call.
|
||||||
* creating a spa link.
|
|
||||||
*/
|
*/
|
||||||
homeserver: string | null;
|
autoLeaveWhenOthersLeft: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user's intent with respect to the call.
|
* If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up).
|
||||||
* e.g. if they clicked a Start Call button, this would be `start_call`.
|
* This is a no-op if not combined with sendNotificationType.
|
||||||
* If it was a Join Call button, it would be `join_existing`.
|
*
|
||||||
|
* This entails:
|
||||||
|
* - show ui that it is awaiting an answer
|
||||||
|
* - play a sound that indicates that it is awaiting an answer
|
||||||
|
* - auto-dismiss the call widget once the notification lifetime expires on the receivers side.
|
||||||
*/
|
*/
|
||||||
intent: string | null;
|
waitForCallPickup: boolean;
|
||||||
|
|
||||||
/**
|
callIntent?: RTCCallIntent;
|
||||||
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
|
||||||
*/
|
|
||||||
rageshakeSubmitUrl: string | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Sentry DSN. This is only used in the embedded package of Element Call.
|
|
||||||
*/
|
|
||||||
sentryDsn: string | null;
|
|
||||||
/**
|
|
||||||
* The Sentry environment. This is only used in the embedded package of Element Call.
|
|
||||||
*/
|
|
||||||
sentryEnvironment: string | null;
|
|
||||||
}
|
}
|
||||||
|
interface IntentAndPlatformDerivedConfiguration {
|
||||||
|
defaultAudioEnabled?: boolean;
|
||||||
|
defaultVideoEnabled?: boolean;
|
||||||
|
}
|
||||||
|
interface IntentAndPlatformDerivedConfiguration {
|
||||||
|
defaultAudioEnabled?: boolean;
|
||||||
|
defaultVideoEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you need to add a new flag to this interface, prefer a name that describes
|
||||||
|
// a specific behavior (such as 'confineToRoom'), rather than one that describes
|
||||||
|
// the situations that call for this behavior ('isEmbedded'). This makes it
|
||||||
|
// clearer what each flag means, and helps us avoid coupling Element Call's
|
||||||
|
// behavior to the needs of specific consumers.
|
||||||
|
export interface UrlParams
|
||||||
|
extends UrlProperties,
|
||||||
|
UrlConfiguration,
|
||||||
|
IntentAndPlatformDerivedConfiguration {}
|
||||||
|
|
||||||
// This is here as a stopgap, but what would be far nicer is a function that
|
// This is here as a stopgap, but what would be far nicer is a function that
|
||||||
// takes a UrlParams and returns a query string. That would enable us to
|
// takes a UrlParams and returns a query string. That would enable us to
|
||||||
@@ -219,6 +294,17 @@ class ParamParser {
|
|||||||
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
|
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getEnumParam<T extends string>(
|
||||||
|
name: string,
|
||||||
|
type: { [s: string]: T } | ArrayLike<T>,
|
||||||
|
): T | undefined {
|
||||||
|
const value = this.getParam(name);
|
||||||
|
if (value !== null && Object.values(type).includes(value as T)) {
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public getAllParams(name: string): string[] {
|
public getAllParams(name: string): string[] {
|
||||||
return [
|
return [
|
||||||
...this.fragmentParams.getAll(name),
|
...this.fragmentParams.getAll(name),
|
||||||
@@ -226,14 +312,30 @@ class ParamParser {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the flag exists and is not "false".
|
||||||
|
*/
|
||||||
public getFlagParam(name: string, defaultValue = false): boolean {
|
public getFlagParam(name: string, defaultValue = false): boolean {
|
||||||
const param = this.getParam(name);
|
const param = this.getParam(name);
|
||||||
return param === null ? defaultValue : param !== "false";
|
return param === null ? defaultValue : param !== "false";
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Returns the value of the flag if it exists, or undefined if it does not.
|
||||||
|
*/
|
||||||
|
public getFlag(name: string): boolean | undefined {
|
||||||
|
const param = this.getParam(name);
|
||||||
|
return param !== null ? param !== "false" : undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let urlParamCache: {
|
||||||
|
search?: string;
|
||||||
|
hash?: string;
|
||||||
|
params?: UrlParams;
|
||||||
|
} = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the app parameters for the current URL.
|
* Gets the url params and loads them from a cache if already computed.
|
||||||
* @param search The URL search string
|
* @param search The URL search string
|
||||||
* @param hash The URL hash
|
* @param hash The URL hash
|
||||||
* @returns The app parameters encoded in the URL
|
* @returns The app parameters encoded in the URL
|
||||||
@@ -242,36 +344,145 @@ export const getUrlParams = (
|
|||||||
search = window.location.search,
|
search = window.location.search,
|
||||||
hash = window.location.hash,
|
hash = window.location.hash,
|
||||||
): UrlParams => {
|
): UrlParams => {
|
||||||
|
if (
|
||||||
|
urlParamCache.search === search &&
|
||||||
|
urlParamCache.hash === hash &&
|
||||||
|
urlParamCache.params
|
||||||
|
) {
|
||||||
|
return urlParamCache.params;
|
||||||
|
}
|
||||||
|
const params = computeUrlParams(search, hash);
|
||||||
|
urlParamCache = { search, hash, params };
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the app parameters for the current URL.
|
||||||
|
* @param search The URL search string
|
||||||
|
* @param hash The URL hash
|
||||||
|
* @returns The app parameters encoded in the URL
|
||||||
|
*/
|
||||||
|
export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||||
const parser = new ParamParser(search, hash);
|
const parser = new ParamParser(search, hash);
|
||||||
|
|
||||||
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||||
|
|
||||||
let intent = parser.getParam("intent");
|
|
||||||
if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) {
|
|
||||||
intent = UserIntent.Unknown;
|
|
||||||
}
|
|
||||||
const widgetId = parser.getParam("widgetId");
|
const widgetId = parser.getParam("widgetId");
|
||||||
const parentUrl = parser.getParam("parentUrl");
|
const parentUrl = parser.getParam("parentUrl");
|
||||||
const isWidget = !!widgetId && !!parentUrl;
|
const isWidget = !!widgetId && !!parentUrl;
|
||||||
|
|
||||||
return {
|
/**
|
||||||
|
* The user's intent with respect to the call.
|
||||||
|
* e.g. if they clicked a Start Call button, this would be `start_call`.
|
||||||
|
* If it was a Join Call button, it would be `join_existing`.
|
||||||
|
* This is a platform specific default set of parameters, that allows to minize the configuration
|
||||||
|
* needed to start a call. And empowers the EC codebase to control the platform/intent behavior in
|
||||||
|
* a central place.
|
||||||
|
*
|
||||||
|
* In short: either provide url query parameters of UrlConfiguration or set the intent
|
||||||
|
* (or the global defaults will be used).
|
||||||
|
*/
|
||||||
|
const intent = !isWidget
|
||||||
|
? UserIntent.Unknown
|
||||||
|
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
||||||
|
// Here we only use constants and `platform` to determine the intent preset.
|
||||||
|
let intentPreset: UrlConfiguration = {
|
||||||
|
confineToRoom: true,
|
||||||
|
appPrompt: false,
|
||||||
|
preload: false,
|
||||||
|
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||||
|
showControls: true,
|
||||||
|
hideScreensharing: false,
|
||||||
|
allowIceFallback: true,
|
||||||
|
perParticipantE2EE: true,
|
||||||
|
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||||
|
skipLobby: true,
|
||||||
|
returnToLobby: false,
|
||||||
|
sendNotificationType: "notification",
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
|
waitForCallPickup: false,
|
||||||
|
};
|
||||||
|
switch (intent) {
|
||||||
|
case UserIntent.StartNewCall:
|
||||||
|
intentPreset.skipLobby = false;
|
||||||
|
intentPreset.callIntent = "video";
|
||||||
|
break;
|
||||||
|
case UserIntent.JoinExistingCall:
|
||||||
|
// On desktop this will be overridden based on which button was used to join the call
|
||||||
|
intentPreset.skipLobby = false;
|
||||||
|
intentPreset.callIntent = "video";
|
||||||
|
break;
|
||||||
|
case UserIntent.StartNewCallDMVoice:
|
||||||
|
intentPreset.callIntent = "audio";
|
||||||
|
// Fall through
|
||||||
|
case UserIntent.StartNewCallDM:
|
||||||
|
intentPreset.skipLobby = true;
|
||||||
|
intentPreset.sendNotificationType = "ring";
|
||||||
|
intentPreset.autoLeaveWhenOthersLeft = true;
|
||||||
|
intentPreset.waitForCallPickup = true;
|
||||||
|
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
||||||
|
break;
|
||||||
|
case UserIntent.JoinExistingCallDMVoice:
|
||||||
|
intentPreset.callIntent = "audio";
|
||||||
|
// Fall through
|
||||||
|
case UserIntent.JoinExistingCallDM:
|
||||||
|
// On desktop this will be overridden based on which button was used to join the call
|
||||||
|
intentPreset.skipLobby = true;
|
||||||
|
intentPreset.autoLeaveWhenOthersLeft = true;
|
||||||
|
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
||||||
|
break;
|
||||||
|
// Non widget usecase defaults
|
||||||
|
default:
|
||||||
|
intentPreset = {
|
||||||
|
confineToRoom: false,
|
||||||
|
appPrompt: true,
|
||||||
|
preload: false,
|
||||||
|
header: HeaderStyle.Standard,
|
||||||
|
showControls: true,
|
||||||
|
hideScreensharing: false,
|
||||||
|
allowIceFallback: false,
|
||||||
|
perParticipantE2EE: false,
|
||||||
|
controlledAudioDevices: false,
|
||||||
|
skipLobby: false,
|
||||||
|
returnToLobby: false,
|
||||||
|
sendNotificationType: undefined,
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
|
waitForCallPickup: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const intentAndPlatformDerivedConfiguration: IntentAndPlatformDerivedConfiguration =
|
||||||
|
{};
|
||||||
|
// Desktop also includes web. Its anything that is not mobile.
|
||||||
|
const desktopMobile = platform === "desktop" ? "desktop" : "mobile";
|
||||||
|
switch (desktopMobile) {
|
||||||
|
case "desktop":
|
||||||
|
case "mobile":
|
||||||
|
switch (intent) {
|
||||||
|
case UserIntent.StartNewCall:
|
||||||
|
case UserIntent.JoinExistingCall:
|
||||||
|
case UserIntent.StartNewCallDM:
|
||||||
|
case UserIntent.JoinExistingCallDM:
|
||||||
|
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
||||||
|
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = true;
|
||||||
|
break;
|
||||||
|
case UserIntent.StartNewCallDMVoice:
|
||||||
|
case UserIntent.JoinExistingCallDMVoice:
|
||||||
|
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
||||||
|
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties: UrlProperties = {
|
||||||
widgetId,
|
widgetId,
|
||||||
parentUrl,
|
parentUrl,
|
||||||
|
|
||||||
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
|
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
|
||||||
// what would we do if it were invalid? If the widget API says that's what
|
// what would we do if it were invalid? If the widget API says that's what
|
||||||
// the room ID is, then that's what it is.
|
// the room ID is, then that's what it is.
|
||||||
roomId: parser.getParam("roomId"),
|
roomId: parser.getParam("roomId"),
|
||||||
password: parser.getParam("password"),
|
password: parser.getParam("password"),
|
||||||
// This flag has 'embed' as an alias for historical reasons
|
|
||||||
confineToRoom:
|
|
||||||
parser.getFlagParam("confineToRoom") || parser.getFlagParam("embed"),
|
|
||||||
appPrompt: parser.getFlagParam("appPrompt", true),
|
|
||||||
preload: isWidget ? parser.getFlagParam("preload") : false,
|
|
||||||
hideHeader: parser.getFlagParam("hideHeader"),
|
|
||||||
showControls: parser.getFlagParam("showControls", true),
|
|
||||||
hideScreensharing: parser.getFlagParam("hideScreensharing"),
|
|
||||||
e2eEnabled: parser.getFlagParam("enableE2EE", true),
|
|
||||||
userId: isWidget ? parser.getParam("userId") : null,
|
userId: isWidget ? parser.getParam("userId") : null,
|
||||||
displayName: parser.getParam("displayName"),
|
displayName: parser.getParam("displayName"),
|
||||||
deviceId: isWidget ? parser.getParam("deviceId") : null,
|
deviceId: isWidget ? parser.getParam("deviceId") : null,
|
||||||
@@ -279,19 +490,9 @@ export const getUrlParams = (
|
|||||||
lang: parser.getParam("lang"),
|
lang: parser.getParam("lang"),
|
||||||
fonts: parser.getAllParams("font"),
|
fonts: parser.getAllParams("font"),
|
||||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||||
allowIceFallback: parser.getFlagParam("allowIceFallback"),
|
|
||||||
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
|
|
||||||
skipLobby: parser.getFlagParam(
|
|
||||||
"skipLobby",
|
|
||||||
isWidget && intent === UserIntent.StartNewCall,
|
|
||||||
),
|
|
||||||
// In SPA mode the user should always exit to the home screen when hanging
|
|
||||||
// up, rather than being sent back to the lobby
|
|
||||||
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : false,
|
|
||||||
theme: parser.getParam("theme"),
|
theme: parser.getParam("theme"),
|
||||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||||
intent,
|
|
||||||
posthogApiHost: parser.getParam("posthogApiHost"),
|
posthogApiHost: parser.getParam("posthogApiHost"),
|
||||||
posthogApiKey: parser.getParam("posthogApiKey"),
|
posthogApiKey: parser.getParam("posthogApiKey"),
|
||||||
posthogUserId:
|
posthogUserId:
|
||||||
@@ -299,6 +500,52 @@ export const getUrlParams = (
|
|||||||
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
|
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
|
||||||
sentryDsn: parser.getParam("sentryDsn"),
|
sentryDsn: parser.getParam("sentryDsn"),
|
||||||
sentryEnvironment: parser.getParam("sentryEnvironment"),
|
sentryEnvironment: parser.getParam("sentryEnvironment"),
|
||||||
|
e2eEnabled: parser.getFlagParam("enableE2EE", true),
|
||||||
|
};
|
||||||
|
|
||||||
|
const configuration: Partial<UrlConfiguration> = {
|
||||||
|
confineToRoom: parser.getFlag("confineToRoom"),
|
||||||
|
appPrompt: parser.getFlag("appPrompt"),
|
||||||
|
preload: isWidget ? parser.getFlag("preload") : undefined,
|
||||||
|
// Check hideHeader for backwards compatibility. If header is set, hideHeader
|
||||||
|
// is ignored.
|
||||||
|
header: parser.getEnumParam("header", HeaderStyle),
|
||||||
|
showControls: parser.getFlag("showControls"),
|
||||||
|
hideScreensharing: parser.getFlag("hideScreensharing"),
|
||||||
|
allowIceFallback: parser.getFlag("allowIceFallback"),
|
||||||
|
perParticipantE2EE: parser.getFlag("perParticipantE2EE"),
|
||||||
|
controlledAudioDevices: parser.getFlag("controlledAudioDevices"),
|
||||||
|
skipLobby: isWidget ? parser.getFlag("skipLobby") : false,
|
||||||
|
// In SPA mode the user should always exit to the home screen when hanging
|
||||||
|
// up, rather than being sent back to the lobby
|
||||||
|
returnToLobby: isWidget ? parser.getFlag("returnToLobby") : false,
|
||||||
|
sendNotificationType: parser.getEnumParam("sendNotificationType", [
|
||||||
|
"ring",
|
||||||
|
"notification",
|
||||||
|
]),
|
||||||
|
waitForCallPickup: parser.getFlag("waitForCallPickup"),
|
||||||
|
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log the final configuration for debugging purposes.
|
||||||
|
// This will only log when the cache is not yet set.
|
||||||
|
logger.info(
|
||||||
|
"UrlParams: final set of url params\n",
|
||||||
|
"intent:",
|
||||||
|
intent,
|
||||||
|
"\nproperties:",
|
||||||
|
properties,
|
||||||
|
"configuration:",
|
||||||
|
configuration,
|
||||||
|
"intentAndPlatformDerivedConfiguration:",
|
||||||
|
intentAndPlatformDerivedConfiguration,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...properties,
|
||||||
|
...intentPreset,
|
||||||
|
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
||||||
|
...intentAndPlatformDerivedConfiguration,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -357,10 +604,16 @@ export function getRoomIdentifierFromUrl(
|
|||||||
|
|
||||||
// Make sure roomId is valid
|
// Make sure roomId is valid
|
||||||
let roomId: string | null = parser.getParam("roomId");
|
let roomId: string | null = parser.getParam("roomId");
|
||||||
if (!roomId?.startsWith("!")) {
|
if (roomId !== null) {
|
||||||
roomId = null;
|
// Replace any non-printable characters that another client may have inserted.
|
||||||
} else if (!roomId.includes("")) {
|
// For instance on iOS, some copied links end up with zero width characters on the end which get encoded into the URL.
|
||||||
roomId = null;
|
// This isn't valid for a roomId, so we can freely strip the content.
|
||||||
|
roomId = roomId.replaceAll(/^[^ -~]+|[^ -~]+$/g, "");
|
||||||
|
if (!roomId.startsWith("!")) {
|
||||||
|
roomId = null;
|
||||||
|
} else if (!roomId.includes("")) {
|
||||||
|
roomId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
50
src/__snapshots__/AppBar.test.tsx.snap
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`AppBar > renders 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="bar"
|
||||||
|
style="display: block;"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="nav leftNav"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-labelledby="«r0»"
|
||||||
|
class="_icon-button_1pz9o_8"
|
||||||
|
data-kind="primary"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_zr2a0_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 11.034a1 1 0 0 0 .29.702l.005.005c.18.18.43.29.705.29h8a1 1 0 0 0 0-2h-5.586L22 3.445a1 1 0 0 0-1.414-1.414L14 8.617V3.031a1 1 0 1 0-2 0zm0 1.963a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 11 12H3a1 1 0 1 0 0 2h5.586L2 20.586A1 1 0 1 0 3.414 22L10 15.414V21a1 1 0 0 0 2 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav rightNav"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
This is the content.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
exports[`the content is rendered when the modal is open 1`] = `
|
exports[`the content is rendered when the modal is open 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-labelledby="radix-:r4:"
|
aria-labelledby="radix-«r4»"
|
||||||
class="overlay animate modal dialog _glass_1x9g9_17"
|
class="overlay animate modal dialog _glass_sepwu_8"
|
||||||
data-state="open"
|
data-state="open"
|
||||||
id="radix-:r3:"
|
id="radix-«r3»"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
style="pointer-events: auto;"
|
style="pointer-events: auto;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -17,8 +17,8 @@ exports[`the content is rendered when the modal is open 1`] = `
|
|||||||
class="header"
|
class="header"
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
|
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||||
id="radix-:r4:"
|
id="radix-«r4»"
|
||||||
>
|
>
|
||||||
My modal
|
My modal
|
||||||
</h2>
|
</h2>
|
||||||
@@ -36,7 +36,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
|||||||
|
|
||||||
exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-labelledby="radix-:ra:"
|
aria-labelledby="radix-«ra»"
|
||||||
class="overlay modal drawer"
|
class="overlay modal drawer"
|
||||||
data-state="open"
|
data-state="open"
|
||||||
data-vaul-animate="true"
|
data-vaul-animate="true"
|
||||||
@@ -45,7 +45,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
|||||||
data-vaul-drawer=""
|
data-vaul-drawer=""
|
||||||
data-vaul-drawer-direction="bottom"
|
data-vaul-drawer-direction="bottom"
|
||||||
data-vaul-snap-points="false"
|
data-vaul-snap-points="false"
|
||||||
id="radix-:r9:"
|
id="radix-«r9»"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
style="pointer-events: auto;"
|
style="pointer-events: auto;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -60,7 +60,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
|||||||
class="handle"
|
class="handle"
|
||||||
/>
|
/>
|
||||||
<h2
|
<h2
|
||||||
id="radix-:ra:"
|
id="radix-«ra»"
|
||||||
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
|
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
|
||||||
>
|
>
|
||||||
My modal
|
My modal
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
exports[`Toast > renders 1`] = `
|
exports[`Toast > renders 1`] = `
|
||||||
<button
|
<button
|
||||||
aria-labelledby="radix-:r4:"
|
aria-labelledby="radix-«r4»"
|
||||||
class="overlay animate toast"
|
class="overlay animate toast"
|
||||||
data-state="open"
|
data-state="open"
|
||||||
id="radix-:r3:"
|
id="radix-«r3»"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
style="pointer-events: auto;"
|
style="pointer-events: auto;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45"
|
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36"
|
||||||
id="radix-:r4:"
|
id="radix-«r4»"
|
||||||
>
|
>
|
||||||
Hello world!
|
Hello world!
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -107,13 +107,13 @@ export class RageshakeSpanProcessor implements SpanProcessor {
|
|||||||
startTime,
|
startTime,
|
||||||
duration,
|
duration,
|
||||||
references:
|
references:
|
||||||
span.parentSpanId === undefined
|
span.parentSpanContext?.spanId === undefined
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
refType: "CHILD_OF",
|
refType: "CHILD_OF",
|
||||||
traceID: traceId,
|
traceID: traceId,
|
||||||
spanID: span.parentSpanId,
|
spanID: span.parentSpanContext?.spanId,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tags: dumpAttributes(span.attributes),
|
tags: dumpAttributes(span.attributes),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
EndCallIcon,
|
EndCallIcon,
|
||||||
ShareScreenSolidIcon,
|
ShareScreenSolidIcon,
|
||||||
SettingsSolidIcon,
|
SettingsSolidIcon,
|
||||||
SwitchCameraSolidIcon,
|
|
||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
@@ -67,23 +66,6 @@ export const VideoButton: FC<VideoButtonProps> = ({ muted, ...props }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SwitchCameraButton: FC<ComponentPropsWithoutRef<"button">> = (
|
|
||||||
props,
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip label={t("switch_camera")}>
|
|
||||||
<CpdButton
|
|
||||||
iconOnly
|
|
||||||
Icon={SwitchCameraSolidIcon}
|
|
||||||
kind="secondary"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type ComponentProps, type FC, type MouseEvent } from "react";
|
||||||
type ComponentPropsWithoutRef,
|
|
||||||
forwardRef,
|
|
||||||
type MouseEvent,
|
|
||||||
} from "react";
|
|
||||||
import { Link as CpdLink } from "@vector-im/compound-web";
|
import { Link as CpdLink } from "@vector-im/compound-web";
|
||||||
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
|
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@@ -26,31 +22,30 @@ export function useLink(
|
|||||||
return [href, onClick];
|
return [href, onClick];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<ComponentProps<typeof CpdLink>, "href" | "onClick"> & {
|
||||||
ComponentPropsWithoutRef<typeof CpdLink>,
|
to: LinkProps["to"];
|
||||||
"href" | "onClick"
|
state?: unknown;
|
||||||
> & { to: LinkProps["to"]; state?: unknown };
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A version of Compound's link component that integrates with our router setup.
|
* A version of Compound's link component that integrates with our router setup.
|
||||||
* This is only for app-internal links.
|
* This is only for app-internal links.
|
||||||
*/
|
*/
|
||||||
export const Link = forwardRef<HTMLAnchorElement, Props>(function Link(
|
export const Link: FC<Props> = ({ ref, to, state, ...props }) => {
|
||||||
{ to, state, ...props },
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const [path, onClick] = useLink(to, state);
|
const [path, onClick] = useLink(to, state);
|
||||||
return <CpdLink ref={ref} {...props} href={path} onClick={onClick} />;
|
return <CpdLink ref={ref} {...props} href={path} onClick={onClick} />;
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A link to an external web page, made to fit into blocks of text more subtly
|
* A link to an external web page, made to fit into blocks of text more subtly
|
||||||
* than the normal Compound link component.
|
* than the normal Compound link component.
|
||||||
*/
|
*/
|
||||||
export const ExternalLink = forwardRef<
|
export const ExternalLink: FC<ComponentProps<"a">> = ({
|
||||||
HTMLAnchorElement,
|
ref,
|
||||||
ComponentPropsWithoutRef<"a">
|
className,
|
||||||
>(function ExternalLink({ className, children, ...props }, ref) {
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -62,4 +57,4 @@ export const ExternalLink = forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -5,24 +5,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ComponentPropsWithoutRef, forwardRef } from "react";
|
import { type ComponentProps, type FC } from "react";
|
||||||
import { Button } from "@vector-im/compound-web";
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import type { LinkProps } from "react-router-dom";
|
import type { LinkProps } from "react-router-dom";
|
||||||
import { useLink } from "./Link";
|
import { useLink } from "./Link";
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<ComponentProps<typeof Button<"a">>, "as" | "href"> & {
|
||||||
ComponentPropsWithoutRef<typeof Button<"a">>,
|
to: LinkProps["to"];
|
||||||
"as" | "href"
|
state?: unknown;
|
||||||
> & { to: LinkProps["to"]; state?: unknown };
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A version of Compound's button component that acts as a link and integrates
|
* A version of Compound's button component that acts as a link and integrates
|
||||||
* with our router setup.
|
* with our router setup.
|
||||||
*/
|
*/
|
||||||
export const LinkButton = forwardRef<HTMLAnchorElement, Props>(
|
export const LinkButton: FC<Props> = ({ ref, to, state, ...props }) => {
|
||||||
function LinkButton({ to, state, ...props }, ref) {
|
const [path, onClick] = useLink(to, state);
|
||||||
const [path, onClick] = useLink(to, state);
|
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
||||||
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { act, render } from "@testing-library/react";
|
import { act, render } from "@testing-library/react";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test, vi } from "vitest";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { userEvent } from "@testing-library/user-event";
|
import { userEvent } from "@testing-library/user-event";
|
||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
|
||||||
|
|
||||||
import { ReactionToggleButton } from "./ReactionToggleButton";
|
import { ReactionToggleButton } from "./ReactionToggleButton";
|
||||||
import { ElementCallReactionEventType } from "../reactions";
|
import { ElementCallReactionEventType } from "../reactions";
|
||||||
@@ -20,7 +19,9 @@ import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
|||||||
import { type MockRTCSession } from "../utils/test";
|
import { type MockRTCSession } from "../utils/test";
|
||||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||||
|
|
||||||
const localIdent = `${localRtcMember.sender}:${localRtcMember.deviceId}`;
|
vi.mock("livekit-client/e2ee-worker?worker");
|
||||||
|
|
||||||
|
const localIdent = `${localRtcMember.userId}:${localRtcMember.deviceId}`;
|
||||||
|
|
||||||
function TestComponent({
|
function TestComponent({
|
||||||
rtcSession,
|
rtcSession,
|
||||||
@@ -33,7 +34,7 @@ function TestComponent({
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<ReactionsSenderProvider
|
<ReactionsSenderProvider
|
||||||
vm={vm}
|
vm={vm}
|
||||||
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
rtcSession={rtcSession.asMockedSession()}
|
||||||
>
|
>
|
||||||
<ReactionToggleButton vm={vm} identifier={localIdent} />
|
<ReactionToggleButton vm={vm} identifier={localIdent} />
|
||||||
</ReactionsSenderProvider>
|
</ReactionsSenderProvider>
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useObservableState } from "observable-hooks";
|
|
||||||
import { map } from "rxjs";
|
|
||||||
|
|
||||||
import { useReactionsSender } from "../reactions/useReactionsSender";
|
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||||
import styles from "./ReactionToggleButton.module.css";
|
import styles from "./ReactionToggleButton.module.css";
|
||||||
@@ -36,6 +34,7 @@ import {
|
|||||||
} from "../reactions";
|
} from "../reactions";
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import { type CallViewModel } from "../state/CallViewModel";
|
import { type CallViewModel } from "../state/CallViewModel";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
|
||||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
raised: boolean;
|
raised: boolean;
|
||||||
@@ -180,12 +179,8 @@ export function ReactionToggleButton({
|
|||||||
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
||||||
const [errorText, setErrorText] = useState<string>();
|
const [errorText, setErrorText] = useState<string>();
|
||||||
|
|
||||||
const isHandRaised = useObservableState(
|
const isHandRaised = !!useBehavior(vm.handsRaised$)[identifier];
|
||||||
vm.handsRaised$.pipe(map((v) => !!v[identifier])),
|
const canReact = !useBehavior(vm.reactions$)[identifier];
|
||||||
);
|
|
||||||
const canReact = useObservableState(
|
|
||||||
vm.reactions$.pipe(map((v) => !v[identifier])),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clear whenever the reactions menu state changes.
|
// Clear whenever the reactions menu state changes.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ exports[`Can close reaction dialog 1`] = `
|
|||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby=":rb5:"
|
aria-labelledby="«rbb»"
|
||||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -26,7 +26,7 @@ exports[`Can close reaction dialog 1`] = `
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -43,8 +43,8 @@ exports[`Can fully expand emoji picker 1`] = `
|
|||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby=":r7m:"
|
aria-labelledby="«r7m»"
|
||||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -60,7 +60,7 @@ exports[`Can fully expand emoji picker 1`] = `
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -74,8 +74,8 @@ exports[`Can lower hand 1`] = `
|
|||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby=":r36:"
|
aria-labelledby="«r36»"
|
||||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||||
data-kind="secondary"
|
data-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -91,7 +91,7 @@ exports[`Can lower hand 1`] = `
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -108,8 +108,8 @@ exports[`Can open menu 1`] = `
|
|||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby=":r0:"
|
aria-labelledby="«r0»"
|
||||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -125,7 +125,7 @@ exports[`Can open menu 1`] = `
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -139,8 +139,8 @@ exports[`Can raise hand 1`] = `
|
|||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby=":r1j:"
|
aria-labelledby="«r1j»"
|
||||||
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
|
class="_button_vczzf_8 raisedButton _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -155,7 +155,7 @@ exports[`Can raise hand 1`] = `
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
|
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.943 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 1.999 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface ConfigOptions {
|
|||||||
api_key: string;
|
api_key: string;
|
||||||
api_host: string;
|
api_host: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Sentry endpoint to which crash data will be sent.
|
* The Sentry endpoint to which crash data will be sent.
|
||||||
* This is only used in the full package of Element Call.
|
* This is only used in the full package of Element Call.
|
||||||
@@ -22,6 +23,7 @@ export interface ConfigOptions {
|
|||||||
DSN: string;
|
DSN: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The rageshake server to which feedback and debug logs will be sent.
|
* The rageshake server to which feedback and debug logs will be sent.
|
||||||
* This is only used in the full package of Element Call.
|
* This is only used in the full package of Element Call.
|
||||||
@@ -66,6 +68,7 @@ export interface ConfigOptions {
|
|||||||
* Allow to join group calls without audio and video.
|
* Allow to join group calls without audio and video.
|
||||||
*/
|
*/
|
||||||
feature_group_calls_without_video_and_audio?: boolean;
|
feature_group_calls_without_video_and_audio?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send device-specific call session membership state events instead of
|
* Send device-specific call session membership state events instead of
|
||||||
* legacy user-specific call membership state events.
|
* legacy user-specific call membership state events.
|
||||||
@@ -86,6 +89,7 @@ export interface ConfigOptions {
|
|||||||
* Defines whether participants should start with audio enabled by default.
|
* Defines whether participants should start with audio enabled by default.
|
||||||
*/
|
*/
|
||||||
enable_audio?: boolean;
|
enable_audio?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines whether participants should start with video enabled by default.
|
* Defines whether participants should start with video enabled by default.
|
||||||
*/
|
*/
|
||||||
@@ -109,19 +113,43 @@ export interface ConfigOptions {
|
|||||||
* How long (in milliseconds) to wait before rotating end-to-end media encryption keys
|
* How long (in milliseconds) to wait before rotating end-to-end media encryption keys
|
||||||
* when someone leaves a call.
|
* when someone leaves a call.
|
||||||
*/
|
*/
|
||||||
key_rotation_on_leave_delay?: number;
|
wait_for_key_rotation_ms?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How often (in milliseconds) keep-alive messages should be sent to the server for
|
* The duration (in milliseconds) after the most recent keep-alive (delayed leave event restart)
|
||||||
* the MatrixRTC membership event.
|
* that the server waits before sending the leave MatrixRTC membership event.
|
||||||
*/
|
*/
|
||||||
membership_keep_alive_period?: number;
|
delayed_leave_event_delay_ms?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long (in milliseconds) after the last keep-alive the server should expire the
|
* The time (in milliseconds) after which a we consider a delayed event restart http request to have failed.
|
||||||
* MatrixRTC membership event.
|
* Setting this to a lower value will result in more frequent retries but also a higher chance of failiour.
|
||||||
|
*
|
||||||
|
* In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs
|
||||||
|
* helps by keeping more delayed event reset candidates in flight,
|
||||||
|
* improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration,
|
||||||
|
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
||||||
*/
|
*/
|
||||||
membership_server_side_expiry_timeout?: number;
|
delayed_leave_event_restart_local_timeout_ms?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time interval (in milliseconds) at which the client sends membership keep-alive
|
||||||
|
* messages to the server by restarting the timer for the delayed leave event.
|
||||||
|
*/
|
||||||
|
delayed_leave_event_restart_ms?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long we wait before retrying after a network error on any of the requests.
|
||||||
|
*/
|
||||||
|
network_error_retry_ms?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timeout (in milliseconds) after we joined the call, that our membership should expire
|
||||||
|
* unless we have explicitly updated it.
|
||||||
|
*
|
||||||
|
* This is what goes into the m.rtc.member event expiry field and is typically set to a number of hours.
|
||||||
|
*/
|
||||||
|
membership_event_expiry_ms?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,72 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024-2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
export interface Controls {
|
export interface Controls {
|
||||||
canEnterPip: () => boolean;
|
canEnterPip(): boolean;
|
||||||
enablePip: () => void;
|
enablePip(): void;
|
||||||
disablePip: () => void;
|
disablePip(): void;
|
||||||
|
|
||||||
|
setAvailableAudioDevices(devices: OutputDevice[]): void;
|
||||||
|
setAudioDevice(id: string): void;
|
||||||
|
onAudioDeviceSelect?: (id: string) => void;
|
||||||
|
onAudioPlaybackStarted?: () => void;
|
||||||
|
setAudioEnabled(enabled: boolean): void;
|
||||||
|
showNativeAudioDevicePicker?: () => void;
|
||||||
|
onBackButtonPressed?: () => void;
|
||||||
|
|
||||||
|
/** @deprecated use setAvailableAudioDevices instead*/
|
||||||
|
setAvailableOutputDevices(devices: OutputDevice[]): void;
|
||||||
|
/** @deprecated use setAudioDevice instead*/
|
||||||
|
setOutputDevice(id: string): void;
|
||||||
|
/** @deprecated use onAudioDeviceSelect instead*/
|
||||||
|
onOutputDeviceSelect?: (id: string) => void;
|
||||||
|
/** @deprecated use setAudioEnabled instead*/
|
||||||
|
setOutputEnabled(enabled: boolean): void;
|
||||||
|
/** @deprecated use showNativeAudioDevicePicker instead*/
|
||||||
|
showNativeOutputDevicePicker?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OutputDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
forEarpiece?: boolean;
|
||||||
|
isEarpiece?: boolean;
|
||||||
|
isSpeaker?: boolean;
|
||||||
|
isExternalHeadset?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If pipMode is enabled, EC will render a adapted call view layout.
|
||||||
|
*/
|
||||||
export const setPipEnabled$ = new Subject<boolean>();
|
export const setPipEnabled$ = new Subject<boolean>();
|
||||||
|
|
||||||
|
export const availableOutputDevices$ = new Subject<OutputDevice[]>();
|
||||||
|
|
||||||
|
export const outputDevice$ = new Subject<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This allows the os to mute the call if the user
|
||||||
|
* presses the volume down button when it is at the minimum volume.
|
||||||
|
*
|
||||||
|
* This should also be used to display a darkened overlay screen letting the user know that audio is muted.
|
||||||
|
*/
|
||||||
|
export const setAudioEnabled$ = new Subject<boolean>();
|
||||||
|
|
||||||
|
let playbackStartedEmitted = false;
|
||||||
|
export const setPlaybackStarted = (): void => {
|
||||||
|
if (!playbackStartedEmitted) {
|
||||||
|
playbackStartedEmitted = true;
|
||||||
|
window.controls.onAudioPlaybackStarted?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.controls = {
|
window.controls = {
|
||||||
canEnterPip(): boolean {
|
canEnterPip(): boolean {
|
||||||
return setPipEnabled$.observed;
|
return setPipEnabled$.observed;
|
||||||
@@ -27,4 +79,32 @@ window.controls = {
|
|||||||
if (!setPipEnabled$.observed) throw new Error("No call is running");
|
if (!setPipEnabled$.observed) throw new Error("No call is running");
|
||||||
setPipEnabled$.next(false);
|
setPipEnabled$.next(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAvailableAudioDevices(devices: OutputDevice[]): void {
|
||||||
|
logger.info("setAvailableAudioDevices called from native:", devices);
|
||||||
|
availableOutputDevices$.next(devices);
|
||||||
|
},
|
||||||
|
setAudioDevice(id: string): void {
|
||||||
|
logger.info("setAudioDevice called from native", id);
|
||||||
|
outputDevice$.next(id);
|
||||||
|
},
|
||||||
|
setAudioEnabled(enabled: boolean): void {
|
||||||
|
logger.info("setAudioEnabled called from native:", enabled);
|
||||||
|
if (!setAudioEnabled$.observed)
|
||||||
|
throw new Error(
|
||||||
|
"Output controls are disabled. No setAudioEnabled$ observer",
|
||||||
|
);
|
||||||
|
setAudioEnabled$.next(enabled);
|
||||||
|
},
|
||||||
|
|
||||||
|
// wrappers for the deprecated controls fields
|
||||||
|
setOutputEnabled(enabled: boolean): void {
|
||||||
|
this.setAudioEnabled(enabled);
|
||||||
|
},
|
||||||
|
setAvailableOutputDevices(devices: OutputDevice[]): void {
|
||||||
|
this.setAvailableAudioDevices(devices);
|
||||||
|
},
|
||||||
|
setOutputDevice(id: string): void {
|
||||||
|
this.setAudioDevice(id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,58 +6,70 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
|
import {
|
||||||
import { type UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
|
setLocalStorageItemReactive,
|
||||||
|
useLocalStorage,
|
||||||
|
} from "../useLocalStorage";
|
||||||
|
import { getUrlParams } from "../UrlParams";
|
||||||
import { E2eeType } from "./e2eeType";
|
import { E2eeType } from "./e2eeType";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This setter will update the state for all `useRoomSharedKey` hooks
|
||||||
|
* if the password is different from the one in local storage or if its not yet in the local storage.
|
||||||
|
*/
|
||||||
export function saveKeyForRoom(roomId: string, password: string): void {
|
export function saveKeyForRoom(roomId: string, password: string): void {
|
||||||
setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password);
|
if (
|
||||||
|
localStorage.getItem(getRoomSharedKeyLocalStorageKey(roomId)) !== password
|
||||||
|
)
|
||||||
|
setLocalStorageItemReactive(
|
||||||
|
getRoomSharedKeyLocalStorageKey(roomId),
|
||||||
|
password,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
|
const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
|
||||||
`room-shared-key-${roomId}`;
|
`room-shared-key-${roomId}`;
|
||||||
|
|
||||||
const useInternalRoomSharedKey = (roomId: string): string | null => {
|
/**
|
||||||
const key = getRoomSharedKeyLocalStorageKey(roomId);
|
* An upto-date shared key for the room. Either from local storage or the value from `setInitialValue`.
|
||||||
const roomSharedKey = useLocalStorage(key)[0];
|
* @param roomId
|
||||||
|
* @param setInitialValue The value we get from the URL. The hook will overwrite the local storage value with this.
|
||||||
|
* @returns [roomSharedKey, setRoomSharedKey] like a react useState hook.
|
||||||
|
*/
|
||||||
|
const useRoomSharedKey = (
|
||||||
|
roomId: string,
|
||||||
|
setInitialValue?: string,
|
||||||
|
): [string | null, setKey: (key: string) => void] => {
|
||||||
|
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(
|
||||||
|
getRoomSharedKeyLocalStorageKey(roomId),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
// If setInitialValue is available, update the local storage (usually the password from the url).
|
||||||
|
// This will update roomSharedKey but wont update the returned value since
|
||||||
|
// that already defaults to setInitialValue.
|
||||||
|
if (setInitialValue) setRoomSharedKey(setInitialValue);
|
||||||
|
}, [setInitialValue, setRoomSharedKey]);
|
||||||
|
|
||||||
return roomSharedKey;
|
// make sure we never return the initial null value from `useLocalStorage`
|
||||||
|
return [setInitialValue ?? roomSharedKey, setRoomSharedKey];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getKeyForRoom(roomId: string): string | null {
|
export function getKeyForRoom(roomId: string): string | null {
|
||||||
saveKeyFromUrlParams(getUrlParams());
|
const { roomId: urlRoomId, password } = getUrlParams();
|
||||||
const key = getRoomSharedKeyLocalStorageKey(roomId);
|
if (roomId !== urlRoomId)
|
||||||
return localStorage.getItem(key);
|
logger.warn(
|
||||||
|
"requested key for a roomId which is not the current call room id (from the URL)",
|
||||||
|
roomId,
|
||||||
|
urlRoomId,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
password ?? localStorage.getItem(getRoomSharedKeyLocalStorageKey(roomId))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveKeyFromUrlParams(urlParams: UrlParams): void {
|
|
||||||
if (!urlParams.password || !urlParams.roomId) return;
|
|
||||||
|
|
||||||
// Take the key from the URL and save it.
|
|
||||||
// It's important to always use the room ID specified in the URL
|
|
||||||
// when saving keys rather than whatever the current room ID might be,
|
|
||||||
// in case we've moved to a different room but the URL hasn't changed.
|
|
||||||
saveKeyForRoom(urlParams.roomId, urlParams.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the room password from the URL if one is present, saving it in localstorage
|
|
||||||
* and returning it in a tuple with the corresponding room ID from the URL.
|
|
||||||
* @returns A tuple of the roomId and password from the URL if the URL has both,
|
|
||||||
* otherwise [undefined, undefined]
|
|
||||||
*/
|
|
||||||
const useKeyFromUrl = (): [string, string] | [undefined, undefined] => {
|
|
||||||
const urlParams = useUrlParams();
|
|
||||||
|
|
||||||
useEffect(() => saveKeyFromUrlParams(urlParams), [urlParams]);
|
|
||||||
|
|
||||||
return urlParams.roomId && urlParams.password
|
|
||||||
? [urlParams.roomId, urlParams.password]
|
|
||||||
: [undefined, undefined];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Unencrypted = { kind: E2eeType.NONE };
|
export type Unencrypted = { kind: E2eeType.NONE };
|
||||||
export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string };
|
export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string };
|
||||||
export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT };
|
export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT };
|
||||||
@@ -66,12 +78,11 @@ export type EncryptionSystem = Unencrypted | SharedSecret | PerParticipantE2EE;
|
|||||||
export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
|
export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
|
||||||
const { client } = useClient();
|
const { client } = useClient();
|
||||||
|
|
||||||
// make sure we've extracted the key from the URL first
|
const [storedPassword] = useRoomSharedKey(
|
||||||
// (and we still need to take the value it returns because
|
getRoomSharedKeyLocalStorageKey(roomId),
|
||||||
// the effect won't run in time for it to save to localstorage in
|
getKeyForRoom(roomId) ?? undefined,
|
||||||
// time for us to read it out again).
|
);
|
||||||
const [urlRoomId, passwordFromUrl] = useKeyFromUrl();
|
|
||||||
const storedPassword = useInternalRoomSharedKey(roomId);
|
|
||||||
const room = client?.getRoom(roomId);
|
const room = client?.getRoom(roomId);
|
||||||
const e2eeSystem = <EncryptionSystem>useMemo(() => {
|
const e2eeSystem = <EncryptionSystem>useMemo(() => {
|
||||||
if (!room) return { kind: E2eeType.NONE };
|
if (!room) return { kind: E2eeType.NONE };
|
||||||
@@ -80,15 +91,10 @@ export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
|
|||||||
kind: E2eeType.SHARED_KEY,
|
kind: E2eeType.SHARED_KEY,
|
||||||
secret: storedPassword,
|
secret: storedPassword,
|
||||||
};
|
};
|
||||||
if (urlRoomId === roomId)
|
|
||||||
return {
|
|
||||||
kind: E2eeType.SHARED_KEY,
|
|
||||||
secret: passwordFromUrl,
|
|
||||||
};
|
|
||||||
if (room.hasEncryptionStateEvent()) {
|
if (room.hasEncryptionStateEvent()) {
|
||||||
return { kind: E2eeType.PER_PARTICIPANT };
|
return { kind: E2eeType.PER_PARTICIPANT };
|
||||||
}
|
}
|
||||||
return { kind: E2eeType.NONE };
|
return { kind: E2eeType.NONE };
|
||||||
}, [passwordFromUrl, room, roomId, storedPassword, urlRoomId]);
|
}, [room, storedPassword]);
|
||||||
return e2eeSystem;
|
return e2eeSystem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,28 +6,32 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { type FormEventHandler, forwardRef, type ReactNode } from "react";
|
import {
|
||||||
|
type FC,
|
||||||
|
type Ref,
|
||||||
|
type FormEventHandler,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import styles from "./Form.module.css";
|
import styles from "./Form.module.css";
|
||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
|
ref?: Ref<HTMLFormElement>;
|
||||||
className: string;
|
className: string;
|
||||||
onSubmit: FormEventHandler<HTMLFormElement>;
|
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||||
children: ReactNode[];
|
children: ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Form = forwardRef<HTMLFormElement, FormProps>(
|
export const Form: FC<FormProps> = ({ ref, children, className, onSubmit }) => {
|
||||||
({ children, className, onSubmit }, ref) => {
|
return (
|
||||||
return (
|
<form
|
||||||
<form
|
onSubmit={onSubmit}
|
||||||
onSubmit={onSubmit}
|
className={classNames(styles.form, className)}
|
||||||
className={classNames(styles.form, className)}
|
ref={ref}
|
||||||
ref={ref}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</form>
|
||||||
</form>
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Form.displayName = "Form";
|
Form.displayName = "Form";
|
||||||
|
|||||||
@@ -18,23 +18,22 @@ import {
|
|||||||
type ComponentType,
|
type ComponentType,
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
type FC,
|
type FC,
|
||||||
type LegacyRef,
|
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
|
type Ref,
|
||||||
type SetStateAction,
|
type SetStateAction,
|
||||||
createContext,
|
createContext,
|
||||||
forwardRef,
|
|
||||||
memo,
|
memo,
|
||||||
useContext,
|
use,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
useSyncExternalStore,
|
||||||
} from "react";
|
} from "react";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
|
||||||
import { fromEvent, map, startWith } from "rxjs";
|
|
||||||
|
|
||||||
import styles from "./Grid.module.css";
|
import styles from "./Grid.module.css";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
@@ -124,7 +123,7 @@ interface LayoutContext {
|
|||||||
const LayoutContext = createContext<LayoutContext | null>(null);
|
const LayoutContext = createContext<LayoutContext | null>(null);
|
||||||
|
|
||||||
function useLayoutContext(): LayoutContext {
|
function useLayoutContext(): LayoutContext {
|
||||||
const context = useContext(LayoutContext);
|
const context = use(LayoutContext);
|
||||||
if (context === null)
|
if (context === null)
|
||||||
throw new Error("useUpdateLayout called outside a Grid layout context");
|
throw new Error("useUpdateLayout called outside a Grid layout context");
|
||||||
return context;
|
return context;
|
||||||
@@ -156,13 +155,8 @@ export function useVisibleTiles(callback: VisibleTilesCallback): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowHeightObservable$ = fromEvent(window, "resize").pipe(
|
|
||||||
startWith(null),
|
|
||||||
map(() => window.innerHeight),
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
||||||
ref: LegacyRef<R>;
|
ref?: Ref<R>;
|
||||||
model: LayoutModel;
|
model: LayoutModel;
|
||||||
/**
|
/**
|
||||||
* Component creating an invisible "slot" for a tile to go in.
|
* Component creating an invisible "slot" for a tile to go in.
|
||||||
@@ -171,7 +165,7 @@ export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TileProps<Model, R extends HTMLElement> {
|
export interface TileProps<Model, R extends HTMLElement> {
|
||||||
ref: LegacyRef<R>;
|
ref?: Ref<R>;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: ComponentProps<typeof animated.div>["style"];
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
/**
|
/**
|
||||||
@@ -262,7 +256,13 @@ export function Grid<
|
|||||||
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
||||||
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
||||||
|
|
||||||
const windowHeight = useObservableEagerState(windowHeightObservable$);
|
const windowHeight = useSyncExternalStore(
|
||||||
|
useCallback((onChange) => {
|
||||||
|
window.addEventListener("resize", onChange);
|
||||||
|
return (): void => window.removeEventListener("resize", onChange);
|
||||||
|
}, []),
|
||||||
|
useCallback(() => window.innerHeight, []),
|
||||||
|
);
|
||||||
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
|
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
|
||||||
const [generation, setGeneration] = useState<number | null>(null);
|
const [generation, setGeneration] = useState<number | null>(null);
|
||||||
const [visibleTilesCallback, setVisibleTilesCallback] =
|
const [visibleTilesCallback, setVisibleTilesCallback] =
|
||||||
@@ -297,14 +297,13 @@ export function Grid<
|
|||||||
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
|
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
|
||||||
const LayoutMemo = useMemo(
|
const LayoutMemo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
memo(
|
memo(function LayoutMemo({
|
||||||
forwardRef<
|
ref,
|
||||||
LayoutRef,
|
Layout,
|
||||||
LayoutMemoProps<LayoutModel, TileModel, LayoutRef>
|
...props
|
||||||
>(function LayoutMemo({ Layout, ...props }, ref): ReactNode {
|
}: LayoutMemoProps<LayoutModel, TileModel, LayoutRef>): ReactNode {
|
||||||
return <Layout {...props} ref={ref} />;
|
return <Layout {...props} ref={ref} />;
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -532,14 +531,14 @@ export function Grid<
|
|||||||
className={classNames(className, styles.grid)}
|
className={classNames(className, styles.grid)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<LayoutContext.Provider value={context}>
|
<LayoutContext value={context}>
|
||||||
<LayoutMemo
|
<LayoutMemo
|
||||||
ref={setLayoutRoot}
|
ref={setLayoutRoot}
|
||||||
Layout={Layout}
|
Layout={Layout}
|
||||||
model={model}
|
model={model}
|
||||||
Slot={Slot}
|
Slot={Slot}
|
||||||
/>
|
/>
|
||||||
</LayoutContext.Provider>
|
</LayoutContext>
|
||||||
{tileTransitions((spring, { id, model, onDrag, width, height }) => (
|
{tileTransitions((spring, { id, model, onDrag, width, height }) => (
|
||||||
<TileWrapper
|
<TileWrapper
|
||||||
key={id}
|
key={id}
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type CSSProperties, forwardRef, useCallback, useMemo } from "react";
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { distinctUntilChanged } from "rxjs";
|
import { distinctUntilChanged } from "rxjs";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
|
||||||
import { type GridLayout as GridLayoutModel } from "../state/CallViewModel";
|
import { type GridLayout as GridLayoutModel } from "../state/layout-types.ts";
|
||||||
import styles from "./GridLayout.module.css";
|
import styles from "./GridLayout.module.css";
|
||||||
import { useInitial } from "../useInitial";
|
import { useInitial } from "../useInitial";
|
||||||
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
||||||
@@ -33,7 +38,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
|
|
||||||
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
||||||
// lives
|
// lives
|
||||||
fixed: forwardRef(function GridLayoutFixed({ model, Slot }, ref) {
|
fixed: function GridLayoutFixed({ ref, model, Slot }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const alignment = useObservableEagerState(
|
const alignment = useObservableEagerState(
|
||||||
useInitial(() =>
|
useInitial(() =>
|
||||||
@@ -68,10 +73,10 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
// The scrolling part of the layout is where all the grid tiles live
|
// The scrolling part of the layout is where all the grid tiles live
|
||||||
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
|
scrolling: function GridLayout({ ref, model, Slot }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useVisibleTiles(model.setVisibleTiles);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
const { width, height: minHeight } = useObservableEagerState(minBounds$);
|
const { width, height: minHeight } = useObservableEagerState(minBounds$);
|
||||||
@@ -98,5 +103,5 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef, useCallback, useMemo } from "react";
|
import { type ReactNode, useCallback, useMemo } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { type OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel";
|
import { type OneOnOneLayout as OneOnOneLayoutModel } from "../state/layout-types.ts";
|
||||||
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
||||||
import styles from "./OneOnOneLayout.module.css";
|
import styles from "./OneOnOneLayout.module.css";
|
||||||
import { type DragCallback, useUpdateLayout } from "./Grid";
|
import { type DragCallback, useUpdateLayout } from "./Grid";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the "one-on-one" layout, in which the remote participant
|
* An implementation of the "one-on-one" layout, in which the remote participant
|
||||||
@@ -24,15 +25,15 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
}) => ({
|
}) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function OneOnOneLayoutFixed(_props, ref) {
|
fixed: function OneOnOneLayoutFixed({ ref }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
return <div ref={ref} />;
|
return <div ref={ref} />;
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
|
scrolling: function OneOnOneLayoutScrolling({ ref, model, Slot }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const { width, height } = useObservableEagerState(minBounds$);
|
const { width, height } = useObservableEagerState(minBounds$);
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
const pipAlignmentValue = useBehavior(pipAlignment$);
|
||||||
const { tileWidth, tileHeight } = useMemo(
|
const { tileWidth, tileHeight } = useMemo(
|
||||||
() => arrangeTiles(width, height, 1),
|
() => arrangeTiles(width, height, 1),
|
||||||
[width, height],
|
[width, height],
|
||||||
@@ -66,5 +67,5 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
</Slot>
|
</Slot>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef, useCallback } from "react";
|
import { type ReactNode, useCallback } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
|
||||||
|
|
||||||
import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
|
import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/layout-types.ts";
|
||||||
import { type CallLayout } from "./CallLayout";
|
import { type CallLayout } from "./CallLayout";
|
||||||
import { type DragCallback, useUpdateLayout } from "./Grid";
|
import { type DragCallback, useUpdateLayout } from "./Grid";
|
||||||
import styles from "./SpotlightExpandedLayout.module.css";
|
import styles from "./SpotlightExpandedLayout.module.css";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the "expanded spotlight" layout, in which the spotlight
|
* An implementation of the "expanded spotlight" layout, in which the spotlight
|
||||||
@@ -22,10 +22,11 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
> = ({ pipAlignment$ }) => ({
|
> = ({ pipAlignment$ }) => ({
|
||||||
scrollingOnTop: true,
|
scrollingOnTop: true,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
|
fixed: function SpotlightExpandedLayoutFixed({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,14 +38,15 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function SpotlightExpandedLayoutScrolling(
|
scrolling: function SpotlightExpandedLayoutScrolling({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
const pipAlignmentValue = useBehavior(pipAlignment$);
|
||||||
|
|
||||||
const onDragPip: DragCallback = useCallback(
|
const onDragPip: DragCallback = useCallback(
|
||||||
({ xRatio, yRatio }) =>
|
({ xRatio, yRatio }) =>
|
||||||
@@ -69,5 +71,5 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { type CallLayout } from "./CallLayout";
|
import { type CallLayout } from "./CallLayout";
|
||||||
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
|
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/layout-types.ts";
|
||||||
import styles from "./SpotlightLandscapeLayout.module.css";
|
import styles from "./SpotlightLandscapeLayout.module.css";
|
||||||
import { useUpdateLayout, useVisibleTiles } from "./Grid";
|
import { useUpdateLayout, useVisibleTiles } from "./Grid";
|
||||||
|
|
||||||
@@ -24,10 +24,11 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
> = ({ minBounds$ }) => ({
|
> = ({ minBounds$ }) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightLandscapeLayoutFixed(
|
fixed: function SpotlightLandscapeLayoutFixed({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useObservableEagerState(minBounds$);
|
useObservableEagerState(minBounds$);
|
||||||
|
|
||||||
@@ -43,12 +44,13 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
<div className={styles.grid} />
|
<div className={styles.grid} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function SpotlightLandscapeLayoutScrolling(
|
scrolling: function SpotlightLandscapeLayoutScrolling({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useVisibleTiles(model.setVisibleTiles);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
useObservableEagerState(minBounds$);
|
useObservableEagerState(minBounds$);
|
||||||
@@ -69,5 +71,5 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type CSSProperties, forwardRef } from "react";
|
import { type ReactNode, type CSSProperties } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
||||||
import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
|
import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/layout-types.ts";
|
||||||
import styles from "./SpotlightPortraitLayout.module.css";
|
import styles from "./SpotlightPortraitLayout.module.css";
|
||||||
import { useUpdateLayout, useVisibleTiles } from "./Grid";
|
import { useUpdateLayout, useVisibleTiles } from "./Grid";
|
||||||
|
import { useBehavior } from "../useBehavior";
|
||||||
|
|
||||||
interface GridCSSProperties extends CSSProperties {
|
interface GridCSSProperties extends CSSProperties {
|
||||||
"--grid-gap": string;
|
"--grid-gap": string;
|
||||||
@@ -30,10 +31,11 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
> = ({ minBounds$ }) => ({
|
> = ({ minBounds$ }) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightPortraitLayoutFixed(
|
fixed: function SpotlightPortraitLayoutFixed({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,12 +49,13 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function SpotlightPortraitLayoutScrolling(
|
scrolling: function SpotlightPortraitLayoutScrolling({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useVisibleTiles(model.setVisibleTiles);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
const { width } = useObservableEagerState(minBounds$);
|
const { width } = useObservableEagerState(minBounds$);
|
||||||
@@ -63,8 +66,7 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
width,
|
width,
|
||||||
model.grid.length,
|
model.grid.length,
|
||||||
);
|
);
|
||||||
const withIndicators =
|
const withIndicators = useBehavior(model.spotlight.media$).length > 1;
|
||||||
useObservableEagerState(model.spotlight.media$).length > 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -90,5 +92,5 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||