Compare commits
795 Commits
robin/rese
...
valere/dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e715765d9 | ||
|
|
da55d84bde | ||
|
|
72ec1439f4 | ||
|
|
67d20a8f3d | ||
|
|
9bd7888fab | ||
|
|
150dda16c8 | ||
|
|
725ff31d6f | ||
|
|
852d2ee375 | ||
|
|
e78f37a6b3 | ||
|
|
6cfa95bcfa | ||
|
|
80e760ca55 | ||
|
|
a061b9f91e | ||
|
|
26f41c434e | ||
|
|
ed585e1d34 | ||
|
|
6149dd2c9a | ||
|
|
ea6f934667 | ||
|
|
93ab3ba1ff | ||
|
|
2c54263b2f | ||
|
|
92bcc52e87 | ||
|
|
87fbbb9a3f | ||
|
|
c7e9f1ce14 | ||
|
|
53cc79f738 | ||
|
|
15a12b2d9c | ||
|
|
e3b4ffb801 | ||
|
|
8a18e70e20 | ||
|
|
00d4b8e985 | ||
|
|
190cdfcb60 | ||
|
|
8f2055b4f4 | ||
|
|
93da69983d | ||
|
|
b3b76d8b3d | ||
|
|
d052d55547 | ||
|
|
610af439a8 | ||
|
|
f8310b4611 | ||
|
|
cf453baf5d | ||
|
|
7edc97b917 | ||
|
|
8225e4f260 | ||
|
|
207b161b3b | ||
|
|
9a7e797af4 | ||
|
|
08306d663a | ||
|
|
c1c73b0f02 | ||
|
|
68a32fdff6 | ||
|
|
328cc7133a | ||
|
|
170a38c0ba | ||
|
|
6c622fc6e7 | ||
|
|
aabd76044b | ||
|
|
e626698fda | ||
|
|
8dac0366b6 | ||
|
|
b380532d30 | ||
|
|
667a3d0e3d | ||
|
|
1941fc9ca1 | ||
|
|
6efce232f8 | ||
|
|
ef2f53c38a | ||
|
|
0ebc6078dd | ||
|
|
7af89b4216 | ||
|
|
ee2b0c6a5d | ||
|
|
bf801364a6 | ||
|
|
6ee3ef2795 | ||
|
|
2f3f9f95eb | ||
|
|
9481dc401c | ||
|
|
d8b9568400 | ||
|
|
a7a3d4e93c | ||
|
|
47cd343d44 | ||
|
|
cc8e250d96 | ||
|
|
5a9a62039c | ||
|
|
2986f90a5f | ||
|
|
e4fee457cf | ||
|
|
7c40b0e177 | ||
|
|
e4404e5bb1 | ||
|
|
f5d4017981 | ||
|
|
71bf55f358 | ||
|
|
7a2c1af44b | ||
|
|
940c787040 | ||
|
|
5d55f83e90 | ||
|
|
9e3e359445 | ||
|
|
be7407ea3d | ||
|
|
0a3483729f | ||
|
|
f599e22e94 | ||
|
|
fdc66a1d62 | ||
|
|
c7491c3e97 | ||
|
|
af08c1830e | ||
|
|
bcb2b36888 | ||
|
|
b9e346d64c | ||
|
|
0ed7194d87 | ||
|
|
bbd92f666b | ||
|
|
b34a75d990 | ||
|
|
8110d22c8b | ||
|
|
88721be952 | ||
|
|
6e7bedbaec | ||
|
|
a93ceeae4b | ||
|
|
b85f36598c | ||
|
|
2e646bfac1 | ||
|
|
e0bf51b63b | ||
|
|
f6a3a371cb | ||
|
|
ac9acc0158 | ||
|
|
91f3b8b114 | ||
|
|
be0c7eb365 | ||
|
|
44980a2744 | ||
|
|
83ea154e1a | ||
|
|
0e04fd9433 | ||
|
|
93de12d79c | ||
|
|
b5d3f3c72a | ||
|
|
60bc6f1e93 | ||
|
|
f76a6cb599 | ||
|
|
ab675cf00b | ||
|
|
43266e6200 | ||
|
|
af47002613 | ||
|
|
f05d4b158e | ||
|
|
0240087afe | ||
|
|
4ec1b19fe2 | ||
|
|
5f6a45f0e9 | ||
|
|
e2916d9093 | ||
|
|
90d4b6a6ce | ||
|
|
e3eb2ea12b | ||
|
|
4f45825cf2 | ||
|
|
8cb8357398 | ||
|
|
63cd4f79dd | ||
|
|
7fd4424e7b | ||
|
|
b7fc86ee06 | ||
|
|
f26aa8f970 | ||
|
|
1ff479a315 | ||
|
|
47c6a17d1e | ||
|
|
24ed43ce13 | ||
|
|
1490359e4c | ||
|
|
0664af0f1b | ||
|
|
284a52c23c | ||
|
|
2d8ffc0ccd | ||
|
|
28158bfc23 | ||
|
|
04bbf83ac5 | ||
|
|
5aa82295fd | ||
|
|
3331af1108 | ||
|
|
66dece98a5 | ||
|
|
2011aef116 | ||
|
|
f5c422c559 | ||
|
|
c6bfda94cf | ||
|
|
cdd391b3a2 | ||
|
|
59d21d2421 | ||
|
|
149f3d02ae | ||
|
|
5bc7b31dcd | ||
|
|
c0913b6546 | ||
|
|
46f8fe4ec7 | ||
|
|
c00e332dd8 | ||
|
|
e5509ba49b | ||
|
|
4c2c7d51cf | ||
|
|
fc39e82666 | ||
|
|
4b0f6e76c4 | ||
|
|
e5117f705d | ||
|
|
794585514a | ||
|
|
c078dd87fa | ||
|
|
2b3e2f6188 | ||
|
|
acd5dde6d8 | ||
|
|
d22d7460fe | ||
|
|
7f3596845c | ||
|
|
e2607d6399 | ||
|
|
3491a68362 | ||
|
|
294b90b6dc | ||
|
|
ddd015d696 | ||
|
|
7532f72c90 | ||
|
|
e60bc9e98f | ||
|
|
1aa1af2ade | ||
|
|
ccadc17406 | ||
|
|
a731981388 | ||
|
|
4099c4383d | ||
|
|
d34775fc47 | ||
|
|
a29f0162ad | ||
|
|
a69a50d30c | ||
|
|
89a1bfac2d | ||
|
|
1fd9ac93c9 | ||
|
|
f6ef872242 | ||
|
|
07de8483d5 | ||
|
|
42b4c8301f | ||
|
|
c2f541ff8c | ||
|
|
c1049b6088 | ||
|
|
6577db16ce | ||
|
|
cdc3c2b216 | ||
|
|
47324b52f8 | ||
|
|
82706cca4f | ||
|
|
665a6be484 | ||
|
|
0076b54e52 | ||
|
|
614762b5f8 | ||
|
|
222982c52c | ||
|
|
2a19f06e4a | ||
|
|
171c289f2d | ||
|
|
2e2c799f72 | ||
|
|
16e1c59e11 | ||
|
|
b51df36a06 | ||
|
|
49ec4b4298 | ||
|
|
f518111887 | ||
|
|
4414fe3c82 | ||
|
|
e9f400e9f1 | ||
|
|
7ab90d3c1f | ||
|
|
348f0dbfa8 | ||
|
|
9ad029d4ff | ||
|
|
cf8298cd69 | ||
|
|
bb746dc8dc | ||
|
|
53aaa011b7 | ||
|
|
494ed7c126 | ||
|
|
b9ad83bf3b | ||
|
|
6d8d2d2384 | ||
|
|
b731c2e9fd | ||
|
|
da85e74596 | ||
|
|
8ce8067956 | ||
|
|
f6d0d7323e | ||
|
|
fdce3ec1aa | ||
|
|
a3950869fb | ||
|
|
0115242a2b | ||
|
|
c7f50b53f5 | ||
|
|
a62d8368a1 | ||
|
|
27b76b4b1d | ||
|
|
8d421899a6 | ||
|
|
9f4d954cfa | ||
|
|
8671d3fd67 | ||
|
|
85f659bcc9 | ||
|
|
93c4dc5beb | ||
|
|
93659931ca | ||
|
|
5c83e0dce1 | ||
|
|
92ddc4c797 | ||
|
|
b4c17ed26d | ||
|
|
1f386a1d57 | ||
|
|
fc842d4be7 | ||
|
|
b8635b52d8 | ||
|
|
cf5c35bccd | ||
|
|
e741285b11 | ||
|
|
28047217b8 | ||
|
|
92fdce33ea | ||
|
|
7c41aef801 | ||
|
|
2e6b1767b9 | ||
|
|
a55ce19048 | ||
|
|
6e1a582265 | ||
|
|
5afc3c2598 | ||
|
|
8ad0cb1955 | ||
|
|
9bd464a7f1 | ||
|
|
f0adae41ef | ||
|
|
f9cc930155 | ||
|
|
eebcd133f3 | ||
|
|
d8e29467f6 | ||
|
|
c19e2245c8 | ||
|
|
4d0de2fb71 | ||
|
|
107ef16d94 | ||
|
|
57bf86fc4c | ||
|
|
870b706672 | ||
|
|
06734ae086 | ||
|
|
79bd458dc2 | ||
|
|
5961cb65df | ||
|
|
a7d2a3b9db | ||
|
|
4c5f06a8a9 | ||
|
|
a4b1ce52cd | ||
|
|
b22a937e7c | ||
|
|
a44171da1c | ||
|
|
4f892e358a | ||
|
|
c8ef8d6a24 | ||
|
|
6b513534f1 | ||
|
|
633a0f9290 | ||
|
|
62ef49ca05 | ||
|
|
3de0bbcfc9 | ||
|
|
cfe05f1ed9 | ||
|
|
9cdbb1135f | ||
|
|
0309ceb6aa | ||
|
|
3ecffccd9e | ||
|
|
3dd0b156cf | ||
|
|
5b21691c21 | ||
|
|
23bcb5777c | ||
|
|
5e0a3f3974 | ||
|
|
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 | ||
|
|
214bb75d27 | ||
|
|
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 |
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "rxjs"],
|
||||
plugins: ["matrix-org", "rxjs", "jsdoc"],
|
||||
extends: [
|
||||
"plugin:matrix-org/react",
|
||||
"plugin:matrix-org/a11y",
|
||||
@@ -26,10 +26,15 @@ module.exports = {
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
"jsdoc/check-values": "error",
|
||||
"jsdoc/check-param-names": "warn",
|
||||
// "jsdoc/require-param": "warn",
|
||||
"jsdoc/require-param-description": "warn",
|
||||
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
// We should use the js-sdk logger, never console directly.
|
||||
"no-console": ["error"],
|
||||
"react/display-name": "error",
|
||||
// Encourage proper usage of Promises:
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
@@ -44,8 +49,57 @@ module.exports = {
|
||||
],
|
||||
// To encourage good usage of RxJS:
|
||||
"rxjs/no-exposed-subjects": "error",
|
||||
"rxjs/finnish": "error",
|
||||
"rxjs/finnish": ["error", { names: { "^this$": false } }],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: ["matrix-widget-api", "matrix-js-sdk"].flatMap((lib) =>
|
||||
["src", "src/", "src/index", "lib", "lib/", "lib/index"]
|
||||
.map((path) => `${lib}/${path}`)
|
||||
.map((name) => ({ name, message: `Please use ${lib} instead` })),
|
||||
),
|
||||
patterns: [
|
||||
...["matrix-widget-api"].map((lib) => ({
|
||||
group: ["src", "src/", "src/**", "lib", "lib/", "lib/**"].map(
|
||||
(path) => `${lib}/${path}`,
|
||||
),
|
||||
message: `Please use ${lib} instead`,
|
||||
})),
|
||||
// XXX: We use /lib in lots of places, so allow for now.
|
||||
...["matrix-js-sdk"].map((lib) => ({
|
||||
group: ["src", "src/", "src/**"].map((path) => `${lib}/${path}`),
|
||||
message: `Please use ${lib} instead`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["src/*/**"],
|
||||
rules: {
|
||||
// In application code we should use the js-sdk logger, never console directly.
|
||||
"no-console": ["error"],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/test.ts",
|
||||
"**/test.tsx",
|
||||
"**/test-**",
|
||||
],
|
||||
rules: {
|
||||
"jsdoc/no-types": "off",
|
||||
"jsdoc/empty-tags": "off",
|
||||
"jsdoc/check-property-names": "off",
|
||||
"jsdoc/check-values": "off",
|
||||
"jsdoc/check-param-names": "off",
|
||||
"jsdoc/require-param-description": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
|
||||
4
.github/workflows/blocked.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Prevent blocked
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, labeled, unlabeled]
|
||||
types: [opened, labeled, unlabeled, synchronize]
|
||||
jobs:
|
||||
prevent-blocked:
|
||||
name: Prevent blocked
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- 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')
|
||||
with:
|
||||
script: |
|
||||
|
||||
10
.github/workflows/build-and-publish-docker.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check it out
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Log in to container registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ inputs.docker_tags}}
|
||||
@@ -50,10 +50,10 @@ jobs:
|
||||
org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
|
||||
- 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
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
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
|
||||
description: The package type to be built. Must be one of 'full' or 'embedded'
|
||||
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:
|
||||
SENTRY_ORG:
|
||||
required: true
|
||||
@@ -27,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Yarn cache
|
||||
@@ -37,20 +42,8 @@ jobs:
|
||||
node-version-file: ".node-version"
|
||||
- name: Install dependencies
|
||||
run: "yarn install --immutable"
|
||||
- name: Build full version
|
||||
if: ${{ inputs.package == 'full' }}
|
||||
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"
|
||||
- name: Build Element Call
|
||||
run: ${{ format('yarn run build:{0}:{1}', inputs.package, inputs.build_mode) }}
|
||||
env:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
2
.github/workflows/build.yaml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
with:
|
||||
package: full
|
||||
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:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
@@ -61,6 +62,7 @@ jobs:
|
||||
with:
|
||||
package: embedded
|
||||
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:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
2
.github/workflows/changelog-label.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
steps:
|
||||
- uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||
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"
|
||||
BANNED_LABELS: "banned"
|
||||
|
||||
2
.github/workflows/lint.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Yarn cache
|
||||
|
||||
20
.github/workflows/publish-embedded-packages.yaml
vendored
@@ -44,6 +44,8 @@ jobs:
|
||||
run: |
|
||||
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "TAG=latest" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\-rc\.[0-9]+$ ]]; then
|
||||
echo "TAG=rc" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "TAG=other" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -83,7 +85,7 @@ jobs:
|
||||
run: find ${{ env.FILENAME_PREFIX }} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${{ env.FILENAME_PREFIX }}.sha256
|
||||
- name: Upload
|
||||
if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }}
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.FILENAME_PREFIX }}.tar.gz
|
||||
@@ -98,10 +100,10 @@ jobs:
|
||||
ARTIFACT_VERSION: ${{ steps.artifact_version.outputs.ARTIFACT_VERSION }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # required for the provenance flag on npm publish
|
||||
id-token: write # Allow npm to authenticate as a trusted publisher
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: 📥 Download built element-call artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
@@ -124,8 +126,6 @@ jobs:
|
||||
npm version ${{ needs.versioning.outputs.PREFIXED_VERSION }} --no-git-tag-version
|
||||
echo "ARTIFACT_VERSION=$(jq '.version' --raw-output package.json)" >> "$GITHUB_ENV"
|
||||
npm publish --provenance --access public --tag ${{ needs.versioning.outputs.TAG }} ${{ needs.versioning.outputs.DRY_RUN == 'true' && '--dry-run' || '' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
|
||||
- id: artifact_version
|
||||
name: Output artifact version
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: 📥 Download built element-call artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
@@ -163,6 +163,8 @@ jobs:
|
||||
run: |
|
||||
if [[ "${{ needs.versioning.outputs.TAG }}" == "latest" ]]; then
|
||||
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
|
||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}-SNAPSHOT" >> "$GITHUB_ENV"
|
||||
fi
|
||||
@@ -195,7 +197,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
path: element-call
|
||||
|
||||
@@ -208,7 +210,7 @@ jobs:
|
||||
path: element-call/embedded/ios/Sources/dist
|
||||
|
||||
- name: Checkout element-call-swift
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
repository: element-hq/element-call-swift
|
||||
path: element-call-swift
|
||||
@@ -260,7 +262,7 @@ jobs:
|
||||
echo "iOS: ${{ needs.publish_ios.outputs.ARTIFACT_VERSION }}"
|
||||
- name: Add release notes
|
||||
if: ${{ needs.versioning.outputs.DRY_RUN == 'false' }}
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
|
||||
4
.github/workflows/publish.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Create Checksum
|
||||
run: find ${{ env.FILENAME_PREFIX }} -type f -print0 | sort -z | xargs -0 sha256sum | tee ${{ env.FILENAME_PREFIX }}.sha256
|
||||
- name: Upload
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.FILENAME_PREFIX }}.tar.gz
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add release note
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
|
||||
15
.github/workflows/test.yaml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Yarn cache
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Vitest
|
||||
run: "yarn run test:coverage"
|
||||
- name: Upload to codecov
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
playwright:
|
||||
name: Run end-to-end tests
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
@@ -46,11 +46,12 @@ jobs:
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Run backend components
|
||||
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
|
||||
- name: Copy config file
|
||||
run: cp config/config.devenv.json public/config.json
|
||||
- name: Run Playwright tests
|
||||
env:
|
||||
USE_DOCKER: 1
|
||||
run: yarn playwright test
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
4
.github/workflows/translations-download.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/localazy-download
|
||||
|
||||
2
.github/workflows/translations-upload.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Upload
|
||||
uses: localazy/upload@27e6b5c0fddf4551596b42226b1c24124335d24a # v1
|
||||
|
||||
4
.gitignore
vendored
@@ -8,7 +8,9 @@ dist-ssr
|
||||
.idea/
|
||||
public/config.json
|
||||
backend/synapse_tmp/*
|
||||
backend/synapse_tmp_othersite/*
|
||||
/coverage
|
||||
config.json
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
@@ -27,4 +29,4 @@ yarn-error.log
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.cache/
|
||||
|
||||
@@ -1 +1 @@
|
||||
22
|
||||
24
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY ./dist /dist
|
||||
WORKDIR /dist/assets
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
[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
|
||||
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
|
||||
|
||||
@@ -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`
|
||||
key in their `org.matrix.msc3401.call.member` which actual MatrixRTC backend
|
||||
will be used for this call.
|
||||
- During the actual call join flow, the **LiveKit JWT Auth Service** provides
|
||||
the client with the **LiveKit SFU WebSocket URL** and an **access JWT token**
|
||||
in order to exchange media via WebRTC.
|
||||
- During the actual call join flow, the **[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)**
|
||||
provides the client with the **LiveKit SFU WebSocket URL** and an
|
||||
**access JWT token** in order to exchange media via WebRTC.
|
||||
|
||||
The example below illustrates how backend selection works across **Matrix
|
||||
federation**, using the setup from sites A, B, and C. It demonstrates backend
|
||||
@@ -205,22 +206,22 @@ See also:
|
||||
### Backend
|
||||
|
||||
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`)
|
||||
- LiveKit Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
|
||||
- Minimum LiveKit SFU Setup using dev defaults for config
|
||||
- Redis db for completeness
|
||||
- Minimum Synapse Setup (servernameis: `synapse.m.localhost`, `synapse.othersite.m.localhost`)
|
||||
- MatrixRTC Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
|
||||
- Minimum LiveKit SFU setup using dev defaults for config
|
||||
- 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
|
||||
certificates
|
||||
- Minimum TLS reverse proxy for
|
||||
- Synapse homeserver: `synapse.m.localhost`
|
||||
- MatrixRTC backend: `matrix-rtc.m.localhost`
|
||||
- Synapse homeserver: `synapse.m.localhost` and `synapse.othersite.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 `
|
||||
- Element Web `app.m.localhost`
|
||||
- Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT
|
||||
- Element Web `app.m.localhost` and `app.othersite.m.localhost`
|
||||
- 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
|
||||
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
|
||||
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: "matrix.org"
|
||||
- server_name: "synapse.othersite.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
|
||||
experimental_features:
|
||||
# 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
|
||||
# correctly track the state of the room.
|
||||
msc4222_enabled: true
|
||||
# sticky events for matrixRTC user state
|
||||
msc4354_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:
|
||||
@@ -45,10 +64,3 @@ rc_message:
|
||||
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
||||
per_second: 0.5
|
||||
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_end: 50200
|
||||
use_external_ip: false
|
||||
#redis:
|
||||
# address: redis:6379
|
||||
# username: ""
|
||||
# password: ""
|
||||
# db: 0
|
||||
turn:
|
||||
enabled: false
|
||||
domain: localhost
|
||||
@@ -21,3 +16,5 @@ turn:
|
||||
external_tls: true
|
||||
keys:
|
||||
devkey: secret
|
||||
room:
|
||||
auto_create: false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Synapse reverse proxy including .well-known/matrix/client
|
||||
# domain synapse.m.localhost
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
@@ -26,14 +27,53 @@ server {
|
||||
# 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 / {
|
||||
proxy_pass "http://homeserver:8008";
|
||||
location ~ ^(/_matrix|/_synapse/client) {
|
||||
proxy_pass "http://homeserver:8008";
|
||||
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-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;
|
||||
@@ -41,19 +81,23 @@ server {
|
||||
}
|
||||
|
||||
# MatrixRTC reverse proxy
|
||||
# domain matrix-rtc.m.localhost
|
||||
# - MatrixRTC Authorization Service
|
||||
# - LiveKit SFU websocket signaling connection
|
||||
upstream jwt-auth-services {
|
||||
server auth-server:6080;
|
||||
server host.docker.internal:6080;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
listen 8448 ssl;
|
||||
listen [::]:8448 ssl;
|
||||
server_name matrix-rtc.m.localhost;
|
||||
ssl_certificate /root/ssl/cert.pem;
|
||||
ssl_certificate_key /root/ssl/key.pem;
|
||||
|
||||
http2 on;
|
||||
|
||||
|
||||
location ^~ /livekit/jwt/ {
|
||||
|
||||
@@ -62,8 +106,9 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# JWT Service running at port 8080
|
||||
proxy_pass http://auth-server:8080/;
|
||||
# JWT Service running at port 6080
|
||||
proxy_pass http://jwt-auth-services/;
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
server {
|
||||
listen 80;
|
||||
@@ -153,3 +246,36 @@ server {
|
||||
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-----
|
||||
MIIDGjCCAgKgAwIBAgIUGdiFHhH4KL2pqBjMQHQ+PVIkSV8wDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy
|
||||
MDJaFw0zNTA1MDMxMDMyMDJaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
|
||||
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDA2y0hjmNn1vRsVSdy
|
||||
8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH9jdT
|
||||
tG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeFw9fw
|
||||
eRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZNWnie
|
||||
Ui7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAveL9K
|
||||
FGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BLPiQU
|
||||
KGKrAgMBAAGjUDBOMB0GA1UdDgQWBBQJqBjMu61c1p24txw/y+kv3D+V6DAfBgNV
|
||||
HSMEGDAWgBQJqBjMu61c1p24txw/y+kv3D+V6DAMBgNVHRMEBTADAQH/MA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQB8m2YfFGLugNt5vAAOvNxVqDA8c72yCVYr3CBCpmTIEY5Z
|
||||
d3qVGhG9//ux6+J8ntkSwd9nV5GJyYXHukCG1VavnAWolWdNF/WAllf0jhLuz7kD
|
||||
/cJnuI1By4tBsBmSz851i6HJ4t5k99Be+6GQVzi0e7zzfxTHZE4xP2J6Ox8QbPsP
|
||||
n0m76nIp/WbWaJqzvIIjJhmUUPPv+4wN+eOArgjiGLzptM2qTtGZtd0c9nS5gvep
|
||||
+mEbSUN9zkhAroZf80wf+hEvy+fJ94VbZ9QjTzTg7odZLrsXGIe8DaG63EYRQ25b
|
||||
W5iYBAreln5fGSt7qHsGfqwZibTEk/Lx3dydO1Kg
|
||||
MIIDGjCCAgKgAwIBAgIUbSbx+1UGptOTGefqEn7Zh3yoChIwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA5MjIxMjI3
|
||||
MzVaFw0zNTA5MjAxMjI3MzVaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
|
||||
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHODfkrFsOkqCnXnTb
|
||||
QWz3LkUtNCoVvM7wFouirRnITJYG+lFwF+zNl89Eaq+uUN4bwd8ml1ZuR9p+1azC
|
||||
SlklD5adhCR/ErknfUWamQEf6amSs3p0NnqnhXbnDEEbQOwNaPU/aGc6aw0+I9O6
|
||||
NQ/H830GlVuKd24Bfv0mx6Imo0Hi9jxKYhqFh80nmltk2uyXefaJxuo1jXBhwLyC
|
||||
DW8RVj55QvkZyBUzid8yslxrlo0LHKCCjZflwJJv5f+jaubkH5c0qxVaoR4+Liyt
|
||||
X/4viIwt3Mhj04ppudTvt973mTbjRG5haCz9y7OkT1mMWhc0xrdMFX+gjPERYS2H
|
||||
Ru/RAgMBAAGjUDBOMB0GA1UdDgQWBBTXNfLAKVayGQda/JZLPszrpz6LVzAfBgNV
|
||||
HSMEGDAWgBTXNfLAKVayGQda/JZLPszrpz6LVzAMBgNVHRMEBTADAQH/MA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQCvGfyopHHgZB+horGH6i/Xg41V+r4d0o092F1Lfr4vh86e
|
||||
XMakRw92vsyk/iWOnLPNPcpVWzPcvINaCs/bahgnGSOAnrA4jjcXqymyGIy/6xc5
|
||||
1EeZAxehiL9E5q4LQ841HDX0gps4ZzUO1BRYQcjG9Rdts83JO2ekkfHkZdNj2eQr
|
||||
KOrr92Na1/w+EQdo/T9Rs2ES623xKEOxPqb8d/rx5Z4DdeuGx1u+3AfS76Lpo4ni
|
||||
EJ0g1ImqdSUtiOLzeCQh6pqqb+vuFbxAyeyYSAJ49847EtFBvZCmWmPL2JICg9uq
|
||||
7rKW/qDfEK9GUs0GWCs3+mJkNvOOxBwtMuQrL7ZF
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA2y0hjmNn1vRs
|
||||
VSdy8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH
|
||||
9jdTtG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeF
|
||||
w9fweRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZN
|
||||
WnieUi7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAv
|
||||
eL9KFGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BL
|
||||
PiQUKGKrAgMBAAECggEAAPX2kxi5AQ7ul82SzT1KgpSXyDHLdYaUyAoYnaX9RO+B
|
||||
8ylmpyeqygs4+KQS4EMJm9jpo85Oy37bIKdG3kljU6wQcKlL5Y+ZUOo1nzpV6fid
|
||||
hGVs6ts8VXw8KshKQ9AyccZ8L/pirUfgOffgTwfjY7/90zceAL/s98GuZWc62nkX
|
||||
55joQv/OikqYfAGP/U6Bp2Zyf23DwJB09Z3B6NnZj/ZyAbDrDEHuA15LhCOcCczp
|
||||
IU/mFEywBPHT9Tg4w4Beq78PeAETvku2UalYRLhP3RLlXr2oEbwUtINRVt2QjZ85
|
||||
Esps4uCqL/mgQluIebtudD9HL/YMlNPXue1mDXFxJQKBgQDgZZY4yJBcf488T1V6
|
||||
HNm06b/LvVGj253pKgw14hpY1xQu3Ymgzv1GEqzhSYdzxhpmj0tMUNHxAp+YdGQu
|
||||
SZ0wcPKhw0aYVkIjDRYDC3Wn5GJhyIEYHGYMo/n4l49UzHRBPOTDzp49DkHTKBgh
|
||||
XgIIazYT3CkjTIMRrkUv+qfIPQKBgQDcBGu/mqbjxs4sN3zqPS4aB21o6t6W0sXs
|
||||
ZP9w6RlTPQi5U2oRbftjZtYc0bbEgkMUImB1HwYPQT5pJ+MyC414xDvSc2exBr5d
|
||||
To6yyPIy78Tf5PHM12fpKV92nSvoz/pSjYcGxxDtKfPqu+t8mOJfjCV1lLLA+xuB
|
||||
DDaE4p8dBwKBgQCdAne6A5v/HMH8UQZeCxHJpESvKiiVnnU/UEx651nID7XvlNNX
|
||||
0X0mKqsMd4ZvW43ddSYan/JF0LAa3FW8jYWO/3jF9vzOWoysOdvNBZetgf/Uq5ao
|
||||
aDZ/YbzmVCXWD7jIbPMkjs3pqrAkL0mzDzQc7+dGviWKrV6IYIfIqnn7gQKBgDCz
|
||||
vdIk/qpO+JZrFfiX4Fucp0hhLTJ/p5ZDaRPqVVPKn+K+Jy2ChfIj8mNgvK9VEloj
|
||||
nexvGJ1J2PHYBX+vdPp1nbRhHWPfVUY8PHQw7QP/dToGaMvqJrNDGEGeWvjnCMc7
|
||||
UtdaO1H0Rm0AegkTopB56lTTvJnhO95eALd7nrMDAoGAEPdzJtWoKafp49svhSj0
|
||||
hiXQv2SPBwVUN4LZ4SOWiXUcmYYm80aNpYKLkBxYjrfqFWhE7NUHLGp8YorQWKY2
|
||||
acD9AReHk/xku0ABy6jeYmSCmCxASxst5liKD+l12sk0gB0rk5MBxB4Uu1MIbQZ2
|
||||
aCASX3AVD2/XyC2MKkzc8Eg=
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHODfkrFsOkqCn
|
||||
XnTbQWz3LkUtNCoVvM7wFouirRnITJYG+lFwF+zNl89Eaq+uUN4bwd8ml1ZuR9p+
|
||||
1azCSlklD5adhCR/ErknfUWamQEf6amSs3p0NnqnhXbnDEEbQOwNaPU/aGc6aw0+
|
||||
I9O6NQ/H830GlVuKd24Bfv0mx6Imo0Hi9jxKYhqFh80nmltk2uyXefaJxuo1jXBh
|
||||
wLyCDW8RVj55QvkZyBUzid8yslxrlo0LHKCCjZflwJJv5f+jaubkH5c0qxVaoR4+
|
||||
LiytX/4viIwt3Mhj04ppudTvt973mTbjRG5haCz9y7OkT1mMWhc0xrdMFX+gjPER
|
||||
YS2HRu/RAgMBAAECggEABhB9CxYAE5p9D3s9nWsJcSDUdELRQSYlOoPFLmeMkF9c
|
||||
dcvq7LmduMh1Q8TnoivOBxRIwbj7pZHEYfYJM0TmH82wrQzXu5KLVltm4gTkVt9b
|
||||
DR8vjBgYdb8HVpM17Cl2xhW62XpJIiseFRUsHc/9sf2Egc3MIpPuIleGR0budbSW
|
||||
ybBkqEokTYTSiAztcu3G+VN0U9MsJgLMa8HApya7M48ojdrhzngVHZRUOXul9o7u
|
||||
zYJWSxPHIIYp5C4pYQBAx8OttThwKK1A9lwbQ2EJx0KnTbBC6O5Gna/jENpGd1h2
|
||||
rzK/9MONtsjln7IejP+4mDlNupS6SF3zzHPBHjqKAQKBgQDtXUIKPiVTFS45yWtK
|
||||
XD62s3j8jfIi+22b/C30fCPtppn0cm/0zY+vovgWVUBnQXkExafRthZCuxnE8ry7
|
||||
E29S40+4z9yivAC9dz7vHZUbyIFP6VG9WyhUYo+/WqOIePyh+iBISQ9TA1DneIYz
|
||||
+VZ8iU5GvdybUPl2C5WN8seaoQKBgQDW3EwVN2EEkChLRJbQYN2qpjn+0vYESMJ8
|
||||
K0sgMRtgh4+/T2Xb9b8O/dd87Fi/4oaUqWZ2E2sdsXq8P/IEo0cv6SRfHMy7GyxL
|
||||
RM7ztwUfMC4LVWi0ZIXMrm4gRDGN2XjGvhkX6fU2lSf6azWL1K3wI3amNV2b7P7d
|
||||
ItpvdkH3MQKBgQCXf29YJEQkXB9t6J3fDzND3xb4cwy5wSo7ZeBa7CTuWOhoeeX1
|
||||
JIJyAp0/e9goT0SThChRlFtu6gZPivJkoMnr6IOInLrg7we15fc4HPR/kCDgxTVT
|
||||
m2wJOAMxigNYZogwRfn2yRLL1BD+PBHD+H936xcX1bSJOUyPSGOC/xLhIQKBgQCb
|
||||
kCDd85ygyycBaAWxlZCor3WqFF/fNjbp5Aaepi9mMoBXSUs8eK7+UbelURHozEAY
|
||||
fpYaw3B4rTlp9vppdTZjb+/PlXB9v+zQCl+0gTyKGj4cIpiOk4F0co51eipOw7f4
|
||||
XUaZ0+CgxlmNq/W26iONjH+pU1YVQQA+Z6+zp/GW4QKBgQCrzYgeugxxqgJzyIRu
|
||||
0njJkIg+T5gHvsQrtpzq7LVob+HBiBiT7eDOeGDXTK8F//sk969QVrDMQsTMvGW9
|
||||
sG1oTqxciALTMqkJTf8+hT9Uogir0/iTbJUzTt5vPYpQOEQwQHIXMUTjZ9C6NDKT
|
||||
QlmeMCxeWyPYqoMfwKmdtDP/Iw==
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDZzCCAk+gAwIBAgIUXizLjwkdqepX0bh0K3abeJxj68IwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMzU5
|
||||
MTFaFw0zNTA1MDMxMzU5MTFaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrzGSScSgaQuZdELGFYiLiYRwr
|
||||
LKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI39WmH95aX3d+A
|
||||
U7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ouuN6AOdzWs8hp
|
||||
RYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFft1ZXxIZOCjDs
|
||||
rEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71IfieLCEzMTP1VXa
|
||||
tP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZrQUGnL2RtAgMB
|
||||
AAGjgaIwgZ8wHwYDVR0jBBgwFoAUCagYzLutXNaduLccP8vpL9w/legwCQYDVR0T
|
||||
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMAYDVR0RBCkw
|
||||
J4IJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdDAdBgNVHQ4E
|
||||
FgQUfdh1p52ZgWyZcBgBXGwKi4EnUE0wDQYJKoZIhvcNAQELBQADggEBAKrHEuB6
|
||||
33j8+EwSHw3zrvt/DRXK2BDHI1Ir9JcztSunaKAjZXVvf/dvZp0Xs1dEdJIdnv6G
|
||||
iZYhBbOqDqpQZbf2h/h0kuu5yZSBUdnQXnYNxlhp2UaC/UEgw5iZT/p1rm7RjVie
|
||||
y4Dp2WytV5iZOLmLj6xDvd3DXazgJPWIRX8p8qJZbKTkwCjTr7nDIj8jjG1sVFf7
|
||||
1RJBO5/6WSnImrpDmlLUrvjiKvbxcdseDJyBOhTwdRdSk4S2M+s5tR5j2I1gXLOq
|
||||
J5ioN76+SCrTY0K0WKRy9oOXWO1/X3+VYcekp+0F3SGkd5w17jylCv1XIGHAdEsQ
|
||||
v2z2/aMI/7sAD2Q=
|
||||
MIIDgDCCAmigAwIBAgIUT9NYpZbrAKokSPSTE3zzsAMowvEwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA5MjIxMjI3
|
||||
MzVaFw0zNTA5MjAxMjI3MzVaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK1DNwTQWmyK71Ar56NvmSMQ8s
|
||||
qUY3jGqqPVORjfDUtDCrPPdCxT+ZlnsAgdonElWoWqczMrSyBRgfJlZMd4lEvt6V
|
||||
EEiZGUvA/lG1XIVgrx1kMSHKBoJj7lCBN6r3IWmYe6CxgfZurgp+7Z22i6cGMOnQ
|
||||
0XduX5Asup6zk5V7AE6i9eKrJsUjYmRBXtk099IitkER4TMqh6WxJmFF+eV9P/ax
|
||||
fxkon+bQWITwP1PLC1UOTK7lR0EcVan5aY6WMs/6RfO4Gw/dvuiVG1jCrVcaKNGT
|
||||
PYqmQqs+MOvyIqJ9kYELRZu+6bhPWSXk2ESpSIUIPH9twfnmWrncneIJR24/AgMB
|
||||
AAGjgbswgbgwHwYDVR0jBBgwFoAU1zXywClWshkHWvyWSz7M66c+i1cwCQYDVR0T
|
||||
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwSQYDVR0RBEIw
|
||||
QIIJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdIIXKi5vdGhl
|
||||
cnNpdGUubS5sb2NhbGhvc3QwHQYDVR0OBBYEFIkGX+cEJ1ISKIwuT1zzp7uHJ90e
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBAQBnnnfB7KmyYo16ZYUCmoqGhbM4p8npeYTh5ySb
|
||||
K01YwGCnMU1qGfJnKHaRwQ2+KtVGZnpBdjmsHcOUetA3V2BirPaYowMCMtaI36LD
|
||||
LnxvboSZLX0mgEYuN7HmxW4a7fSelDecTYa7xti1sNhE/w8xW7Lky046/DousyUy
|
||||
d9x3wJ183GGj1W2p6bR1E4sqTr/VbmoULQxnqA3GUNOxW3lRL5e8lQ6jJVRmMF4k
|
||||
92BtMPrI/m7jwHj0f/WBLI8mdJ/O/W/NxQOG475FZePDfrg+MkeXPChPggf42/ou
|
||||
AMm56FNB7e1l0b1Fots730RfpCPuXpiAxL4pisS0X1dMVeeM
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrzGSScSgaQuZd
|
||||
ELGFYiLiYRwrLKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI3
|
||||
9WmH95aX3d+AU7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ou
|
||||
uN6AOdzWs8hpRYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFf
|
||||
t1ZXxIZOCjDsrEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71Ifi
|
||||
eLCEzMTP1VXatP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZr
|
||||
QUGnL2RtAgMBAAECggEAJaFQii8U/KOYt9vXNoMnZvSkaeSQLLhn2V6Kciu1CtWE
|
||||
aMTWLsFE6nk+G5xXkYcTmM3T0GghtH3u5CjyI6EcsEkeEorCZJt0wbmayDmqiekR
|
||||
LfMzOdHuTHX5+edPgMGYYG1BFyRKyYFsjH1b5zRFZhXdGQnrl5760GsVlz9D1KZQ
|
||||
iHcT+q1S2tmZeoUukQnADENKXUMCyTGM5FCddgNtsWnGDsTDayh7hUdvDkB+mW4G
|
||||
lSp+BZuc3PCwpbD6qkXvfugWs6CUAAtXoV3ceWgxQ+TEnNlwxaG1AyugfgNUBolk
|
||||
8xgeZt4r5QId03jsHDf7hpBAofcaCd5EMIIQYFvWoQKBgQDlbAvAzEFPTZZn2nRV
|
||||
Xagw4xjqVc1LLEKLCWq0N5rEkwn0h90Dz5N7/3NuonP/sIDsDHCbyiOYBI1Ck6Xi
|
||||
0WuB+OyKDh+xeF2mekN9G9ywPahdK5lT/TVsxXFyZlwtVv1x/6KBO4yv5URizxqU
|
||||
gyAPDDxfD/KcNjkOBaodWEwQGQKBgQC/s2gPDBtQkjLwkHXchBomLww5eLlVrac1
|
||||
WK4UX6uSdOgrjJ375OOgMTxe8NVZdOuAKytGXRWDwgH3nVWvuZhe7dGlX3JMuSer
|
||||
e9VwDpBESrvqcR4ruL6wm8wej6BXyjH0wD3FHb0S5HfuBDxTn+4bDwrbRzOUMNgy
|
||||
lSppuflxdQKBgQDiZcIfazFT8evn5nMAvuC4BZNTxIJHmZC9JfjPiUPIkpWzYtOe
|
||||
7BvNtKOT3Op9uw8uYYRKqKqBXJSNy6ha8XCXHS9HeXKbLn20SFkLQBCDNwVLlDfF
|
||||
40zyXtF6JDr4XyzSb4NM5pgKCER5AYloXxGm59s3sEQpFXUuOjbKqJS/GQKBgAoI
|
||||
c7vF4HAZFr1sch62cz/oWnVvkhOf4Q5zs7ixQSOLJtOQqnwSgK9TpFs7s47ZBbJR
|
||||
kBRAru2Ua9Hv1Bo8VnMxczV6h1roneDlvEf/GyHX33nnrbKQGrrXjJlU3wl5NaAf
|
||||
p5v3cHvapUQ5yIZ/6lBUOzc6xMJOxCHxmKSr7Rg5AoGAbEE4lt6Xh2dnBPJ81eNI
|
||||
IDrw/3ITY53qAY4Bx88CByIFuu8CEUdUZprh98jSl6ic1tMinZfUhRMwABLrUD51
|
||||
DGst8iGLPD9u83iMcUHI/L+p7AbxrKLvWXZrF5UZm440c9mSWqfhPaTBosPtNDsG
|
||||
LfETwH1flKXMTXd2xA9RTE4=
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK1DNwTQWmyK71
|
||||
Ar56NvmSMQ8sqUY3jGqqPVORjfDUtDCrPPdCxT+ZlnsAgdonElWoWqczMrSyBRgf
|
||||
JlZMd4lEvt6VEEiZGUvA/lG1XIVgrx1kMSHKBoJj7lCBN6r3IWmYe6CxgfZurgp+
|
||||
7Z22i6cGMOnQ0XduX5Asup6zk5V7AE6i9eKrJsUjYmRBXtk099IitkER4TMqh6Wx
|
||||
JmFF+eV9P/axfxkon+bQWITwP1PLC1UOTK7lR0EcVan5aY6WMs/6RfO4Gw/dvuiV
|
||||
G1jCrVcaKNGTPYqmQqs+MOvyIqJ9kYELRZu+6bhPWSXk2ESpSIUIPH9twfnmWrnc
|
||||
neIJR24/AgMBAAECggEANRp6vzfDN4gKWoaV5TyYegCTNv+4rRl74cd9pjmx6Jam
|
||||
uWaUXCx1etpNqPPWcG1Z9OKLLRnk+kjgKGOqq4mObGvGreNeBot7bHOJZADtwMMI
|
||||
YG+Gp7StlclS1YoEHoDmezA/AcqDgTXa9KF0rdMBb1sGFJCLAuBNSJCxtVV6CQIz
|
||||
X26uT0m+Wx8MQyQWA7Sqy6DQNJo++IZkvr7a3cidqBOUPs+QvnIV5JsUb2gp5tGn
|
||||
zk+ObeRjoFFWYAN/NK7bneRenkP40m3MSL8ZfaEuuonui7CrxM1SiQyq2N1u/Aoy
|
||||
OE1JtNaVPbLBo6kG5al7Sj4Z0zhRt+iv93S2lZMkBQKBgQD2+FpLTqyLO1NDOFkE
|
||||
kxU+LdLOx0OV9wASC0ApPOu1dHMG6ksByr7TWeiu6GJDgajusPB7NVPOt2cm4iWU
|
||||
xPxXPO5l87uiSvu80h5uG4Qdj8KEijHkdap2wbVkU/mm8lBKC36jyBQIlJKySyXY
|
||||
zSEMfLK9jQPKz5cKKT3dVj/fAwKBgQDSPq9oks6K96MAB66o6cm214otQlnTQkPM
|
||||
xgjtjddX+Lp9tgihGvtSfPbyy89oUDHCfKvW/AHG52e5dec5YUi6mVdHEWbk33Kt
|
||||
BoQuxeK3XseIDlD/JD9Dd7KfUyO5w2jtYLfNdqez41O4qj2N52m1KwJYTwMsc8Kq
|
||||
izVgkC5hFQKBgBFAc/5CtqbbNAvECePZ6mf3h3xOSxhUsrqP8qFu0gBQ7CAVibvM
|
||||
T9wvsaNWNFcG3age0A2rQfl0sk3zCjEEOaRWa0jP59GEb2VXQCzs2yO9gRcFGEsf
|
||||
NRMqoOMrQos47gbeGrCSL2QSDNVLjo9AdQiMRWgcS6GFMsXQ77NgbQHFAoGBAI4a
|
||||
YGTGFWRITJvQlXUFz5kNxg8hMaVgvILDt3UY0dxb+XDOgLajjgsK+77Pkrhmu7tA
|
||||
mMUOQAU4kxr/XfGil43H5v3Z/Tnk7ZWVOfKDPeHC5gpH4ucQkNIBLXISt6rvMRSA
|
||||
srrk4CTuGcBPEJvBNemF0Gfvv61j8MdkoAdMbIyhAoGAfGR6yZLBmRMsW5PKmcpT
|
||||
nq2oSeUpmtGZra6pWz/3XU7AgrCLcx1DmqEjm4w7y5NQJmxyMZqqdTJILCjr3Srt
|
||||
+2F0NqQL6Li+xQGibAvDj0Jxyol38RvFC0J/w2vQmuF0hTuH95yknSd7FPXK+DPG
|
||||
qYgXLjun9dht6kx9vGJ69wI=
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -34,5 +34,6 @@ subjectAltName = @alt_names
|
||||
DNS.1 = localhost
|
||||
DNS.2 = m.localhost
|
||||
DNS.3 = *.m.localhost
|
||||
DNS.4 = *.othersite.m.localhost
|
||||
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
|
||||
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: "matrix.org"
|
||||
- server_name: "synapse.othersite.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
@@ -34,6 +44,13 @@ experimental_features:
|
||||
# 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:
|
||||
@@ -44,6 +61,10 @@ rc_message:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_delayed_event_mgmt:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
@@ -58,10 +79,3 @@ rc_login:
|
||||
rc_registration:
|
||||
per_second: 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
|
||||
@@ -13,7 +13,6 @@ coverage:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
# Encourage (but don't enforce) 80% coverage on all lines that a PR
|
||||
# Enforce 80% coverage on all lines that a PR
|
||||
# touches
|
||||
target: 80%
|
||||
informational: true
|
||||
|
||||
@@ -8,5 +8,12 @@
|
||||
"features": {
|
||||
"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": {
|
||||
"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,12 +5,20 @@
|
||||
"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": {
|
||||
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
|
||||
"api_host": "https://posthog-element-call.element.io"
|
||||
},
|
||||
"rageshake": {
|
||||
"submit_url": "https://element.io/bugreports/submit"
|
||||
"submit_url": "https://rageshakes.element.io/api/submit"
|
||||
},
|
||||
"sentry": {
|
||||
"environment": "netlify-pr-preview",
|
||||
|
||||
@@ -4,26 +4,51 @@ networks:
|
||||
services:
|
||||
auth-service:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||
pull_policy: always
|
||||
hostname: auth-server
|
||||
environment:
|
||||
- LIVEKIT_JWT_PORT=8080
|
||||
- LIVEKIT_JWT_PORT=6080
|
||||
- LIVEKIT_URL=wss://matrix-rtc.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
|
||||
- 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:
|
||||
- ecbackend
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
image: livekit/livekit-server:v1.9.4
|
||||
pull_policy: always
|
||||
hostname: livekit-sfu
|
||||
command: --dev --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
@@ -41,20 +66,30 @@ services:
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
command: redis-server /etc/redis.conf
|
||||
livekit-1:
|
||||
image: livekit/livekit-server:v1.9.4
|
||||
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:
|
||||
# HOST_PORT:CONTAINER_PORT
|
||||
- 6379:6379
|
||||
- 17880:17880/tcp
|
||||
- 17881:17881/tcp
|
||||
- 17882:17882/tcp
|
||||
- 50300-50400:50300-50400/udp
|
||||
volumes:
|
||||
- ./backend/redis.conf:/etc/redis.conf:Z
|
||||
- ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
synapse:
|
||||
hostname: homeserver
|
||||
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
|
||||
@@ -69,6 +104,24 @@ services:
|
||||
networks:
|
||||
- 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:
|
||||
image: ghcr.io/element-hq/element-web:develop
|
||||
pull_policy: always
|
||||
@@ -81,10 +134,24 @@ services:
|
||||
networks:
|
||||
- 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:
|
||||
# see backend/dev_tls_setup for how to generate the tls certs
|
||||
hostname: synapse.m.localhost
|
||||
image: nginx:latest
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
|
||||
- ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z
|
||||
@@ -100,4 +167,9 @@ services:
|
||||
depends_on:
|
||||
- synapse
|
||||
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
|
||||
|
||||
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.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.
|
||||
|
||||
## 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
|
||||
[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`
|
||||
[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
|
||||
[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
|
||||
[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 |
|
||||
| -------- | ------- | ------- |
|
||||
| [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:
|
||||
|
||||
@@ -102,7 +102,7 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
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/;
|
||||
}
|
||||
|
||||
@@ -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_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# 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_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### MatrixRTC backend announcement
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -145,10 +171,6 @@ server {
|
||||
{
|
||||
"type": "livekit",
|
||||
"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
|
||||
used at build time, while `public/config.json` holds variables that are used at
|
||||
runtime. Documentation and default values for `public/config.json` can be found
|
||||
in [ConfigOptions.ts](src/config/ConfigOptions.ts).
|
||||
in [ConfigOptions.ts](../src/config/ConfigOptions.ts).
|
||||
|
||||
> [!CAUTION]
|
||||
> 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-Tos & Tutorials
|
||||
## 📝 How-Tos & Tutorials
|
||||
|
||||
- [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/)
|
||||
- [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/)
|
||||
- [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?
|
||||
|
||||
|
||||
@@ -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 |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `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. |
|
||||
@@ -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. |
|
||||
| `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...`. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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()`. |
|
||||
| `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.) |
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
@@ -92,6 +96,6 @@ These parameters are only supported in the [embedded](./embedded-standalone.md)
|
||||
| -------------------- | -------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `posthogApiHost` | Posthog server URL | No | e.g. `https://posthog-element-call.element.io`. Only supported in embedded package. In full package the value from config is used. |
|
||||
| `posthogApiKey` | Posthog project API key | No | Only supported in embedded package. In full package the value from config is used. |
|
||||
| `rageshakeSubmitUrl` | Rageshake server URL endpoint | No | e.g. `https://element.io/bugreports/submit`. In full package the value from config is used. |
|
||||
| `rageshakeSubmitUrl` | Rageshake server URL endpoint | No | e.g. `https://rageshakes.element.io/api/submit`. In full package the value from config is used. |
|
||||
| `sentryDsn` | Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/) | No | In full package the value from config is used. |
|
||||
| `sentryEnvironment` | Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/) | No | In full package the value from config is used. |
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
|
||||
[versions]
|
||||
android_gradle_plugin = "8.10.0"
|
||||
android_gradle_plugin = "8.13.1"
|
||||
|
||||
[libraries]
|
||||
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
|
||||
|
||||
[plugins]
|
||||
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.35.0" }
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import com.vanniktech.maven.publish.SonatypeHost
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.maven.publish)
|
||||
@@ -27,7 +25,7 @@ android {
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishToMavenCentral(SonatypeHost.S01, automaticRelease = true)
|
||||
publishToMavenCentral(automaticRelease = true)
|
||||
|
||||
signAllPublications()
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default {
|
||||
import type { UserConfig } from "i18next-parser";
|
||||
|
||||
const config: UserConfig = {
|
||||
keySeparator: ".",
|
||||
namespaceSeparator: false,
|
||||
contextSeparator: "|",
|
||||
@@ -26,3 +28,5 @@ export default {
|
||||
input: ["src/**/*.{ts,tsx}"],
|
||||
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 {
|
||||
vite: {
|
||||
config: ["vite.config.js", "vite-embedded.config.js"],
|
||||
config: ["vite.config.ts", "vite-embedded.config.ts", "vite-sdk.config.ts"],
|
||||
},
|
||||
entry: ["src/main.tsx", "i18next-parser.config.ts"],
|
||||
ignoreBinaries: [
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Zobrazit možnost sluchátek pro iPhone na všech platformách",
|
||||
"crypto_version": "Kryptografická verze: {{version}}",
|
||||
"debug_tile_layout_label": "Ladění rozložení dlaždic",
|
||||
"device_id": "ID zařízení: {{id}}",
|
||||
@@ -81,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Volání není podporováno",
|
||||
"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_description": "Hovor byl přerušen.",
|
||||
"e2ee_unsupported": "Nekompatibilní prohlížeč",
|
||||
@@ -104,6 +105,11 @@
|
||||
"knock_reject_heading": "Přístup odepřen",
|
||||
"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",
|
||||
"header_label": "Domov Element Call",
|
||||
"header_participants_label": "Účastníci",
|
||||
@@ -173,8 +179,11 @@
|
||||
"devices": {
|
||||
"camera": "Fotoaparát",
|
||||
"camera_numbered": "Fotoaparát {{n}}",
|
||||
"change_device_button": "Změnit zvukové zařízení",
|
||||
"default": "Výchozí",
|
||||
"default_named": "Výchozí <2> ({{name}}) </2>",
|
||||
"handset": "Sluchátko",
|
||||
"loudspeaker": "Reproduktor",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Reproduktor",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Vis mulighed for iPhone-høretelefon på alle platforme",
|
||||
"crypto_version": "Krypto-version: {{version}}",
|
||||
"debug_tile_layout_label": "Fejlfinding af fliselayout",
|
||||
"device_id": "Enheds-id: {{id}}",
|
||||
@@ -70,6 +71,7 @@
|
||||
"livekit_server_info": "LiveKit Serverinfo",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"mute_all_audio": "Slå al lyd fra (deltagere, reaktioner, deltagelseslyde)",
|
||||
"show_connection_stats": "Vis forbindelsesstatistik",
|
||||
"show_non_member_tiles": "Vis fliser for medier fra ikke-medlemmer",
|
||||
"url_params": "URL-parametre",
|
||||
@@ -80,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Opkald er ikke understøttet",
|
||||
"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_description": "Du blev afbrudt fra opkaldet.",
|
||||
"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 }}).",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -103,6 +107,11 @@
|
||||
"knock_reject_heading": "Adgang nægtet",
|
||||
"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",
|
||||
"header_label": "Element Ring hjem",
|
||||
"header_participants_label": "Deltagere",
|
||||
@@ -164,12 +173,18 @@
|
||||
"effect_volume_description": "Juster den lydstyrke som reaktioner og håndsoprækninger afspilles med.",
|
||||
"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",
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Skift lydenhed",
|
||||
"default": "Standard",
|
||||
"default_named": "Standard <2>({{name}})</2>",
|
||||
"handset": "Telefon",
|
||||
"loudspeaker": "Højttaler",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Højttaler",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"profile": "Profil",
|
||||
"reaction": "Reaktion",
|
||||
"reactions": "Reaktionen",
|
||||
"reconnecting": "Verbindung wird wiederhergestellt...",
|
||||
"settings": "Einstellungen",
|
||||
"unencrypted": "Nicht verschlüsselt",
|
||||
"username": "Benutzername",
|
||||
@@ -82,7 +83,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Anrufe werden nicht unterstützt",
|
||||
"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_description": "Ihre Verbindung zum Anruf wurde unterbrochen.",
|
||||
"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 }}).",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -105,6 +108,11 @@
|
||||
"knock_reject_heading": "Zugriff verweigert",
|
||||
"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",
|
||||
"header_label": "Element Call-Startseite",
|
||||
"header_participants_label": "Teilnehmende",
|
||||
@@ -173,9 +181,11 @@
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Audiogerät wechseln",
|
||||
"default": "Standard",
|
||||
"default_named": "Standard<2> ({{name}} )</2>",
|
||||
"earpiece": "Ohrhörer",
|
||||
"handset": "Ohrhörer",
|
||||
"loudspeaker": "Lautsprecher",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon{{n}}",
|
||||
"speaker": "Lautsprecher",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"profile": "Profile",
|
||||
"reaction": "Reaction",
|
||||
"reactions": "Reactions",
|
||||
"reconnecting": "Reconnecting…",
|
||||
"settings": "Settings",
|
||||
"unencrypted": "Not encrypted",
|
||||
"username": "Username",
|
||||
@@ -63,6 +64,14 @@
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Show iPhone earpiece option on all platforms",
|
||||
"crypto_version": "Crypto version: {{version}}",
|
||||
"custom_livekit_url": {
|
||||
"current_url": "Currently set to: ",
|
||||
"from_config": "Currently, no overwrite is set. Url from well-known or config is used.",
|
||||
"label": "Custom Livekit-url",
|
||||
"reset": "Reset overwrite",
|
||||
"save": "Save",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"debug_tile_layout_label": "Debug tile layout",
|
||||
"device_id": "Device ID: {{id}}",
|
||||
"duplicate_tiles_label": "Number of additional tile copies per participant",
|
||||
@@ -71,29 +80,46 @@
|
||||
"livekit_server_info": "LiveKit Server Info",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"matrixRTCMode": {
|
||||
"Comptibility": {
|
||||
"description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)",
|
||||
"label": "Compatibility: state events & multi SFU"
|
||||
},
|
||||
"Legacy": {
|
||||
"description": "Compatible with old versions of EC that do not support multi SFU",
|
||||
"label": "Legacy: state events & oldest membership SFU"
|
||||
},
|
||||
"Matrix_2_0": {
|
||||
"description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later",
|
||||
"label": "Matrix 2.0: sticky events & multi SFU"
|
||||
},
|
||||
"title": "MatrixRTC mode"
|
||||
},
|
||||
"mute_all_audio": "Mute all audio (participants, reactions, join sounds)",
|
||||
"show_connection_stats": "Show connection statistics",
|
||||
"show_non_member_tiles": "Show tiles for non-member media",
|
||||
"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"
|
||||
"url_params": "URL parameters"
|
||||
},
|
||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||
"error": {
|
||||
"call_is_not_supported": "Call is not supported",
|
||||
"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_description": "You were disconnected from the call.",
|
||||
"e2ee_unsupported": "Incompatible browser",
|
||||
"e2ee_unsupported_description": "Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.",
|
||||
"failed_to_start_livekit": "Failed to start Livekit connection",
|
||||
"generic": "Something went wrong",
|
||||
"generic_description": "Submitting debug logs will help us track down the problem.",
|
||||
"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.",
|
||||
"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 }}).",
|
||||
"membership_manager": "Membership Manager Error",
|
||||
"membership_manager_description": "The Membership Manager had to shut down. This is caused by many consequtive failed network requests.",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -105,6 +131,11 @@
|
||||
"knock_reject_heading": "Access denied",
|
||||
"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",
|
||||
"header_label": "Element Call Home",
|
||||
"header_participants_label": "Participants",
|
||||
@@ -173,9 +204,11 @@
|
||||
"devices": {
|
||||
"camera": "Camera",
|
||||
"camera_numbered": "Camera {{n}}",
|
||||
"change_device_button": "Change audio device",
|
||||
"default": "Default",
|
||||
"default_named": "Default <2>({{name}})</2>",
|
||||
"earpiece": "Earpiece",
|
||||
"handset": "Handset",
|
||||
"loudspeaker": "Loudspeaker",
|
||||
"microphone": "Microphone",
|
||||
"microphone_numbered": "Microphone {{n}}",
|
||||
"speaker": "Speaker",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Näita iPhone'i kuulari valikut kõikidel platvormidel",
|
||||
"crypto_version": "Krüptoteekide versioon: {{version}}",
|
||||
"debug_tile_layout_label": "Meediapaanide paigutus",
|
||||
"device_id": "Seadme tunnus: {{id}}",
|
||||
@@ -81,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Kõne pole toetatud",
|
||||
"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_description": "Sinu ühendus selle kõnega on katkenud.",
|
||||
"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 }}).",
|
||||
"open_elsewhere": "Avatud teisel vahekaardil",
|
||||
"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."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -104,6 +107,10 @@
|
||||
"knock_reject_heading": "Liitumine pole lubatud",
|
||||
"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",
|
||||
"header_label": "Avaleht: Element Call",
|
||||
"header_participants_label": "Osalejad",
|
||||
@@ -172,8 +179,10 @@
|
||||
"devices": {
|
||||
"camera": "Kaamera",
|
||||
"camera_numbered": "Kaamera {{n}}",
|
||||
"change_device_button": "Muuda heliseadet",
|
||||
"default": "Vaikimisi",
|
||||
"default_named": "Vaikimisi <2>({{name}})</2>",
|
||||
"loudspeaker": "Valjuhääldi",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Kõlar",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Видео"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Показать опцию наушников для iPhone на всех платформах",
|
||||
"crypto_version": "Версия криптографии: {{version}}",
|
||||
"debug_tile_layout_label": "Отладка расположения плиток",
|
||||
"device_id": "Идентификатор устройства: {{id}}",
|
||||
@@ -70,6 +71,7 @@
|
||||
"livekit_server_info": "Информация о сервере LiveKit",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"mute_all_audio": "Отключить все звуки (участников, реакции, звуки присоединения)",
|
||||
"show_connection_stats": "Показать статистику подключений",
|
||||
"show_non_member_tiles": "Показать плитки для медиафайлов, не являющихся участниками",
|
||||
"url_params": "Параметры URL-адреса",
|
||||
@@ -172,6 +174,7 @@
|
||||
"devices": {
|
||||
"camera": "Камера",
|
||||
"camera_numbered": "Камера {{n}}",
|
||||
"change_device_button": "Изменить аудиоустройство",
|
||||
"default": "По умолчанию",
|
||||
"default_named": "По умолчанию <2>({{name}})</2>",
|
||||
"microphone": "Микрофон",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Zobraziť možnosť slúchadla iPhone na všetkých platformách",
|
||||
"crypto_version": "Krypto verzia: {{version}}",
|
||||
"debug_tile_layout_label": "Ladenie rozloženia dlaždíc",
|
||||
"device_id": "ID zariadenia: {{id}}",
|
||||
@@ -81,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Hovor nie je podporovaný",
|
||||
"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_description": "Boli ste odpojení od hovoru.",
|
||||
"e2ee_unsupported": "Nekompatibilný prehliadač",
|
||||
@@ -173,8 +174,10 @@
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Zmeniť zvukové zariadenie",
|
||||
"default": "Predvolené",
|
||||
"default_named": "Predvolené <2>({{name}})</2>",
|
||||
"loudspeaker": "Reproduktor",
|
||||
"microphone": "Mikrofón",
|
||||
"microphone_numbered": "Mikrofón {{n}}",
|
||||
"speaker": "Reproduktor",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Visa iPhone-hörsnäckealternativ på alla plattformar",
|
||||
"crypto_version": "Kryptoversion: {{version}}",
|
||||
"debug_tile_layout_label": "Felsök panelarrangemang",
|
||||
"device_id": "Enhets-ID: {{id}}",
|
||||
@@ -70,6 +71,7 @@
|
||||
"livekit_server_info": "LiveKit-serverinfo",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix-ID: {{id}}",
|
||||
"mute_all_audio": "Tysta allt ljud (deltagare, reaktioner, anslutningsljud)",
|
||||
"show_connection_stats": "Visa anslutningsstatistik",
|
||||
"show_non_member_tiles": "Visa paneler för media som inte är medlemmar",
|
||||
"url_params": "URL-parametrar",
|
||||
@@ -80,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Call stöds 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_description": "Du kopplades bort från samtalet.",
|
||||
"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 }}).",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -103,6 +107,11 @@
|
||||
"knock_reject_heading": "Inte tillåten att gå med",
|
||||
"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",
|
||||
"header_label": "Element Call Hem",
|
||||
"header_participants_label": "Deltagare",
|
||||
@@ -164,12 +173,18 @@
|
||||
"effect_volume_description": "Justera volymen vid vilken reaktioner och handuppräckningseffekter spelas",
|
||||
"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",
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Byt ljudenhet",
|
||||
"default": "Förval",
|
||||
"default_named": "Förval <2>({{name}})</2>",
|
||||
"handset": "Telefonlur",
|
||||
"loudspeaker": "Högtalare",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Högtalare",
|
||||
|
||||
55
package.json
@@ -8,7 +8,13 @@
|
||||
"dev:embedded": "vite --config vite-embedded.config.js",
|
||||
"build": "yarn build:full",
|
||||
"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:production": "yarn build:embedded",
|
||||
"build:embedded:development": "yarn build:embedded --mode development",
|
||||
"build:sdk": "yarn build:full --config vite-sdk.config.js",
|
||||
"build:sdk:development": "yarn build:sdk --mode development",
|
||||
"serve": "vite preview",
|
||||
"prettier:check": "prettier -c .",
|
||||
"prettier:format": "prettier -w .",
|
||||
@@ -22,6 +28,7 @@
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"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:open": "yarn test:playwright --ui",
|
||||
"links:enable": "mv .links.disabled.yaml .links.yaml & touch .links.yaml",
|
||||
@@ -39,21 +46,21 @@
|
||||
"@formatjs/intl-segmenter": "^11.7.3",
|
||||
"@livekit/components-core": "^0.12.0",
|
||||
"@livekit/components-react": "^2.0.0",
|
||||
"@livekit/protocol": "^1.38.0",
|
||||
"@livekit/protocol": "^1.42.2",
|
||||
"@livekit/track-processors": "^0.5.5",
|
||||
"@mediapipe/tasks-vision": "^0.10.18",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/core": "^1.25.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.25.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@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/vite-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
@@ -66,44 +73,48 @@
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^3.0.0",
|
||||
"@vector-im/compound-web": "^7.2.0",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-deprecate": "^0.8.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^61.5.0",
|
||||
"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-hooks": "^5.0.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fetch-mock": "11.1.5",
|
||||
"global-jsdom": "^26.0.0",
|
||||
"i18next": "^24.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-parser": "^9.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"knip": "^5.27.2",
|
||||
"livekit-client": "^2.11.3",
|
||||
"livekit-client": "^2.13.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=develop",
|
||||
"matrix-widget-api": "^1.13.0",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#2218ec4e3102e841ba3e794e1c492c0a5aa6c1c3",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"node-stdlib-browser": "^1.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"pako": "^2.0.4",
|
||||
@@ -112,11 +123,10 @@
|
||||
"posthog-js": "1.160.3",
|
||||
"prettier": "^3.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18",
|
||||
"react-dom": "18",
|
||||
"react": "19",
|
||||
"react-dom": "19",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"react-use-clipboard": "^1.0.7",
|
||||
"react-use-measure": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sass": "^1.42.1",
|
||||
@@ -124,9 +134,10 @@
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"vaul": "^1.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-generate-file": "^0.2.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-generate-file": "^0.3.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-node-stdlib-browser": "^0.2.1",
|
||||
"vite-plugin-svgr": "^4.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"vitest-axe": "^1.0.0-pre.3"
|
||||
|
||||
@@ -2,3 +2,6 @@ services:
|
||||
synapse:
|
||||
volumes:
|
||||
- ./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";
|
||||
|
||||
const baseURL = process.env.USE_DOCKER
|
||||
? "http://localhost:8080"
|
||||
: "https://localhost:3000";
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
use: {
|
||||
/* 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 */
|
||||
trace: "on-first-retry",
|
||||
@@ -34,6 +38,7 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
testIgnore: "**/mobile/**",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
permissions: [
|
||||
@@ -52,9 +57,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
testIgnore: "**/mobile/**",
|
||||
use: {
|
||||
...devices["Desktop Firefox"],
|
||||
ignoreHTTPSErrors: true,
|
||||
@@ -66,6 +71,27 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mobile",
|
||||
testMatch: "**/mobile/**",
|
||||
use: {
|
||||
...devices["Pixel 7"],
|
||||
ignoreHTTPSErrors: true,
|
||||
permissions: [
|
||||
"clipboard-write",
|
||||
"clipboard-read",
|
||||
"microphone",
|
||||
"camera",
|
||||
],
|
||||
launchOptions: {
|
||||
args: [
|
||||
"--use-fake-ui-for-media-stream",
|
||||
"--use-fake-device-for-media-stream",
|
||||
"--mute-audio",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// No safari for now, until I find a solution to fix `Not allowed to request resource` due to calling
|
||||
// clear http to the homeserver
|
||||
@@ -73,9 +99,13 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "yarn dev",
|
||||
url: "https://localhost:3000",
|
||||
command: "./scripts/playwright-webserver-command.sh",
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
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
|
||||
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
|
||||
await page.getByTestId("incall_leave").click();
|
||||
await expect(page.getByRole("heading")).toContainText(
|
||||
|
||||
@@ -7,6 +7,8 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { createJTWToken } from "./fixtures/jwt-token";
|
||||
|
||||
test("Should show error screen if fails to get JWT token", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
@@ -72,3 +74,61 @@ test("Should automatically retry non fatal JWT errors", async ({
|
||||
await hasRetriedPromise;
|
||||
await expect(page.getByTestId("video").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("Should show error screen if call creation is restricted", async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.skip(
|
||||
browserName === "firefox",
|
||||
"The is test is not working on firefox CI environment.",
|
||||
);
|
||||
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: createJTWToken("@fake:user", "!fake:room"),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
73
playwright/fixtures/fixture-mobile-create.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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 Browser, type Page, test, expect } from "@playwright/test";
|
||||
|
||||
export interface MobileCreateFixtures {
|
||||
asMobile: {
|
||||
creatorPage: Page;
|
||||
inviteLink: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const mobileTest = test.extend<MobileCreateFixtures>({
|
||||
asMobile: async ({ browser }, pUse) => {
|
||||
const fixtures = await createCallAndInvite(browser);
|
||||
await pUse({
|
||||
creatorPage: fixtures.page,
|
||||
inviteLink: fixtures.inviteLink,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a call and generate an invite link
|
||||
*/
|
||||
async function createCallAndInvite(
|
||||
browser: Browser,
|
||||
): Promise<{ page: Page; inviteLink: string }> {
|
||||
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();
|
||||
|
||||
await creatorPage
|
||||
.getByRole("button", { name: "Continue in browser" })
|
||||
.click();
|
||||
// join
|
||||
await creatorPage.getByTestId("lobby_joinCall").click();
|
||||
|
||||
// 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/#/");
|
||||
|
||||
return {
|
||||
page: creatorPage,
|
||||
inviteLink,
|
||||
};
|
||||
}
|
||||
22
playwright/fixtures/jwt-token.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export function createJTWToken(sub: string, room: string): string {
|
||||
return [
|
||||
{}, // header
|
||||
{
|
||||
// payload
|
||||
sub,
|
||||
video: {
|
||||
room,
|
||||
},
|
||||
},
|
||||
{}, // signature
|
||||
]
|
||||
.map((d) => global.btoa(JSON.stringify(d)))
|
||||
.join(".");
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
@@ -30,8 +36,8 @@ const PASSWORD = "foobarbaz1!";
|
||||
const CONFIG_JSON = {
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: "http://synapse.localhost:8008",
|
||||
server_name: "synapse.localhost",
|
||||
base_url: "https://synapse.m.localhost",
|
||||
server_name: "synapse.m.localhost",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -61,17 +67,81 @@ const CONFIG_JSON = {
|
||||
|
||||
/**
|
||||
* Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`.
|
||||
* @param page
|
||||
*/
|
||||
async function setDevToolElementCallDevUrl(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"https://localhost:3000/room",
|
||||
);
|
||||
const setDevToolElementCallDevUrl = process.env.USE_DOCKER
|
||||
? async (page: Page): Promise<void> => {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"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();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: `Welcome ${username}` }),
|
||||
).toBeVisible();
|
||||
|
||||
const browserUnsupportedToast = page
|
||||
.getByText("Element does not support this browser")
|
||||
.locator("..")
|
||||
.locator("..");
|
||||
|
||||
// Dismiss incompatible browser toast
|
||||
const dismissButton = browserUnsupportedToast.getByRole("button", {
|
||||
name: "Dismiss",
|
||||
});
|
||||
try {
|
||||
await expect(dismissButton).toBeVisible({ timeout: 700 });
|
||||
await dismissButton.click();
|
||||
} catch {
|
||||
// dismissButton not visible, continue as normal
|
||||
}
|
||||
|
||||
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>({
|
||||
@@ -83,65 +153,25 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
const userA = `brooks_${Date.now()}`;
|
||||
const userB = `whistler_${Date.now()}`;
|
||||
|
||||
const user1Context = await browser.newContext({
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
const ewPage1 = await user1Context.newPage();
|
||||
// Register the first user
|
||||
await ewPage1.goto("http://localhost:8081/#/welcome");
|
||||
await ewPage1.getByRole("link", { name: "Create Account" }).click();
|
||||
await ewPage1.getByRole("textbox", { name: "Username" }).fill(userA);
|
||||
await ewPage1
|
||||
.getByRole("textbox", { name: "Password", exact: true })
|
||||
.fill(PASSWORD);
|
||||
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))!;
|
||||
// Register users
|
||||
const {
|
||||
page: ewPage1,
|
||||
clientHandle: brooksClientHandle,
|
||||
mxId: brooksMxId,
|
||||
} = await registerUser(browser, userA);
|
||||
const {
|
||||
page: ewPage2,
|
||||
clientHandle: whistlerClientHandle,
|
||||
mxId: whistlerMxId,
|
||||
} = await registerUser(browser, userB);
|
||||
|
||||
// Invite the second user
|
||||
await ewPage1.getByRole("button", { name: "Add room" }).click();
|
||||
await ewPage1.getByText("New room").click();
|
||||
await ewPage1
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
|
||||
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
|
||||
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
|
||||
await ewPage1.getByRole("button", { name: "Create room" }).click();
|
||||
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
|
||||
@@ -165,9 +195,9 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
|
||||
// Accept the invite
|
||||
await expect(
|
||||
ewPage2.getByRole("treeitem", { name: "Welcome Room" }),
|
||||
ewPage2.getByRole("option", { name: "Welcome Room" }),
|
||||
).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 expect(
|
||||
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
|
||||
|
||||
115
playwright/mobile/create-call-mobile.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations 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 { mobileTest } from "../fixtures/fixture-mobile-create.ts";
|
||||
|
||||
test("@mobile Start a new call then leave and show the feedback screen", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
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();
|
||||
|
||||
// await page.pause();
|
||||
await expect(page.locator("video")).toBeVisible();
|
||||
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Continue in browser" }).click();
|
||||
// Join the call
|
||||
await page.getByTestId("lobby_joinCall").click();
|
||||
|
||||
// Ensure that the call is connected
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^HelloCall$/ })
|
||||
.click();
|
||||
// Check the number of participants
|
||||
await expect(page.locator("div").filter({ hasText: /^1$/ })).toBeVisible();
|
||||
// The tooltip with the name should be visible
|
||||
await expect(page.getByTestId("name_tag")).toContainText("John Doe");
|
||||
|
||||
// leave the call
|
||||
await page.getByTestId("incall_leave").click();
|
||||
await expect(page.getByRole("heading")).toContainText(
|
||||
"John Doe, your call has ended. How did it go?",
|
||||
);
|
||||
await expect(page.getByRole("main")).toContainText(
|
||||
"Why not finish by setting up a password to keep your account?",
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Not now, return to home screen" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
mobileTest(
|
||||
"Test earpiece overlay in controlledAudioDevices mode",
|
||||
async ({ asMobile, browser }) => {
|
||||
test.slow(); // Triples the timeout
|
||||
const { creatorPage, inviteLink } = asMobile;
|
||||
|
||||
// ========
|
||||
// ACT: The other user use the invite link to join the call as a guest
|
||||
// ========
|
||||
const guestInviteeContext = await browser.newContext({
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
const guestPage = await guestInviteeContext.newPage();
|
||||
await guestPage.goto(inviteLink + "&controlledAudioDevices=true");
|
||||
|
||||
await guestPage
|
||||
.getByRole("button", { name: "Continue in browser" })
|
||||
.click();
|
||||
|
||||
await guestPage.getByTestId("joincall_displayName").fill("Invitee");
|
||||
await expect(guestPage.getByTestId("joincall_joincall")).toBeVisible();
|
||||
await guestPage.getByTestId("joincall_joincall").click();
|
||||
await guestPage.getByTestId("lobby_joinCall").click();
|
||||
|
||||
// ========
|
||||
// ASSERT: check that there are two members in the call
|
||||
// ========
|
||||
|
||||
// There should be two participants now
|
||||
await expect(
|
||||
guestPage.getByTestId("roomHeader_participants_count"),
|
||||
).toContainText("2");
|
||||
expect(await guestPage.getByTestId("videoTile").count()).toBe(2);
|
||||
|
||||
// Same in creator page
|
||||
await expect(
|
||||
creatorPage.getByTestId("roomHeader_participants_count"),
|
||||
).toContainText("2");
|
||||
expect(await creatorPage.getByTestId("videoTile").count()).toBe(2);
|
||||
|
||||
// TEST: control audio devices from the invitee page
|
||||
|
||||
await guestPage.evaluate(() => {
|
||||
window.controls.setAvailableAudioDevices([
|
||||
{ id: "speaker", name: "Speaker", isSpeaker: true },
|
||||
{ id: "earpiece", name: "Handset", isEarpiece: true },
|
||||
{ id: "headphones", name: "Headphones" },
|
||||
]);
|
||||
window.controls.setAudioDevice("earpiece");
|
||||
});
|
||||
await expect(
|
||||
guestPage.getByRole("heading", { name: "Handset Mode" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
guestPage.getByRole("button", { name: "Back to Speaker Mode" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Should auto-mute the video when earpiece is selected
|
||||
await expect(guestPage.getByTestId("incall_videomute")).toBeDisabled();
|
||||
},
|
||||
);
|
||||
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();
|
||||
});
|
||||
98
playwright/sfu-reconnect-bug.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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();
|
||||
// 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);
|
||||
});
|
||||
@@ -9,11 +9,14 @@ import { expect, test } from "@playwright/test";
|
||||
|
||||
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 }) => {
|
||||
test.skip(
|
||||
browserName === "firefox",
|
||||
"This test is not working on firefox, after hangup brooks is locked in a strange state with a blank widget",
|
||||
);
|
||||
test.slow(); // Triples the timeout
|
||||
|
||||
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
|
||||
await expect(
|
||||
brooks.page.locator("div").filter({ hasText: /^Joined • 1$/ }),
|
||||
brooks.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByRole("button", { name: "End call" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Join from the other side
|
||||
@@ -56,26 +62,28 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
||||
).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Join" }).click();
|
||||
|
||||
await expect(
|
||||
whistler.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall"),
|
||||
).toBeVisible();
|
||||
// Currently disabled due to recent Element Web is bypassing Lobby
|
||||
// await expect(
|
||||
// whistler.page
|
||||
// .locator('iframe[title="Element Call"]')
|
||||
// .contentFrame()
|
||||
// .getByTestId("lobby_joinCall"),
|
||||
// ).toBeVisible();
|
||||
//
|
||||
// await whistler.page
|
||||
// .locator('iframe[title="Element Call"]')
|
||||
// .contentFrame()
|
||||
// .getByTestId("lobby_joinCall")
|
||||
// .click();
|
||||
|
||||
await whistler.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall")
|
||||
.click();
|
||||
// Currrenty disabled due to recent Element Web not indicating the number of participants
|
||||
// await expect(
|
||||
// whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
// ).toBeVisible();
|
||||
|
||||
await expect(
|
||||
whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
).toBeVisible();
|
||||
// await expect(
|
||||
// brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
// ).toBeVisible();
|
||||
|
||||
// Whistler leaves
|
||||
await whistler.page.waitForTimeout(1000);
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
}
|
||||
],
|
||||
"semanticCommits": "disabled",
|
||||
"ignoreDeps": ["posthog-js"],
|
||||
"ignoreDeps": ["posthog-js", "eslint-plugin-matrix-org"],
|
||||
"vulnerabilityAlerts": {
|
||||
"schedule": ["at any time"],
|
||||
"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
|
||||
35
sdk/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# SDK mode
|
||||
|
||||
EC can be build in sdk mode. This will result in a compiled js file that can be imported in very simple webapps.
|
||||
|
||||
It allows to use matrixRTC in combination with livekit without relying on element call.
|
||||
|
||||
This is done by instantiating the call view model and exposing some useful behaviors (observables) and methods.
|
||||
|
||||
This folder contains an example index.html file that showcases the sdk in use (hosted on localhost:8123 with a webserver ellowing cors (for example `npx serve -l 81234 --cors`)) as a godot engine HTML export template.
|
||||
|
||||
## Widgets
|
||||
|
||||
The sdk mode is particularly interesting to be used in widgets where you do not need to pay attention to matrix login/cs api ...
|
||||
To create a widget see the example index.html file in this folder. And add it to EW via:
|
||||
`/addwidget <widgetUrl>` (see **url parameters** for more details on `<widgetUrl>`)
|
||||
|
||||
### url parameters
|
||||
|
||||
```
|
||||
widgetId = $matrix_widget_id
|
||||
perParticipantE2EE = true
|
||||
userId = $matrix_user_id
|
||||
deviceId = $org.matrix.msc3819.matrix_device_id
|
||||
baseUrl = $org.matrix.msc4039.matrix_base_url
|
||||
```
|
||||
|
||||
`parentUrl = // will be inserted automatically`
|
||||
|
||||
Full template use as `<widgetUrl>`:
|
||||
|
||||
```
|
||||
http://localhost:3000?widgetId=$matrix_widget_id&perParticipantE2EE=true&userId=$matrix_user_id&deviceId=$org.matrix.msc3819.matrix_device_id&baseUrl=$org.matrix.msc4039.matrix_base_url&roomId=$matrix_room_id
|
||||
```
|
||||
|
||||
the `$` prefixed variables will be replaced by EW on widget instantiation. (e.g. `$matrix_user_id` -> `@user:example.com` (url encoding will also be applied automatically by EW) -> `%40user%3Aexample.com`)
|
||||
55
sdk/helper.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file contains helper functions and types for the MatrixRTC SDK.
|
||||
*/
|
||||
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import { scan } from "rxjs";
|
||||
|
||||
import { widget as _widget } from "../src/widget";
|
||||
import { type LivekitRoomItem } from "../src/state/CallViewModel/CallViewModel";
|
||||
|
||||
export const logger = rootLogger.getChild("[MatrixRTCSdk]");
|
||||
|
||||
if (!_widget) throw Error("No widget. This webapp can only start as a widget");
|
||||
export const widget = _widget;
|
||||
|
||||
export const tryMakeSticky = (): void => {
|
||||
logger.info("try making sticky MatrixRTCSdk");
|
||||
void widget.api
|
||||
.setAlwaysOnScreen(true)
|
||||
.then(() => {
|
||||
logger.info("sticky MatrixRTCSdk");
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("failed to make sticky MatrixRTCSdk", error);
|
||||
});
|
||||
};
|
||||
export const TEXT_LK_TOPIC = "matrixRTC";
|
||||
/**
|
||||
* simple helper operator to combine the last emitted and the current emitted value of a rxjs observable
|
||||
*
|
||||
* I think there should be a builtin for this but i did not find it...
|
||||
*/
|
||||
export const currentAndPrev = scan<
|
||||
LivekitRoomItem[],
|
||||
{
|
||||
prev: LivekitRoomItem[];
|
||||
current: LivekitRoomItem[];
|
||||
}
|
||||
>(
|
||||
({ current: lastCurrentVal }, items) => ({
|
||||
prev: lastCurrentVal,
|
||||
current: items,
|
||||
}),
|
||||
{
|
||||
prev: [],
|
||||
current: [],
|
||||
},
|
||||
);
|
||||
87
sdk/index.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Godot MatrixRTC Widget</title>
|
||||
<meta charset="utf-8" />
|
||||
<script type="module">
|
||||
// TODO use the url where the matrixrtc-sdk.js file from dist is hosted
|
||||
import { createMatrixRTCSdk } from "http://localhost:8123/matrixrtc-sdk.js";
|
||||
|
||||
try {
|
||||
window.matrixRTCSdk = await createMatrixRTCSdk(
|
||||
"com.github.toger5.godot-game",
|
||||
);
|
||||
console.info("createMatrixRTCSdk was created!");
|
||||
} catch (e) {
|
||||
console.error("createMatrixRTCSdk", e);
|
||||
}
|
||||
const sdk = window.matrixRTCSdk;
|
||||
|
||||
// This is the main bridging interface to godot
|
||||
window.matrixRTCSdkGodot = {
|
||||
dataObs: sdk.data$,
|
||||
memberObs: sdk.members$,
|
||||
// join: sdk.join, // lets stick with autojoin for now
|
||||
sendData: sdk.sendData,
|
||||
leave: sdk.leave,
|
||||
connectedObs: sdk.connected$,
|
||||
};
|
||||
|
||||
console.info("matrixRTCSdk join ", sdk);
|
||||
const connectionState = sdk.join();
|
||||
console.info("matrixRTCSdk joined");
|
||||
|
||||
const div = document.getElementById("data");
|
||||
div.innerHTML = "<h3>Data:</h3>";
|
||||
|
||||
sdk.data$.subscribe((data) => {
|
||||
const child = document.createElement("p");
|
||||
child.innerHTML = JSON.stringify(data);
|
||||
div.appendChild(child);
|
||||
// TODO forward to godot
|
||||
});
|
||||
|
||||
sdk.members$.subscribe((memberObjects) => {
|
||||
// reset div
|
||||
const div = document.getElementById("members");
|
||||
div.innerHTML = "<h3>Members:</h3>";
|
||||
|
||||
// create member list
|
||||
const members = memberObjects.map((member) => member.membership.sender);
|
||||
console.info("members changed", members);
|
||||
for (const m of members) {
|
||||
console.info("member", m);
|
||||
const child = document.createElement("p");
|
||||
child.innerHTML = m;
|
||||
div.appendChild(child);
|
||||
}
|
||||
});
|
||||
|
||||
sdk.connected$.subscribe((connected) => {
|
||||
console.info("connected changed", connected);
|
||||
const div = document.getElementById("connect_status");
|
||||
div.innerHTML = connected ? "Connected" : "Disconnected";
|
||||
});
|
||||
|
||||
let engine = new Engine($GODOT_CONFIG);
|
||||
engine.startGame();
|
||||
</script>
|
||||
<!--// TODO use it as godot HTML template-->
|
||||
<script src="$GODOT_URL"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div
|
||||
id="overlay"
|
||||
style="position: absolute; top: 0; right: 0; background-color: #ffffff10"
|
||||
>
|
||||
<div id="connect_status"></div>
|
||||
<button onclick="window.matrixRTCSdk.leave();">Leave</button>
|
||||
<button onclick="window.matrixRTCSdk.sendData({prop: 'Hello, world!'});">
|
||||
Send Text
|
||||
</button>
|
||||
<div id="members"></div>
|
||||
<div id="data"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
308
sdk/main.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file is the entrypoint for the sdk build of element call: `yarn build:sdk`
|
||||
* use in widgets.
|
||||
* It exposes the `createMatrixRTCSdk` which creates the `MatrixRTCSdk` interface (see below) that
|
||||
* can be used to join a rtc session and exchange realtime data.
|
||||
* It takes care of all the tricky bits:
|
||||
* - sending delayed events
|
||||
* - finding the right sfu
|
||||
* - handling the media stream
|
||||
* - sending join/leave state or sticky events
|
||||
* - setting up encryption and scharing keys
|
||||
*/
|
||||
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
type Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
import {
|
||||
type CallMembership,
|
||||
MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
type Room as LivekitRoom,
|
||||
type TextStreamReader,
|
||||
type LocalParticipant,
|
||||
type RemoteParticipant,
|
||||
} from "livekit-client";
|
||||
|
||||
// TODO how can this get fixed? to just be part of `livekit-client`
|
||||
// Can this be done in the tsconfig.json
|
||||
import { type TextStreamInfo } from "../node_modules/livekit-client/dist/src/room/types";
|
||||
import { type Behavior, constant } from "../src/state/Behavior";
|
||||
import { createCallViewModel$ } from "../src/state/CallViewModel/CallViewModel";
|
||||
import { ObservableScope } from "../src/state/ObservableScope";
|
||||
import { getUrlParams } from "../src/UrlParams";
|
||||
import { MuteStates } from "../src/state/MuteStates";
|
||||
import { MediaDevices } from "../src/state/MediaDevices";
|
||||
import { E2eeType } from "../src/e2ee/e2eeType";
|
||||
import {
|
||||
currentAndPrev,
|
||||
logger,
|
||||
TEXT_LK_TOPIC,
|
||||
tryMakeSticky,
|
||||
widget,
|
||||
} from "./helper";
|
||||
import { ElementWidgetActions } from "../src/widget";
|
||||
import { type Connection } from "../src/state/CallViewModel/remoteMembers/Connection";
|
||||
|
||||
interface MatrixRTCSdk {
|
||||
/**
|
||||
* observe connected$ to track the state.
|
||||
* @returns
|
||||
*/
|
||||
join: () => void;
|
||||
/** @throws on leave errors */
|
||||
leave: () => void;
|
||||
data$: Observable<{ sender: string; data: string }>;
|
||||
/**
|
||||
* flattened list of members
|
||||
*/
|
||||
members$: Behavior<
|
||||
{
|
||||
connection: Connection | null;
|
||||
membership: CallMembership;
|
||||
participant: LocalParticipant | RemoteParticipant | null;
|
||||
}[]
|
||||
>;
|
||||
/** Use the LocalMemberConnectionState returned from `join` for a more detailed connection state */
|
||||
connected$: Behavior<boolean>;
|
||||
sendData?: (data: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
export async function createMatrixRTCSdk(
|
||||
application: string = "m.call",
|
||||
id: string = "",
|
||||
): Promise<MatrixRTCSdk> {
|
||||
logger.info("Hello");
|
||||
const client = await widget.client;
|
||||
logger.info("client created");
|
||||
const scope = new ObservableScope();
|
||||
const { roomId } = getUrlParams();
|
||||
if (roomId === null) throw Error("could not get roomId from url params");
|
||||
|
||||
const room = client.getRoom(roomId);
|
||||
if (room === null) throw Error("could not get room from client");
|
||||
|
||||
const mediaDevices = new MediaDevices(scope);
|
||||
const muteStates = new MuteStates(scope, mediaDevices, constant(true));
|
||||
const slot = { application, id };
|
||||
const rtcSession = new MatrixRTCSession(
|
||||
client,
|
||||
room,
|
||||
MatrixRTCSession.sessionMembershipsForSlot(room, slot),
|
||||
slot,
|
||||
);
|
||||
const callViewModel = createCallViewModel$(
|
||||
scope,
|
||||
rtcSession,
|
||||
room,
|
||||
mediaDevices,
|
||||
muteStates,
|
||||
{ encryptionSystem: { kind: E2eeType.PER_PARTICIPANT } },
|
||||
of({}),
|
||||
of({}),
|
||||
constant({ supported: false, processor: undefined }),
|
||||
);
|
||||
logger.info("CallViewModelCreated");
|
||||
// create data listener
|
||||
const data$ = new Subject<{ sender: string; data: string }>();
|
||||
|
||||
const lkTextStreamHandlerFunction = async (
|
||||
reader: TextStreamReader,
|
||||
participantInfo: { identity: string },
|
||||
livekitRoom: LivekitRoom,
|
||||
): Promise<void> => {
|
||||
const info = reader.info;
|
||||
logger.info(
|
||||
`Received text stream from ${participantInfo.identity}\n` +
|
||||
` Topic: ${info.topic}\n` +
|
||||
` Timestamp: ${info.timestamp}\n` +
|
||||
` ID: ${info.id}\n` +
|
||||
` Size: ${info.size}`, // Optional, only available if the stream was sent with `sendText`
|
||||
);
|
||||
|
||||
const participants = callViewModel.livekitRoomItems$.value.find(
|
||||
(i) => i.livekitRoom === livekitRoom,
|
||||
)?.participants;
|
||||
if (participants && participants.includes(participantInfo.identity)) {
|
||||
const text = await reader.readAll();
|
||||
logger.info(`Received text: ${text}`);
|
||||
data$.next({ sender: participantInfo.identity, data: text });
|
||||
} else {
|
||||
logger.warn(
|
||||
"Received text from unknown participant",
|
||||
participantInfo.identity,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const livekitRoomItemsSub = callViewModel.livekitRoomItems$
|
||||
.pipe(
|
||||
tap((beforecurrentAndPrev) => {
|
||||
logger.info(
|
||||
`LiveKit room items updated: ${beforecurrentAndPrev.length}`,
|
||||
beforecurrentAndPrev,
|
||||
);
|
||||
}),
|
||||
currentAndPrev,
|
||||
tap((aftercurrentAndPrev) => {
|
||||
logger.info(
|
||||
`LiveKit room items updated: ${aftercurrentAndPrev.current.length}, ${aftercurrentAndPrev.prev.length}`,
|
||||
aftercurrentAndPrev,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ prev, current }) => {
|
||||
const prevRooms = prev.map((i) => i.livekitRoom);
|
||||
const currentRooms = current.map((i) => i.livekitRoom);
|
||||
const addedRooms = currentRooms.filter((r) => !prevRooms.includes(r));
|
||||
const removedRooms = prevRooms.filter((r) => !currentRooms.includes(r));
|
||||
addedRooms.forEach((r) => {
|
||||
logger.info(`Registering text stream handler for room `);
|
||||
r.registerTextStreamHandler(
|
||||
TEXT_LK_TOPIC,
|
||||
(reader, participantInfo) =>
|
||||
void lkTextStreamHandlerFunction(reader, participantInfo, r),
|
||||
);
|
||||
});
|
||||
removedRooms.forEach((r) => {
|
||||
logger.info(`Unregistering text stream handler for room `);
|
||||
r.unregisterTextStreamHandler(TEXT_LK_TOPIC);
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
logger.info("Livekit room items subscription completed");
|
||||
for (const item of callViewModel.livekitRoomItems$.value) {
|
||||
logger.info("unregistering room item from room", item.url);
|
||||
item.livekitRoom.unregisterTextStreamHandler(TEXT_LK_TOPIC);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// create sendData function
|
||||
const sendFn: Behavior<(data: string) => Promise<TextStreamInfo>> =
|
||||
scope.behavior(
|
||||
callViewModel.localMatrixLivekitMember$.pipe(
|
||||
switchMap((m) => {
|
||||
if (!m)
|
||||
return of((data: string): never => {
|
||||
throw Error("local membership not yet ready.");
|
||||
});
|
||||
return m.participant.value$.pipe(
|
||||
map((p) => {
|
||||
if (p === null) {
|
||||
return (data: string): never => {
|
||||
throw Error("local participant not yet ready to send data.");
|
||||
};
|
||||
} else {
|
||||
return async (data: string): Promise<TextStreamInfo> =>
|
||||
p.sendText(data, { topic: TEXT_LK_TOPIC });
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const sendData = async (data: unknown): Promise<void> => {
|
||||
const dataString = JSON.stringify(data);
|
||||
logger.info("try sending: ", dataString);
|
||||
try {
|
||||
await Promise.resolve();
|
||||
const info = await sendFn.value(dataString);
|
||||
logger.info(`Sent text with stream ID: ${info.id}`);
|
||||
} catch (e) {
|
||||
logger.error("failed sending: ", dataString, e);
|
||||
}
|
||||
};
|
||||
|
||||
// after hangup gets called
|
||||
const leaveSubs = callViewModel.leave$.subscribe(() => {
|
||||
const scheduleWidgetCloseOnLeave = async (): Promise<void> => {
|
||||
const leaveResolver = Promise.withResolvers<void>();
|
||||
logger.info("waiting for RTC leave");
|
||||
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, (isJoined) => {
|
||||
logger.info("received RTC join update: ", isJoined);
|
||||
if (!isJoined) leaveResolver.resolve();
|
||||
});
|
||||
await leaveResolver.promise;
|
||||
logger.info("send Unstick");
|
||||
await widget.api
|
||||
.setAlwaysOnScreen(false)
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
"Failed to set call widget `alwaysOnScreen` to false",
|
||||
e,
|
||||
),
|
||||
);
|
||||
logger.info("send Close");
|
||||
await widget.api.transport
|
||||
.send(ElementWidgetActions.Close, {})
|
||||
.catch((e) => logger.error("Failed to send close action", e));
|
||||
};
|
||||
|
||||
// schedule close first and then leave (scope.end)
|
||||
void scheduleWidgetCloseOnLeave();
|
||||
|
||||
// actual hangup (ending scope will send the leave event.. its kinda odd. since you might end up closing the widget too fast)
|
||||
scope.end();
|
||||
});
|
||||
|
||||
logger.info("createMatrixRTCSdk done");
|
||||
|
||||
return {
|
||||
join: (): void => {
|
||||
// first lets try making the widget sticky
|
||||
tryMakeSticky();
|
||||
callViewModel.join();
|
||||
},
|
||||
leave: (): void => {
|
||||
callViewModel.hangup();
|
||||
leaveSubs.unsubscribe();
|
||||
livekitRoomItemsSub.unsubscribe();
|
||||
},
|
||||
data$,
|
||||
connected$: callViewModel.connected$,
|
||||
members$: scope.behavior(
|
||||
callViewModel.matrixLivekitMembers$.pipe(
|
||||
switchMap((members) => {
|
||||
const listOfMemberObservables = members.map((member) =>
|
||||
combineLatest([
|
||||
member.connection$,
|
||||
member.membership$,
|
||||
member.participant.value$,
|
||||
]).pipe(
|
||||
map(([connection, membership, participant]) => ({
|
||||
connection,
|
||||
membership,
|
||||
participant,
|
||||
})),
|
||||
// using shareReplay instead of a Behavior here because the behavior would need
|
||||
// a tricky scope.end() setup.
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
),
|
||||
);
|
||||
return combineLatest(listOfMemberObservables);
|
||||
}),
|
||||
),
|
||||
[],
|
||||
),
|
||||
sendData,
|
||||
};
|
||||
}
|
||||
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.
|
||||
*/
|
||||
|
||||
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 * as Sentry from "@sentry/react";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -17,12 +24,14 @@ import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||
import { Initializer } from "./initializer";
|
||||
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
|
||||
import { widget } from "./widget";
|
||||
import { useTheme } from "./useTheme";
|
||||
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);
|
||||
|
||||
@@ -50,7 +59,11 @@ const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
export const App: FC = () => {
|
||||
interface Props {
|
||||
vm: AppViewModel;
|
||||
}
|
||||
|
||||
export const App: FC<Props> = ({ vm }) => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
Initializer.init()
|
||||
@@ -62,41 +75,42 @@ export const App: FC = () => {
|
||||
.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 (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<BrowserRouter>
|
||||
<BackgroundProvider>
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
{loaded ? (
|
||||
<Suspense fallback={null}>
|
||||
<ClientProvider>
|
||||
<MediaDevicesProvider>
|
||||
<ProcessorProvider>
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={(error) => (
|
||||
<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 />
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
{header === HeaderStyle.AppBar ? (
|
||||
<AppBar>{content}</AppBar>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</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,
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
use,
|
||||
useRef,
|
||||
useMemo,
|
||||
type JSX,
|
||||
@@ -69,8 +69,7 @@ const ClientContext = createContext<ClientState | undefined>(undefined);
|
||||
|
||||
export const ClientContextProvider = ClientContext.Provider;
|
||||
|
||||
export const useClientState = (): ClientState | undefined =>
|
||||
useContext(ClientContext);
|
||||
export const useClientState = (): ClientState | undefined => use(ClientContext);
|
||||
|
||||
export function useClient(): {
|
||||
client?: MatrixClient;
|
||||
@@ -350,9 +349,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={state}>{children}</ClientContext.Provider>
|
||||
);
|
||||
return <ClientContext value={state}>{children}</ClientContext>;
|
||||
};
|
||||
|
||||
export type InitResult = {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
.error > h1 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error > p {
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ErrorView: FC<Props> = ({
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<Icon />
|
||||
<Icon aria-hidden />
|
||||
</BigIcon>
|
||||
<Heading as="h1" weight="semibold" size="md">
|
||||
{title}
|
||||
|
||||
@@ -10,7 +10,7 @@ import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Sentry from "@sentry/react";
|
||||
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 styles from "./FullScreenView.module.css";
|
||||
@@ -28,10 +28,10 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
const { hideHeader } = useUrlParams();
|
||||
const { header } = useUrlParams();
|
||||
return (
|
||||
<div className={classNames(styles.page, className)}>
|
||||
{!hideHeader && (
|
||||
{header === "standard" && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
@@ -67,7 +67,7 @@ export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => {
|
||||
) : (
|
||||
<ErrorView
|
||||
widget={widget}
|
||||
Icon={ErrorIcon}
|
||||
Icon={ErrorSolidIcon}
|
||||
title={t("error.generic")}
|
||||
rageshake
|
||||
fatal
|
||||
|
||||
@@ -6,12 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
type FC,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import { type Ref, type FC, type HTMLAttributes, type ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 { EncryptionLock } from "./room/EncryptionLock";
|
||||
import { useMediaQuery } from "./useMediaQuery";
|
||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||
|
||||
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||
ref?: Ref<HTMLElement>;
|
||||
children: ReactNode;
|
||||
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>(
|
||||
({ children, className, ...rest }, ref) => {
|
||||
return (
|
||||
export const Header: FC<HeaderProps> = ({
|
||||
ref,
|
||||
children,
|
||||
className,
|
||||
disconnectedBanner = true,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
ref={ref}
|
||||
className={classNames(styles.header, className)}
|
||||
@@ -38,9 +47,10 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
);
|
||||
},
|
||||
);
|
||||
{disconnectedBanner && <DisconnectedBanner />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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"] {
|
||||
animation: fade-out 130ms;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
|
||||
@@ -19,10 +19,26 @@ import mediaViewStyles from "../src/tile/MediaView.module.css";
|
||||
interface Props {
|
||||
audio?: 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
|
||||
export const RTCConnectionStats: FC<Props> = ({ audio, video, ...rest }) => {
|
||||
export const RTCConnectionStats: FC<Props> = ({
|
||||
audio,
|
||||
video,
|
||||
focusUrl,
|
||||
...rest
|
||||
}) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [modalContents, setModalContents] = useState<
|
||||
"video" | "audio" | "none"
|
||||
@@ -55,6 +71,13 @@ export const RTCConnectionStats: FC<Props> = ({ audio, video, ...rest }) => {
|
||||
</pre>
|
||||
</div>
|
||||
</Modal>
|
||||
{focusUrl && (
|
||||
<div>
|
||||
<Text as="span" size="xs" title="focusURL">
|
||||
{extractDomain(focusUrl)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{audio && (
|
||||
<div>
|
||||
<Button
|
||||
|
||||
@@ -45,6 +45,12 @@ interface Props {
|
||||
* A supporting icon to display within the toast.
|
||||
*/
|
||||
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,
|
||||
children,
|
||||
Icon,
|
||||
modal = true,
|
||||
}) => {
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
@@ -71,29 +78,33 @@ export const Toast: FC<Props> = ({
|
||||
}
|
||||
}, [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 (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<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 open={open} onOpenChange={onOpenChange} modal={modal}>
|
||||
{modal ? <DialogPortal>{content}</DialogPortal> : content}
|
||||
</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.
|
||||
*/
|
||||
|
||||
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 {
|
||||
getRoomIdentifierFromUrl,
|
||||
computeUrlParams,
|
||||
HeaderStyle,
|
||||
getUrlParams,
|
||||
UserIntent,
|
||||
} from "../src/UrlParams";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
@@ -82,6 +85,16 @@ describe("UrlParams", () => {
|
||||
getRoomIdentifierFromUrl("", `?roomId=${ROOM_ID}`, "").roomId,
|
||||
).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", () => {
|
||||
@@ -93,16 +106,16 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("preload", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(getUrlParams().preload).toBe(false);
|
||||
expect(computeUrlParams().preload).toBe(false);
|
||||
});
|
||||
|
||||
it("ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?preload=true").preload).toBe(false);
|
||||
expect(computeUrlParams("?preload=true").preload).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).preload,
|
||||
).toBe(true);
|
||||
@@ -111,19 +124,20 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("returnToLobby", () => {
|
||||
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", () => {
|
||||
expect(
|
||||
getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo")
|
||||
.returnToLobby,
|
||||
computeUrlParams(
|
||||
"?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
).toBe(true);
|
||||
@@ -132,12 +146,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("userId", () => {
|
||||
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", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).userId,
|
||||
).toBe("asd");
|
||||
@@ -146,12 +160,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("deviceId", () => {
|
||||
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", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).deviceId,
|
||||
).toBe("asd");
|
||||
@@ -160,12 +174,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("baseUrl", () => {
|
||||
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", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).baseUrl,
|
||||
).toBe("asd");
|
||||
@@ -175,72 +189,215 @@ describe("UrlParams", () => {
|
||||
describe("viaServers", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).viaServers,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
expect(computeUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("homeserver", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).homeserver,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
expect(computeUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("intent", () => {
|
||||
it("defaults to unknown", () => {
|
||||
expect(getUrlParams().intent).toBe(UserIntent.Unknown);
|
||||
const noIntentDefaults = {
|
||||
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", () => {
|
||||
expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown);
|
||||
expect(computeUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||
});
|
||||
|
||||
it("accepts start_call", () => {
|
||||
expect(getUrlParams("?intent=start_call").intent).toBe(
|
||||
UserIntent.StartNewCall,
|
||||
);
|
||||
expect(
|
||||
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", () => {
|
||||
expect(getUrlParams("?intent=join_existing").intent).toBe(
|
||||
UserIntent.JoinExistingCall,
|
||||
);
|
||||
expect(
|
||||
computeUrlParams(
|
||||
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
||||
),
|
||||
).toMatchObject(joinExistingCallDefaults("desktop"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("skipLobby", () => {
|
||||
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", () => {
|
||||
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(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).skipLobby,
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
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("noiseSuppression", () => {
|
||||
it("defaults to true", () => {
|
||||
expect(computeUrlParams().noiseSuppression).toBe(true);
|
||||
});
|
||||
|
||||
it("is parsed", () => {
|
||||
expect(
|
||||
computeUrlParams("?intent=start_call&noiseSuppression=true")
|
||||
.noiseSuppression,
|
||||
).toBe(true);
|
||||
expect(
|
||||
computeUrlParams("?intent=start_call&noiseSuppression&bar=foo")
|
||||
.noiseSuppression,
|
||||
).toBe(true);
|
||||
expect(computeUrlParams("?noiseSuppression=false").noiseSuppression).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("echoCancellation", () => {
|
||||
it("defaults to true", () => {
|
||||
expect(computeUrlParams().echoCancellation).toBe(true);
|
||||
});
|
||||
|
||||
it("is parsed", () => {
|
||||
expect(computeUrlParams("?echoCancellation=true").echoCancellation).toBe(
|
||||
true,
|
||||
);
|
||||
expect(computeUrlParams("?echoCancellation=false").echoCancellation).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:",
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
466
src/UrlParams.ts
@@ -8,10 +8,16 @@ Please see LICENSE in the repository root for full details.
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
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 { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
import { platform } from "./Platform";
|
||||
|
||||
interface RoomIdentifier {
|
||||
roomAlias: string | null;
|
||||
@@ -22,15 +28,25 @@ interface RoomIdentifier {
|
||||
export enum UserIntent {
|
||||
StartNewCall = "start_call",
|
||||
JoinExistingCall = "join_existing",
|
||||
StartNewCallDM = "start_call_dm",
|
||||
StartNewCallDMVoice = "start_call_dm_voice",
|
||||
JoinExistingCallDM = "join_existing_dm",
|
||||
JoinExistingCallDMVoice = "join_existing_dm_voice",
|
||||
Unknown = "unknown",
|
||||
}
|
||||
|
||||
// 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 {
|
||||
export enum HeaderStyle {
|
||||
None = "none",
|
||||
Standard = "standard",
|
||||
AppBar = "app_bar",
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
widgetId: string | null;
|
||||
parentUrl: string | null;
|
||||
@@ -42,42 +58,11 @@ export interface UrlParams {
|
||||
* is also not validated, where it is in useRoomIdentifier().
|
||||
*/
|
||||
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).
|
||||
*/
|
||||
userId: string | null;
|
||||
|
||||
/**
|
||||
* The display name to use for auto-registration.
|
||||
*/
|
||||
@@ -115,18 +100,106 @@ export interface UrlParams {
|
||||
*/
|
||||
posthogApiKey: string | null;
|
||||
/**
|
||||
* Whether the app is allowed to use fallback STUN servers for ICE in case the
|
||||
* user's homeserver doesn't provide any.
|
||||
* Whether to use end-to-end encryption.
|
||||
*/
|
||||
allowIceFallback: boolean;
|
||||
e2eEnabled: boolean;
|
||||
/**
|
||||
* E2EE password
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* 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.
|
||||
* In the widget this can be combined with preload to pass the device settings
|
||||
@@ -139,45 +212,58 @@ export interface UrlParams {
|
||||
*/
|
||||
returnToLobby: boolean;
|
||||
/**
|
||||
* The theme to use for element call.
|
||||
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
|
||||
* Whether and what type of notification EC should send, when the user joins the call.
|
||||
*/
|
||||
theme: 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;
|
||||
sendNotificationType?: RTCNotificationType;
|
||||
/**
|
||||
* 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.
|
||||
* Whether the app should automatically leave the call when there
|
||||
* is no one left in the call.
|
||||
* This is one part to make the call matrixRTC session behave like a telephone call.
|
||||
*/
|
||||
homeserver: string | null;
|
||||
autoLeaveWhenOthersLeft: boolean;
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up).
|
||||
* This is a no-op if not combined with sendNotificationType.
|
||||
*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
||||
* Whether to enable echo cancellation for audio capture.
|
||||
* Defaults to true.
|
||||
*/
|
||||
rageshakeSubmitUrl: string | null;
|
||||
echoCancellation?: boolean;
|
||||
/**
|
||||
* Whether to enable noise suppression for audio capture.
|
||||
* Defaults to true.
|
||||
*/
|
||||
noiseSuppression?: boolean;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
callIntent?: RTCCallIntent;
|
||||
}
|
||||
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
|
||||
// takes a UrlParams and returns a query string. That would enable us to
|
||||
@@ -219,6 +305,17 @@ class ParamParser {
|
||||
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[] {
|
||||
return [
|
||||
...this.fragmentParams.getAll(name),
|
||||
@@ -226,14 +323,30 @@ class ParamParser {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the flag exists and is not "false".
|
||||
*/
|
||||
public getFlagParam(name: string, defaultValue = false): boolean {
|
||||
const param = this.getParam(name);
|
||||
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 hash The URL hash
|
||||
* @returns The app parameters encoded in the URL
|
||||
@@ -242,36 +355,145 @@ export const getUrlParams = (
|
||||
search = window.location.search,
|
||||
hash = window.location.hash,
|
||||
): 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 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 parentUrl = parser.getParam("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,
|
||||
parentUrl,
|
||||
|
||||
// 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
|
||||
// the room ID is, then that's what it is.
|
||||
roomId: parser.getParam("roomId"),
|
||||
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,
|
||||
displayName: parser.getParam("displayName"),
|
||||
deviceId: isWidget ? parser.getParam("deviceId") : null,
|
||||
@@ -279,19 +501,9 @@ export const getUrlParams = (
|
||||
lang: parser.getParam("lang"),
|
||||
fonts: parser.getAllParams("font"),
|
||||
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"),
|
||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||
intent,
|
||||
posthogApiHost: parser.getParam("posthogApiHost"),
|
||||
posthogApiKey: parser.getParam("posthogApiKey"),
|
||||
posthogUserId:
|
||||
@@ -299,6 +511,54 @@ export const getUrlParams = (
|
||||
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
|
||||
sentryDsn: parser.getParam("sentryDsn"),
|
||||
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"),
|
||||
noiseSuppression: parser.getFlagParam("noiseSuppression", true),
|
||||
echoCancellation: parser.getFlagParam("echoCancellation", true),
|
||||
};
|
||||
|
||||
// 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 +617,16 @@ export function getRoomIdentifierFromUrl(
|
||||
|
||||
// Make sure roomId is valid
|
||||
let roomId: string | null = parser.getParam("roomId");
|
||||
if (!roomId?.startsWith("!")) {
|
||||
roomId = null;
|
||||
} else if (!roomId.includes("")) {
|
||||
roomId = null;
|
||||
if (roomId !== null) {
|
||||
// Replace any non-printable characters that another client may have inserted.
|
||||
// For instance on iOS, some copied links end up with zero width characters on the end which get encoded into the URL.
|
||||
// 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 {
|
||||
|
||||
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`] = `
|
||||
<div
|
||||
aria-labelledby="radix-:r4:"
|
||||
class="overlay animate modal dialog _glass_1x9g9_17"
|
||||
aria-labelledby="radix-«r4»"
|
||||
class="overlay animate modal dialog _glass_sepwu_8"
|
||||
data-state="open"
|
||||
id="radix-:r3:"
|
||||
id="radix-«r3»"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
@@ -17,8 +17,8 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
class="header"
|
||||
>
|
||||
<h2
|
||||
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
|
||||
id="radix-:r4:"
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
id="radix-«r4»"
|
||||
>
|
||||
My modal
|
||||
</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`] = `
|
||||
<div
|
||||
aria-labelledby="radix-:ra:"
|
||||
aria-labelledby="radix-«ra»"
|
||||
class="overlay modal drawer"
|
||||
data-state="open"
|
||||
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-direction="bottom"
|
||||
data-vaul-snap-points="false"
|
||||
id="radix-:r9:"
|
||||
id="radix-«r9»"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
@@ -60,7 +60,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
class="handle"
|
||||
/>
|
||||
<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;"
|
||||
>
|
||||
My modal
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
exports[`Toast > renders 1`] = `
|
||||
<button
|
||||
aria-labelledby="radix-:r4:"
|
||||
aria-labelledby="radix-«r4»"
|
||||
class="overlay animate toast"
|
||||
data-state="open"
|
||||
id="radix-:r3:"
|
||||
id="radix-«r3»"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<h3
|
||||
class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45"
|
||||
id="radix-:r4:"
|
||||
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36"
|
||||
id="radix-«r4»"
|
||||
>
|
||||
Hello world!
|
||||
</h3>
|
||||
|
||||
@@ -247,9 +247,8 @@ export class PosthogAnalytics {
|
||||
// wins, and the first writer will send tracking with an ID that doesn't match the one on the server
|
||||
// until the next time account data is refreshed and this function is called (most likely on next
|
||||
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
|
||||
const accountDataAnalyticsId = analyticsIdGenerator();
|
||||
await this.setAccountAnalyticsId(accountDataAnalyticsId);
|
||||
analyticsID = await this.hashedEcAnalyticsId(accountDataAnalyticsId);
|
||||
analyticsID = analyticsIdGenerator();
|
||||
await this.setAccountAnalyticsId(analyticsID);
|
||||
}
|
||||
} catch (e) {
|
||||
// The above could fail due to network requests, but not essential to starting the application,
|
||||
@@ -270,37 +269,14 @@ export class PosthogAnalytics {
|
||||
|
||||
private async getAnalyticsId(): Promise<string | null> {
|
||||
const client: MatrixClient = window.matrixclient;
|
||||
let accountAnalyticsId: string | null;
|
||||
if (widget) {
|
||||
accountAnalyticsId = getUrlParams().posthogUserId;
|
||||
return getUrlParams().posthogUserId;
|
||||
} else {
|
||||
const accountData = await client.getAccountDataFromServer(
|
||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||
);
|
||||
accountAnalyticsId = accountData?.id ?? null;
|
||||
return accountData?.id ?? null;
|
||||
}
|
||||
if (accountAnalyticsId) {
|
||||
// we dont just use the element web analytics ID because that would allow to associate
|
||||
// users between the two posthog instances. By using a hash from the username and the element web analytics id
|
||||
// it is not possible to conclude the element web posthog user id from the element call user id and vice versa.
|
||||
return await this.hashedEcAnalyticsId(accountAnalyticsId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async hashedEcAnalyticsId(
|
||||
accountAnalyticsId: string,
|
||||
): Promise<string> {
|
||||
const client: MatrixClient = window.matrixclient;
|
||||
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
|
||||
const bufferForPosthogId = await crypto.subtle.digest(
|
||||
"sha-256",
|
||||
new TextEncoder().encode(posthogIdMaterial),
|
||||
);
|
||||
const view = new Int32Array(bufferForPosthogId);
|
||||
return Array.from(view)
|
||||
.map((b) => Math.abs(b).toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
private async setAccountAnalyticsId(analyticsID: string): Promise<void> {
|
||||
|
||||
@@ -107,13 +107,13 @@ export class RageshakeSpanProcessor implements SpanProcessor {
|
||||
startTime,
|
||||
duration,
|
||||
references:
|
||||
span.parentSpanId === undefined
|
||||
span.parentSpanContext?.spanId === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
refType: "CHILD_OF",
|
||||
traceID: traceId,
|
||||
spanID: span.parentSpanId,
|
||||
spanID: span.parentSpanContext?.spanId,
|
||||
},
|
||||
],
|
||||
tags: dumpAttributes(span.attributes),
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
EndCallIcon,
|
||||
ShareScreenSolidIcon,
|
||||
SettingsSolidIcon,
|
||||
SwitchCameraSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
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"> {
|
||||
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.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
type MouseEvent,
|
||||
} from "react";
|
||||
import { type ComponentProps, type FC, type MouseEvent } from "react";
|
||||
import { Link as CpdLink } from "@vector-im/compound-web";
|
||||
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
@@ -26,31 +22,30 @@ export function useLink(
|
||||
return [href, onClick];
|
||||
}
|
||||
|
||||
type Props = Omit<
|
||||
ComponentPropsWithoutRef<typeof CpdLink>,
|
||||
"href" | "onClick"
|
||||
> & { to: LinkProps["to"]; state?: unknown };
|
||||
type Props = Omit<ComponentProps<typeof CpdLink>, "href" | "onClick"> & {
|
||||
to: LinkProps["to"];
|
||||
state?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* A version of Compound's link component that integrates with our router setup.
|
||||
* This is only for app-internal links.
|
||||
*/
|
||||
export const Link = forwardRef<HTMLAnchorElement, Props>(function Link(
|
||||
{ to, state, ...props },
|
||||
ref,
|
||||
) {
|
||||
export const Link: FC<Props> = ({ ref, to, state, ...props }) => {
|
||||
const [path, onClick] = useLink(to, state);
|
||||
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
|
||||
* than the normal Compound link component.
|
||||
*/
|
||||
export const ExternalLink = forwardRef<
|
||||
HTMLAnchorElement,
|
||||
ComponentPropsWithoutRef<"a">
|
||||
>(function ExternalLink({ className, children, ...props }, ref) {
|
||||
export const ExternalLink: FC<ComponentProps<"a">> = ({
|
||||
ref,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
@@ -62,4 +57,4 @@ export const ExternalLink = forwardRef<
|
||||
{children}
|
||||
</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.
|
||||
*/
|
||||
|
||||
import { type ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
import { type ComponentProps, type FC } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import type { LinkProps } from "react-router-dom";
|
||||
import { useLink } from "./Link";
|
||||
|
||||
type Props = Omit<
|
||||
ComponentPropsWithoutRef<typeof Button<"a">>,
|
||||
"as" | "href"
|
||||
> & { to: LinkProps["to"]; state?: unknown };
|
||||
type Props = Omit<ComponentProps<typeof Button<"a">>, "as" | "href"> & {
|
||||
to: LinkProps["to"];
|
||||
state?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* A version of Compound's button component that acts as a link and integrates
|
||||
* with our router setup.
|
||||
*/
|
||||
export const LinkButton = forwardRef<HTMLAnchorElement, Props>(
|
||||
function LinkButton({ to, state, ...props }, ref) {
|
||||
const [path, onClick] = useLink(to, state);
|
||||
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
||||
},
|
||||
);
|
||||
export const LinkButton: FC<Props> = ({ ref, to, state, ...props }) => {
|
||||
const [path, onClick] = useLink(to, state);
|
||||
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
||||
};
|
||||
|
||||
@@ -6,21 +6,22 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
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 { userEvent } from "@testing-library/user-event";
|
||||
import { type ReactNode } from "react";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
|
||||
import { ReactionToggleButton } from "./ReactionToggleButton";
|
||||
import { ElementCallReactionEventType } from "../reactions";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
import { alice, local, localRtcMember } from "../utils/test-fixtures";
|
||||
import { type MockRTCSession } from "../utils/test";
|
||||
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({
|
||||
rtcSession,
|
||||
@@ -33,7 +34,7 @@ function TestComponent({
|
||||
<TooltipProvider>
|
||||
<ReactionsSenderProvider
|
||||
vm={vm}
|
||||
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
||||
rtcSession={rtcSession.asMockedSession()}
|
||||
>
|
||||
<ReactionToggleButton vm={vm} identifier={localIdent} />
|
||||
</ReactionsSenderProvider>
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import classNames from "classnames";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||
import styles from "./ReactionToggleButton.module.css";
|
||||
@@ -35,7 +33,8 @@ import {
|
||||
ReactionsRowSize,
|
||||
} from "../reactions";
|
||||
import { Modal } from "../Modal";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
|
||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
raised: boolean;
|
||||
@@ -180,12 +179,8 @@ export function ReactionToggleButton({
|
||||
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
|
||||
const isHandRaised = useObservableState(
|
||||
vm.handsRaised$.pipe(map((v) => !!v[identifier])),
|
||||
);
|
||||
const canReact = useObservableState(
|
||||
vm.reactions$.pipe(map((v) => !v[identifier])),
|
||||
);
|
||||
const isHandRaised = !!useBehavior(vm.handsRaised$)[identifier];
|
||||
const canReact = !useBehavior(vm.reactions$)[identifier];
|
||||
|
||||
useEffect(() => {
|
||||
// Clear whenever the reactions menu state changes.
|
||||
|
||||
@@ -9,8 +9,8 @@ exports[`Can close reaction dialog 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":rb5:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
aria-labelledby="«rbb»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -26,7 +26,7 @@ exports[`Can close reaction dialog 1`] = `
|
||||
>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
@@ -43,8 +43,8 @@ exports[`Can fully expand emoji picker 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r7m:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
aria-labelledby="«r7m»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -60,7 +60,7 @@ exports[`Can fully expand emoji picker 1`] = `
|
||||
>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
@@ -74,8 +74,8 @@ exports[`Can lower hand 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r36:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
aria-labelledby="«r36»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -91,7 +91,7 @@ exports[`Can lower hand 1`] = `
|
||||
>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
@@ -108,8 +108,8 @@ exports[`Can open menu 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r0:"
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
aria-labelledby="«r0»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -125,7 +125,7 @@ exports[`Can open menu 1`] = `
|
||||
>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
@@ -139,8 +139,8 @@ exports[`Can raise hand 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r1j:"
|
||||
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
|
||||
aria-labelledby="«r1j»"
|
||||
class="_button_vczzf_8 raisedButton _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
@@ -155,7 +155,7 @@ exports[`Can raise hand 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
|
||||