Compare commits

...

125 Commits

Author SHA1 Message Date
David Baker
42aeecd964 Merge pull request #706 from vector-im/dbkr/fix_ptt_safari
Fix Walkie-Talkie mode in Safari
2022-11-02 20:15:35 +00:00
David Baker
45dbaa968a Fix Walkie-Talkie mode in Safari
We didn't check whether we actually had a video device when seeing
if the current video devices was in the list of devices, so this
caused loops which confused Safari.
2022-11-02 20:03:56 +00:00
Robin
db66700595 Merge pull request #704 from robintown/feedless-tiles
Don't show toolbar buttons on connecting tiles
2022-11-02 12:36:36 -04:00
Robin
f93c022c27 Merge pull request #703 from robintown/fix-key-warning
Fix a warning about missing keys
2022-11-02 12:36:28 -04:00
Robin Townsend
84a92845c3 Don't show toolbar buttons on connecting tiles
Because connecting tiles don't have a feed, clicking the local volume button would cause a soft crash. This also fixes a few strict mode errors in the surrounding area while we're at it.
2022-11-02 12:34:31 -04:00
Robin Townsend
8731f83fb5 Fix a warning about missing keys 2022-11-02 12:15:32 -04:00
Robin
7b71e9b20f Merge pull request #692 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2022-11-02 11:47:14 -04:00
Avery
8a245533bb Translated using Weblate (Spanish)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/es/
2022-11-02 15:45:14 +00:00
Avery
43e80dd9e7 Added translation using Weblate (Spanish) 2022-11-02 15:45:14 +00:00
Robin
9b93d45ea0 Merge pull request #702 from robintown/no-double-clients
Don't doubly initialize the client in strict mode
2022-11-02 11:28:21 -04:00
Robin Townsend
f1c050c327 Don't doubly initialize the client in strict mode 2022-11-02 11:23:05 -04:00
Robin
99dbcffcaf Merge pull request #697 from robintown/rename
Clean up remaining 'Matrix Video Chat' branding
2022-11-02 07:45:59 -04:00
Robin
57da08e6ff Merge pull request #698 from robintown/fix-triage-automation
Attempt to fix triage automation
2022-11-02 07:45:47 -04:00
Robin
e1d6f99f25 Merge pull request #696 from robintown/readme
Improve the README
2022-11-02 07:45:35 -04:00
Robin Townsend
956388807f Attempt to fix triage automation
The 'PR_' ID format appears to be deprecated.
2022-11-01 22:31:56 -04:00
Robin Townsend
9bd6f346e9 Improve the README
Apparently linking matrix-js-sdk isn't necessary - I was able to clone and launch the app without a local matrix-js-sdk copy without problems.
2022-11-01 22:21:09 -04:00
Robin Townsend
b1083baacf Clean up remaining 'Matrix Video Chat' branding 2022-11-01 22:19:44 -04:00
Robin Townsend
5b5c649b49 Update matrix-js-sdk 2022-11-01 19:56:30 -04:00
David Baker
2346ad9b7e Merge pull request #695 from vector-im/dbkr/fix_missing_tile_bug
Fix missing tile bug
2022-11-01 18:20:23 +00:00
David Baker
feeb9c4e7c Fix missing tile bug
The 'connecting' tile change meant that we could have tiles right
at the start of the call where we wouldn't have before, and in fact
could have tiles for other users before we even had a tile for ourself.
This threw off the logic for ordering tiles which had a special case
for 1:1 calling which assumed that one of the tiles in a 1:1 call was
the local user. In this case, this assumption wasn't true at the very
start of the call, so the tile orders got assigned incorrectly and then
persisted for the rest of the call.

Fixes https://github.com/vector-im/element-call/issues/694
2022-11-01 18:10:11 +00:00
Robin
0767a6f9dd Merge pull request #691 from robintown/triage-automation
Add basic triage automation
2022-11-01 07:24:39 -04:00
Robin Townsend
77b139226b Add basic triage automation 2022-10-31 16:48:20 -04:00
Robin
658185a119 Merge pull request #690 from robintown/link-underline
Add an underline to blue (external) links on hover
2022-10-31 16:11:16 -04:00
Robin Townsend
37d9e48c0a Add an underline to blue (external) links on hover 2022-10-31 15:16:02 -04:00
Robin
a1c61fc1fd Merge pull request #679 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2022-10-31 15:07:29 -04:00
Priit Jõerüüt
68a50146b1 Translated using Weblate (Estonian)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
effe892757 Translated using Weblate (Estonian)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
f5bc29a226 Translated using Weblate (Estonian)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
Priit Jõerüüt
4189fc7e84 Translated using Weblate (Estonian)
Currently translated at 98.4% (130 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
45dee7906d Translated using Weblate (Estonian)
Currently translated at 98.4% (130 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
f7506ae9b7 Translated using Weblate (Estonian)
Currently translated at 94.6% (125 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
Priit Jõerüüt
7bfe3f9c47 Translated using Weblate (Estonian)
Currently translated at 94.6% (125 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
2954290a48 Translated using Weblate (Estonian)
Currently translated at 85.6% (113 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
Priit Jõerüüt
1dbd0e76ad Translated using Weblate (Estonian)
Currently translated at 85.6% (113 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
afc4c4ac4e Translated using Weblate (Estonian)
Currently translated at 59.0% (78 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
Priit Jõerüüt
368f69f52d Translated using Weblate (Estonian)
Currently translated at 59.0% (78 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
Priit Jõerüüt
de0a68fbcd Translated using Weblate (Estonian)
Currently translated at 40.9% (54 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
d72b9a8d82 Translated using Weblate (Estonian)
Currently translated at 40.9% (54 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
aethralis
f34733bfb2 Translated using Weblate (Estonian)
Currently translated at 11.3% (15 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2022-10-31 19:04:12 +00:00
fkwp
6d7b7de76e Translated using Weblate (German)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-31 19:04:12 +00:00
Robin
9b3835c166 Merge pull request #689 from robintown/logo-description
Add an accessible description to the logo
2022-10-31 14:44:46 -04:00
Robin
54d903933b Merge pull request #688 from robintown/load-olm-once
Ensure that Olm is only loaded once
2022-10-31 14:44:33 -04:00
Robin Townsend
dc1e086ea2 Add an accessible description to the logo 2022-10-31 14:03:41 -04:00
Robin Townsend
a3b8dfcdf2 Update matrix-js-sdk 2022-10-31 13:59:20 -04:00
Robin
e6eb2e093c Merge pull request #685 from robintown/reduced-motion
Disable animations for users that prefer reduced motion
2022-10-31 13:45:58 -04:00
Robin Townsend
d4caa1585b Ensure that Olm is only loaded once
React 18's strict mode intentionally mounts all components twice, which was causing Olm to get double-loaded. Also, it doesn't need to be loaded if the app is running as a widget.
2022-10-31 13:43:03 -04:00
Robin
363ea2e669 Merge pull request #687 from robintown/hide-abort-errors
Don't log AbortErrors from videos that are never played
2022-10-31 13:37:08 -04:00
Robin Townsend
c25874ced5 Don't log AbortErrors from videos that are never played
It's normal for the play operation on video feeds to be cancelled due to tiles unmounting quickly (especially with React 18's strict mode), but it logs a scary error which can be misleading during debugging.
2022-10-31 12:34:56 -04:00
Robin Townsend
7932d7a471 Disable animations for users that prefer reduced motion 2022-10-31 11:46:17 -04:00
David Baker
e42a83bbc4 Merge pull request #674 from vector-im/SimonBrandner/fix/i-am-stupid-sometimes
Avoid Olm loading loop
2022-10-27 16:11:51 +01:00
Šimon Brandner
cb5f7a3f84 Avoid Olm loading loop
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-27 16:48:13 +02:00
Robin
99b3880afc Merge pull request #673 from robintown/fix-warnings
Switch to the React 18 createRoot API
2022-10-27 10:05:16 -04:00
Robin Townsend
18b5ae9d4a Fix lint 2022-10-27 09:54:31 -04:00
Robin Townsend
14a1ff7fe4 Switch to the React 18 createRoot API 2022-10-27 09:48:37 -04:00
Robin
0d22ef2104 Merge pull request #672 from robintown/fatal-error
Show an error when the browser does not support WebRTC
2022-10-27 09:46:45 -04:00
Robin
5ceda993d4 Merge pull request #651 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2022-10-27 09:46:22 -04:00
Robin Townsend
7038a76fc1 Fix type error 2022-10-27 09:43:17 -04:00
Robin Townsend
1a329966ba Show an error when the browser does not support WebRTC 2022-10-27 09:42:27 -04:00
Robin Townsend
31decc6577 Update matrix-js-sdk 2022-10-27 09:40:59 -04:00
aethralis
878c183548 Added translation using Weblate (Estonian) 2022-10-27 13:23:49 +00:00
Šimon Brandner
4e295f7708 Translated using Weblate (Czech)
Currently translated at 61.3% (81 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/cs/
2022-10-27 08:33:02 +00:00
Ihor Hordiichuk
b2b233ae00 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2022-10-27 08:33:02 +00:00
Glandos
86ec677675 Translated using Weblate (French)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2022-10-27 08:33:02 +00:00
Linerly
1104aa2412 Translated using Weblate (Indonesian)
Currently translated at 100.0% (132 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2022-10-27 08:33:02 +00:00
Vri
d9575a0dcf Translated using Weblate (German)
Currently translated at 99.2% (131 of 132 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-27 08:33:02 +00:00
Weblate
7b4650f5f4 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2022-10-27 08:33:02 +00:00
Ariodaad
595eb85431 Translated using Weblate (Persian)
Currently translated at 96.1% (126 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fa/
2022-10-27 08:33:02 +00:00
Przemysław Romanik
4e7e707a92 Translated using Weblate (Polish)
Currently translated at 98.4% (129 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2022-10-27 08:33:02 +00:00
Ariodaad
6186d1d7d2 Translated using Weblate (Persian)
Currently translated at 81.6% (107 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fa/
2022-10-27 08:33:02 +00:00
Ihor Hordiichuk
9b493da519 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2022-10-27 08:33:02 +00:00
Rodion Borisov
b81889bf15 Translated using Weblate (Russian)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ru/
2022-10-27 08:33:02 +00:00
mmehdishafiee
610e320031 Translated using Weblate (Persian)
Currently translated at 80.9% (106 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fa/
2022-10-27 08:33:02 +00:00
Ariodaad
983f8eb737 Translated using Weblate (Persian)
Currently translated at 80.9% (106 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fa/
2022-10-27 08:33:02 +00:00
mmehdishafiee
a2040bf4c2 Translated using Weblate (Persian)
Currently translated at 54.9% (72 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fa/
2022-10-27 08:33:02 +00:00
Šimon Brandner
d8d72d023c Translated using Weblate (Czech)
Currently translated at 11.4% (15 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/cs/
2022-10-27 08:33:02 +00:00
Ihor Hordiichuk
faf4d1b49b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2022-10-27 08:33:02 +00:00
Linerly
9045ba925a Translated using Weblate (Indonesian)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2022-10-27 08:33:02 +00:00
Vri
ff77fa2543 Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-27 08:33:02 +00:00
Przemysław Romanik
66d867b5c7 Added translation using Weblate (Polish) 2022-10-27 08:33:02 +00:00
Šimon Brandner
66ecb7c4e9 Merge pull request #667 from vector-im/SimonBrandner/feat/ice-fallback 2022-10-26 17:52:44 +02:00
Šimon Brandner
e01136e6bb Change default
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-26 16:33:22 +02:00
Šimon Brandner
ee6438a4bd Merge pull request #666 from vector-im/SimonBrandner/task/tests 2022-10-26 15:33:07 +02:00
Šimon Brandner
c4c99c4bcb Use a better var name
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-26 14:27:41 +02:00
Šimon Brandner
cef88e2894 Add option to allow ICE server fallback
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-26 13:58:41 +02:00
Šimon Brandner
88f4b889a1 Add CallList test
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-26 13:21:12 +02:00
Šimon Brandner
e909ff5ad0 Add jest workflow
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-26 13:21:12 +02:00
Šimon Brandner
fcaa126147 Add jest
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-26 13:21:06 +02:00
David Baker
b8af9a0733 Merge pull request #648 from vector-im/dbkr/tiles_for_everyone
Show tiles for members we're trying to connect to
2022-10-25 12:56:43 +01:00
Robin
97c294687e Merge pull request #662 from robintown/matryoshka-rageshake
Make rageshake requests work in matryoshka mode
2022-10-24 15:38:58 -04:00
David Baker
50e9e33922 i18n 2022-10-24 19:02:01 +01:00
Robin Townsend
2b74c2d9ce Merge branch 'main' into matryoshka-rageshake 2022-10-24 13:59:35 -04:00
David Baker
736aa95133 Fix type 2022-10-24 18:58:55 +01:00
David Baker
b39b3c072d Merge pull request #652 from vector-im/dbkr/fixec_screenshare
Fix screen sharing
2022-10-24 18:54:52 +01:00
Robin Townsend
d2a5baf403 Update matrix-js-sdk 2022-10-24 13:51:54 -04:00
Robin Townsend
e1090377f9 Merge branch 'main' into matryoshka-rageshake 2022-10-24 13:50:45 -04:00
Robin
669b1403fc Merge pull request #663 from robintown/matryoshka-avatar
Fix avatars of remote participants in matryoshka mode
2022-10-24 13:49:32 -04:00
Robin
a4076cd528 Merge pull request #661 from robintown/i18n-ci
Add i18n CI
2022-10-24 13:49:13 -04:00
Robin
877726dc3c Merge pull request #656 from robintown/1-1-freedom
Re-enable focusing tiles in 1:1 calls
2022-10-24 13:49:00 -04:00
Robin
c7a2c7110a Merge pull request #655 from robintown/missing-translations
Make more of the lobby translateable
2022-10-24 13:48:49 -04:00
Robin Townsend
efe9e6c2b3 Fix avatars of remote participants in matryoshka mode
RoomWidgetClient doesn't do lazy loading, so it only has the state event data to work with and not the lazy loaded user object.

Previously avatars of remote participants were all replaced by fallback avatars.
2022-10-24 13:19:16 -04:00
Robin Townsend
9bdd5b0e58 Make rageshake requests work in matryoshka mode 2022-10-24 12:30:30 -04:00
Robin Townsend
fbf7b5d022 Add i18n CI 2022-10-24 10:53:55 -04:00
Robin Townsend
7ad84de9c2 Re-enable focusing tiles in 1:1 calls 2022-10-24 10:22:51 -04:00
Robin Townsend
bf94a5dcaf Make more of the lobby translateable 2022-10-24 10:17:12 -04:00
David Baker
537341da3a Fix storybook 2022-10-24 10:06:38 +01:00
David Baker
247ed95976 Merge pull request #647 from vector-im/dbkr/fix_missing_key
Fix missing key in tab container
2022-10-24 10:02:18 +01:00
Robin
4e9abfa3b0 Merge pull request #654 from robintown/no-devices
Fix joining calls in matryoshka mode without audio or video inputs
2022-10-23 13:33:15 -04:00
Robin Townsend
d26de7d27f Fix joining calls in matryoshka mode without audio or video inputs
The join handler was requesting a stream with both video and audio, even if the system lacked video or audio devices. Requesting one of audio or video is enough to get all device labels.
2022-10-23 13:22:43 -04:00
David Baker
3891b042e3 Use commit from js-sdk branch 2022-10-21 21:10:04 +01:00
David Baker
821622f71c Types 2022-10-21 20:28:33 +01:00
David Baker
71dcc94166 Fix screen sharing
* Make the embedded mode screen sharing a request-each-way rather
   than request-and-reply, since replies time out and so can't wait
   for the user.
 * Try normal screen sharing first, then fall back to using the widget
   API if it fails (for lack of a good way of detecting when we
   should be using the widget API).

Fixes https://github.com/vector-im/element-call/issues/649
2022-10-21 20:19:52 +01:00
David Baker
1ea9432769 Show tiles for members we're trying to connect to
This should help give more context on what's going wrong in
splitbrain scenarios.

If users leave calls uncleanly, their tile will remain in until
their member event times out, which will be an hour from when they
joined the call. See https://github.com/vector-im/element-call/issues/639.

Part of https://github.com/vector-im/element-call/issues/616
2022-10-21 17:24:56 +01:00
David Baker
fa4b4eabdf Fix missing key in tab container 2022-10-21 16:26:44 +01:00
David Baker
39c30ebf56 Merge pull request #642 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2022-10-21 14:12:24 +01:00
Šimon Brandner
3ef84c069c Merge pull request #645 from vector-im/SimonBrandner/feat/screenshare-ec 2022-10-20 19:11:47 +02:00
Šimon Brandner
4ee6e450b7 Make Element Call screensharing work on desktop
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-20 19:07:27 +02:00
Vri
50c373e091 Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-20 14:15:57 +00:00
kongo09
5fe92ee30b Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-20 14:15:57 +00:00
Vri
c0d338a504 Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-20 14:14:14 +00:00
kongo09
7859f3813e Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-20 14:14:14 +00:00
Vri
b4d2b8159b Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-20 14:09:09 +00:00
fkwp
9d0e77adf7 Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-20 14:09:09 +00:00
mmehdishafiee
afa7ae69d2 Added translation using Weblate (Persian) 2022-10-20 14:00:52 +00:00
Nui Harime
79506653eb Translated using Weblate (Ukrainian)
Currently translated at 0.7% (1 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2022-10-19 18:31:19 +00:00
Šimon Brandner
f5fea48ccd Added translation using Weblate (Czech) 2022-10-19 18:31:19 +00:00
54 changed files with 3204 additions and 281 deletions

View File

@@ -6,6 +6,7 @@
# Used for determining the homeserver to use for short urls etc. # Used for determining the homeserver to use for short urls etc.
# VITE_DEFAULT_HOMESERVER=http://localhost:8008 # VITE_DEFAULT_HOMESERVER=http://localhost:8008
# VITE_FALLBACK_STUN_ALLOWED=false
# Used for submitting debug logs to an external rageshake server # Used for submitting debug logs to an external rageshake server
# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit # VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit

View File

@@ -28,6 +28,10 @@ module.exports = {
"plugin:matrix-org/react", "plugin:matrix-org/react",
"prettier", "prettier",
], ],
rules: {
// We're aiming to convert this code to strict mode
"@typescript-eslint/no-non-null-assertion": "off",
},
}, },
], ],
settings: { settings: {

View File

@@ -16,6 +16,8 @@ jobs:
run: "yarn install" run: "yarn install"
- name: Prettier - name: Prettier
run: "yarn run prettier:check" run: "yarn run prettier:check"
- name: i18n
run: "yarn run i18n:check"
- name: ESLint - name: ESLint
run: "yarn run lint:js" run: "yarn run lint:js"
- name: Type check - name: Type check

18
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Run jest tests
on:
pull_request: {}
jobs:
jest:
name: Run jest tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install dependencies
run: "yarn install"
- name: Jest
run: "yarn run test"

27
.github/workflows/triage-incoming.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Move new issues into triage board
on:
issues:
types: [ opened ]
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- uses: octokit/graphql-action@v2.x
id: add_to_project
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AH1sa"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@@ -1,26 +1,58 @@
# Element Call # Element Call
Showcase for full mesh video chat powered by Matrix, implementing [MSC3401](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md). [![Chat](https://img.shields.io/matrix/webrtc:matrix.org)](https://matrix.to/#/#webrtc:matrix.org)
[![Translate](https://translate.element.io/widgets/element-call/-/element-call/svg-badge.svg)](https://translate.element.io/engage/element-call/)
Discussion in [#webrtc:matrix.org: ![#webrtc:matrix.org](https://img.shields.io/matrix/webrtc:matrix.org)](https://matrix.to/#/#webrtc:matrix.org) Full mesh group calls powered by [Matrix](https://matrix.org), implementing [MatrixRTC](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md).
## Getting Started To try it out, visit our hosted version at [call.element.io](https://call.element.io). You can also find the latest development version continuously deployed to [element-call.netlify.app](https://element-call.netlify.app).
`element-call` is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902). Because of how this package is configured and Vite's requirements, you will need to clone it locally and use `yarn link` to stich things together. ## Host it yourself
First clone, install, and link `matrix-js-sdk` Until prebuilt tarballs are available, you'll need to build Element Call from source. First, clone and install the package:
```
git clone https://github.com/vector-im/element-call.git
cd element-call
yarn
cp .env.example .env
```
You can now edit the configuration in `.env` to your liking. The most important thing is to set `VITE_DEFAULT_HOMESERVER` to the homeserver that the app should use, such as `https://call.ems.host`.
Next, build the project:
```
yarn build
```
If all went well, you can now find the build output under `dist` as a series of static files. These can be hosted using any web server of your choice.
Because Element Call uses client-side routing, your server must be able to route any requests to non-existing paths back to `/index.html`. For example, in Nginx you can achieve this with the `try_files` directive:
```
server {
...
location / {
...
try_files $uri /$uri /index.html;
}
}
```
## Development
Element Call is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/2553). To get started, clone, install, and link the package:
``` ```
git clone https://github.com/matrix-org/matrix-js-sdk.git git clone https://github.com/matrix-org/matrix-js-sdk.git
cd matrix-js-sdk cd matrix-js-sdk
git checkout robertlong/group-call git switch robertlong/group-call
yarn yarn
yarn link yarn link
``` ```
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008. Next, we can set up this project:
Finally we can set up this project.
``` ```
git clone https://github.com/vector-im/element-call.git git clone https://github.com/vector-im/element-call.git
@@ -28,6 +60,13 @@ cd element-call
yarn yarn
yarn link matrix-js-sdk yarn link matrix-js-sdk
cp .env.example .env cp .env.example .env
```
By default, the app expects you to have [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008. If you wish to use another homeserver, you can set it in your `.env` file.
You're now ready to launch the development server:
```
yarn dev yarn dev
``` ```
@@ -35,20 +74,6 @@ yarn dev
Configuration options are documented in the `.env` file. Configuration options are documented in the `.env` file.
## License ## Translation
All files in this project are: If you'd like to help translate Element Call, head over to [translate.element.io](https://translate.element.io/engage/element-call/). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts.
Copyright 2021-2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

15
babel.config.cjs Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
"@babel/preset-react",
"@babel/preset-typescript",
],
plugins: ["babel-plugin-transform-vite-meta-env"],
};

View File

@@ -12,7 +12,9 @@
"lint": "yarn lint:types && yarn lint:js", "lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src", "lint:js": "eslint --max-warnings 0 src",
"lint:types": "tsc", "lint:types": "tsc",
"i18n": "node_modules/i18next-parser/bin/cli.js" "i18n": "node_modules/i18next-parser/bin/cli.js",
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
@@ -43,15 +45,15 @@
"i18next": "^21.10.0", "i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8", "i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4", "i18next-http-backend": "^1.4.4",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#5a0787349d4951012eabe72f3363c17bdcda0d56", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#7ec726e10be835588d4b188fcd3d137b4690d79a",
"matrix-widget-api": "^1.0.0", "matrix-widget-api": "^1.0.0",
"mermaid": "^8.13.8", "mermaid": "^8.13.8",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pako": "^2.0.4", "pako": "^2.0.4",
"postcss-preset-env": "^7", "postcss-preset-env": "^7",
"re-resizable": "^6.9.0", "re-resizable": "^6.9.0",
"react": "^17.0.0", "react": "18",
"react-dom": "^17.0.0", "react-dom": "18",
"react-i18next": "^11.18.6", "react-i18next": "^11.18.6",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-router": "6", "react-router": "6",
@@ -65,10 +67,13 @@
"@babel/core": "^7.16.5", "@babel/core": "^7.16.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@storybook/react": "^6.5.0-alpha.5", "@storybook/react": "^6.5.0-alpha.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/request": "^2.48.8", "@types/request": "^2.48.8",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.22.0",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"babel-plugin-transform-vite-meta-env": "^1.0.3",
"eslint": "^8.14.0", "eslint": "^8.14.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
@@ -78,6 +83,9 @@
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.5.0",
"i18next-parser": "^6.6.0", "i18next-parser": "^6.6.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.2.2",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"sass": "^1.42.1", "sass": "^1.42.1",
"storybook-builder-vite": "^0.1.12", "storybook-builder-vite": "^0.1.12",
@@ -86,5 +94,20 @@
"vite": "^2.4.2", "vite": "^2.4.2",
"vite-plugin-html-template": "^1.1.0", "vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^0.4.0" "vite-plugin-svgr": "^0.4.0"
},
"jest": {
"testEnvironment": "jsdom",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
],
"transformIgnorePatterns": [
"/node_modules/(?!d3)+$",
"/node_modules/(?!internmap)+$"
],
"moduleNameMapper": {
"\\.(css|less|svg)+$": "identity-obj-proxy",
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
}
} }
} }

View File

@@ -21,7 +21,6 @@
"Connection lost": "Връзката се изгуби", "Connection lost": "Връзката се изгуби",
"Copied!": "Копирано!", "Copied!": "Копирано!",
"Copy and share this call link": "Копирай и сподели връзка към разговора", "Copy and share this call link": "Копирай и сподели връзка към разговора",
"Copy call link and join later": "Копирай връзка към разговора и се присъедини по-късно",
"Create account": "Създай акаунт", "Create account": "Създай акаунт",
"Debug log": "Debug логове", "Debug log": "Debug логове",
"Debug log request": "Заявка за debug логове", "Debug log request": "Заявка за debug логове",

View File

@@ -0,0 +1,83 @@
{
"Copy and share this call link": "Zkopírujte a sdílejte odkaz na hovor",
"Copied!": "Zkopírováno!",
"Connection lost": "Připojení ztraceno",
"Confirm password": "Potvrdit heslo",
"Close": "Zavřít",
"Change layout": "Změnit rozložení",
"Camera/microphone permissions needed to join the call.": "Oprávnění k přístupu ke kameře/mikrofonu jsou nutná pro připojení k hovoru.",
"Camera {{n}}": "Kamera {{n}}",
"Camera": "Kamera",
"Call link copied": "Odkaz na hovor zkopírován",
"Avatar": "Avatar",
"Audio": "Audio",
"Accept microphone permissions to join the call.": "Povolte přístup k mikrofonu pro připojení k hovoru.",
"Accept camera/microphone permissions to join the call.": "Povolte přístup ke kameře/mikrofonu pro připojení do hovoru.",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Vytvořit účet</0> Or <2>Jako host</2>",
"Your recent calls": "Vaše nedávné hovory",
"You can't talk at the same time": "Teď nemůžete mluvit",
"Yes, join call": "Ano, připojit se",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC není podporováno nebo je zakázáno tímto prohlížečem.",
"Waiting for other participants…": "Čekání na další účastníky…",
"Waiting for network": "Čekání na připojení",
"Video call name": "Jméno videohovoru",
"Video call": "Videohovor",
"Video": "Video",
"Version: {{version}}": "Verze: {{version}}",
"Username": "Uživatelské jméno",
"User menu": "Uživatelské menu",
"User ID": "ID uživatele",
"Unmute microphone": "Zapnout mikrofon",
"Turn on camera": "Zapnout kameru",
"Turn off camera": "Vypnout kameru",
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
"Thanks! We'll get right on it.": "Děkujeme! Hned se na to vrhneme.",
"Take me Home": "Domovská obrazovka",
"Submitting feedback…": "Odesílání zpětné vazby…",
"Submit feedback": "Dát feedback",
"Stop sharing screen": "Zastavit sdílení obrazovek",
"Speaker {{n}}": "Reproduktor {{n}}",
"Speaker": "Reproduktor",
"Spatial audio": "Prostorový zvuk",
"Sign out": "Odhlásit se",
"Sign in": "Přihlásit se",
"Show call inspector": "Zobrazit inspektor hovoru",
"Share screen": "Sdílet obrazovku",
"Settings": "Nastavení",
"Sending…": "Posílání…",
"Sending debug logs…": "Posílání ladícího záznamu…",
"Send debug logs": "Poslat ladící záznam",
"Select an option": "Vyberte možnost",
"Saving…": "Ukládání…",
"Save": "Uložit",
"Return to home screen": "Vrátit se na domácí obrazovku",
"Remove": "Odstranit",
"Registering…": "Registrování…",
"Register": "Registrace",
"Profile": "Profil",
"Press and hold to talk": "Stiskněte a držte pro mluvení",
"Press and hold spacebar to talk": "Stiskněte a držte mezerník pro mluvení",
"Passwords must match": "Hesla se musí shodovat",
"Password": "Heslo",
"Not now, return to home screen": "Teď ne, vrátit se na domovskou obrazovku",
"No": "Ne",
"Mute microphone": "Ztlumit mikrofon",
"More": "Více",
"Microphone permissions needed to join the call.": "Přístup k mikrofonu je nutný pro připojení se k hovoru.",
"Microphone {{n}}": "Mikrofon {{n}}",
"Microphone": "Mikrofon",
"Login to your account": "Přihlásit se ke svůmu účtu",
"Login": "Přihlášení",
"Logging in…": "Přihlašování se…",
"Local volume": "Lokální hlasitost",
"Loading…": "Načítání…",
"Loading room…": "Načítání místnosti…",
"Leave": "Opustit hovor",
"Join call now": "Připojit se k hovoru",
"Join call": "Připojit se k hovoru",
"Invite people": "Pozvat lidi",
"Invite": "Pozvat",
"Inspector": "Insepktor",
"Incompatible versions!": "Nekompatibilní verze!",
"Incompatible versions": "Nekompatibilní verze"
}

View File

@@ -2,14 +2,14 @@
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Du hast bereits ein Konto?</0><1><0>Anmelden</0> Oder <2>Als Gast betreten</2></1>", "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Du hast bereits ein Konto?</0><1><0>Anmelden</0> Oder <2>Als Gast betreten</2></1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Konto erstellen</0> Oder <2>Als Gast betreten</2>", "<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Konto erstellen</0> Oder <2>Als Gast betreten</2>",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoppla, da ist etwas schief gelaufen.</0><1>Die Übermittlung von Debug-Protokollen wird uns helfen, das Problem zu finden.</1>", "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoppla, da ist etwas schief gelaufen.</0><1>Die Übermittlung von Debug-Protokollen wird uns helfen, das Problem zu finden.</1>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Warum vergibst Du nicht abschließend ein Passwort, um Dein Konto zu erhalten?</0><1>Du kannst Deinen Namen behalten und einen Avatar für zukünftige Anrufe festlegen.</1>", "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Warum vergibst du nicht abschließend ein Passwort, um dein Konto zu erhalten?</0><1>Du kannst deinen Namen behalten und ein Profilbild für zukünftige Anrufe festlegen.</1>",
"Accept camera/microphone permissions to join the call.": "Erlaube Zugriff auf Kamera/Mikrofon um dem Anruf beizutreten.", "Accept camera/microphone permissions to join the call.": "Erlaube Zugriff auf Kamera/Mikrofon um dem Anruf beizutreten.",
"Accept microphone permissions to join the call.": "Erlaube Zugriff auf das Mikrofon um dem Anruf beizutreten.", "Accept microphone permissions to join the call.": "Erlaube Zugriff auf das Mikrofon um dem Anruf beizutreten.",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um dieses besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.",
"Audio": "Audio", "Audio": "Audio",
"Avatar": "Avatar", "Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Mit dem Klick auf \"Los geht's\", akzeptierst Du unsere <2>Geschäftsbedingungen</2>", "By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Wenn du auf Los gehts“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn Du auf \"Jetzt anrufen\" klickst, erklärst Du dich mit unserer <2>Geschäftsbedingungen</2> einverstanden", "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
"Call link copied": "Anruflink kopiert", "Call link copied": "Anruflink kopiert",
"Call type menu": "Anruftyp Menü", "Call type menu": "Anruftyp Menü",
"Camera": "Kamera", "Camera": "Kamera",
@@ -21,31 +21,30 @@
"Connection lost": "Verbindung verloren", "Connection lost": "Verbindung verloren",
"Copied!": "Kopiert!", "Copied!": "Kopiert!",
"Copy and share this call link": "Kopiere und teile diesen Anruflink", "Copy and share this call link": "Kopiere und teile diesen Anruflink",
"Copy call link and join later": "Kopiere den Anruflink und nehme später teil",
"Create account": "Konto erstellen", "Create account": "Konto erstellen",
"Debug log": "Debug-Protokoll", "Debug log": "Debug-Protokoll",
"Debug log request": "Debug-Log Anfrage", "Debug log request": "Debug-Log Anfrage",
"Description (optional)": "Beschreibung (wahlweise)", "Description (optional)": "Beschreibung (optional)",
"Details": "Details", "Details": "Details",
"Developer": "Entwickler", "Developer": "Entwickler",
"Display name": "Anzeigename", "Display name": "Anzeigename",
"Download debug logs": "Debug-Logs herunterladen", "Download debug logs": "Debug-Protokolle herunterladen",
"Entering room…": "Betrete Raum …", "Entering room…": "Betrete Raum …",
"Exit full screen": "Vollbildmodus verlassen", "Exit full screen": "Vollbildmodus verlassen",
"Freedom": "Freiraum", "Freedom": "Freiraum",
"Full screen": "Vollbild", "Full screen": "Vollbild",
"Go": "Los geht's", "Go": "Los gehts",
"Grid layout menu": "Grid-Layout-Menü", "Grid layout menu": "Grid-Layout-Menü",
"Having trouble? Help us fix it.": "Hast Du Probleme? Hilf uns, es zu beheben.", "Having trouble? Help us fix it.": "Du hast ein Problem? Hilf uns, es zu beheben.",
"Home": "Startseite", "Home": "Startseite",
"Include debug logs": "Debug-Logs hinzufügen", "Include debug logs": "Debug-Protokolle einschließen",
"Incompatible versions": "Inkompatible Versionen", "Incompatible versions": "Inkompatible Versionen",
"Incompatible versions!": "Inkompatible Versionen!", "Incompatible versions!": "Inkompatible Versionen!",
"Inspector": "Inspektor", "Inspector": "Inspektor",
"Invite": "Einladen", "Invite": "Einladen",
"Invite people": "Personen einladen", "Invite people": "Personen einladen",
"Join call": "Anruf beitreten", "Join call": "Anruf beitreten",
"Join call now": "Trete dem Anruf bei", "Join call now": "Anruf beitreten",
"Join existing call?": "An bestehendem Anruf teilnehmen?", "Join existing call?": "An bestehendem Anruf teilnehmen?",
"Leave": "Verlassen", "Leave": "Verlassen",
"Loading room…": "Lade Raum …", "Loading room…": "Lade Raum …",
@@ -53,9 +52,9 @@
"Local volume": "Lokale Lautstärke", "Local volume": "Lokale Lautstärke",
"Logging in…": "Anmelden …", "Logging in…": "Anmelden …",
"Login": "Anmelden", "Login": "Anmelden",
"Login to your account": "Anmeldung bei Deinem Konto", "Login to your account": "Melde dich mit deinem Konto an",
"Microphone": "Mikrofon", "Microphone": "Mikrofon",
"Microphone permissions needed to join the call.": "Mikrofon Berechtigung ist erforderlich, um dem Anruf beizutreten.", "Microphone permissions needed to join the call.": "Mikrofon-Berechtigung ist erforderlich, um dem Anruf beizutreten.",
"Microphone {{n}}": "Mikrofon {{n}}", "Microphone {{n}}": "Mikrofon {{n}}",
"More": "Mehr", "More": "Mehr",
"More menu": "Weiteres Menü", "More menu": "Weiteres Menü",
@@ -66,7 +65,7 @@
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Andere Benutzer versuchen, diesem Aufruf von einer inkompatiblen Softwareversion aus beizutreten. Diese Benutzer sollten ihre Web-Browser Seite neu laden:<1>{userLis}</1>", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Andere Benutzer versuchen, diesem Aufruf von einer inkompatiblen Softwareversion aus beizutreten. Diese Benutzer sollten ihre Web-Browser Seite neu laden:<1>{userLis}</1>",
"Password": "Passwort", "Password": "Passwort",
"Passwords must match": "Passwörter müssen übereinstimmen", "Passwords must match": "Passwörter müssen übereinstimmen",
"Press and hold spacebar to talk": "Zum Sprechen die Leertaste gedrückt halten", "Press and hold spacebar to talk": "Halte zum Sprechen die Leertaste gedrückt",
"Press and hold spacebar to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen die Leertaste gedrückt halten", "Press and hold spacebar to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen die Leertaste gedrückt halten",
"Press and hold to talk": "Zum Sprechen gedrückt halten", "Press and hold to talk": "Zum Sprechen gedrückt halten",
"Press and hold to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen gedrückt halten", "Press and hold to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen gedrückt halten",
@@ -94,19 +93,19 @@
"Speaker {{n}}": "Wiedergabegerät {{n}}", "Speaker {{n}}": "Wiedergabegerät {{n}}",
"Spotlight": "Rampenlicht", "Spotlight": "Rampenlicht",
"Stop sharing screen": "Beenden der Bildschirmfreigabe", "Stop sharing screen": "Beenden der Bildschirmfreigabe",
"Submit feedback": "Feedback senden", "Submit feedback": "Rückmeldung geben",
"Submitting feedback…": "Feedback senden …", "Submitting feedback…": "Sende Rückmeldung …",
"Take me Home": "Zurück zur Startseite", "Take me Home": "Zurück zur Startseite",
"Talk over speaker": "Aktiven Sprecher verdrängen und sprechen", "Talk over speaker": "Aktiven Sprecher verdrängen und sprechen",
"Talking…": "Sprechen …", "Talking…": "Sprechen …",
"Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.", "Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.",
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?", "This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Diese Website ist durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> sowie die <6> Nutzungsbedingungen </6> von Google.<9></9>Indem Du auf \"Registrieren\" klickst, stimmst Du unseren <12>Geschäftsbedingungen</12> zu", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Diese Website wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6> von Google.<9></9>Indem Du auf Registrieren klickst, stimmst du unseren <12>Geschäftsbedingungen</12> zu",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Dadurch wird die Audiowiedergabe eines Sprechers so wiedergegeben, als käme er von der Stelle, an der das zugehörige Videobild auf dem Bildschirm positioniert ist (Experimentelle Funktion: Dies kann die Stabilität der Audiowiedergabe beeinträchtigen).", "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Dies wird die Audiowiedergabe eines Sprechers wirken lassen, als käme sie von der Stelle des zugehörigen Videobildes. (Experimentelle Funktion: Dies könnte die Stabilität der Audiowiedergabe beeinträchtigen.)",
"Turn off camera": "Kamera ausschalten", "Turn off camera": "Kamera ausschalten",
"Turn on camera": "Kamera einschalten", "Turn on camera": "Kamera einschalten",
"Unmute microphone": "Mikrofon aktivieren", "Unmute microphone": "Mikrofon aktivieren",
"User ID": "Benutzer ID", "User ID": "Benutzer-ID",
"User menu": "Benutzermenü", "User menu": "Benutzermenü",
"Username": "Benutzername", "Username": "Benutzername",
"Version: {{version}}": "Version: {{version}}", "Version: {{version}}": "Version: {{version}}",
@@ -115,19 +114,21 @@
"Video call name": "Name des Videoanrufs", "Video call name": "Name des Videoanrufs",
"Waiting for network": "Warte auf Netzwerk", "Waiting for network": "Warte auf Netzwerk",
"Waiting for other participants…": "Warte auf weitere Teilnehmer …", "Waiting for other participants…": "Warte auf weitere Teilnehmer …",
"Walkie-talkie call": "Walkie-talkie Anruf", "Walkie-talkie call": "Walkie-Talkie-Anruf",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Web-Browser nicht unterstützt oder ist blockiert.", "WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Browser nicht unterstützt oder ist blockiert.",
"Yes, join call": "Ja, Anruf beitreten", "Yes, join call": "Ja, Anruf beitreten",
"You can't talk at the same time": "Du kannst nicht gleichzeitig sprechen", "You can't talk at the same time": "Du kannst nicht gleichzeitig sprechen",
"Your recent calls": "Deine lezten Anrufe", "Your recent calls": "Deine letzten Anrufe",
"{{count}} people connected|one": "{{count}} Teilnehmer verbunden", "{{count}} people connected|one": "{{count}} Person verbunden",
"{{count}} people connected|other": "{{count}} Teilnehmer verbunden", "{{count}} people connected|other": "{{count}} Personen verbunden",
"{{displayName}}, your call is now ended": "{{displayName}}, Dein Anruf wurde beendet", "{{displayName}}, your call is now ended": "{{displayName}}, dein Anruf wurde beendet",
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} präsentiert", "{{name}} is presenting": "{{name}} präsentiert",
"{{name}} is talking…": "{{name}} spricht …", "{{name}} is talking…": "{{name}} spricht …",
"{{roomName}} - Walkie-talkie call": "{{roomName}} Walkie-Talkie-Anruf", "{{roomName}} - Walkie-talkie call": "{{roomName}} Walkie-Talkie-Anruf",
"Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.", "Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.",
"Walkie-talkie call name": "Walkie-talkie Anruf Name", "Walkie-talkie call name": "Name des Walkie-Talkie-Anrufs",
"Sending debug logs…": "Sende Debug-Logs …" "Sending debug logs…": "Sende Debug-Protokolle …",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Anruf beitreten</0><1>Oder</1><2>Anruflink kopieren und später beitreten</2>",
"{{name}} (Connecting...)": "{{name}} (verbindet sich …)"
} }

View File

@@ -2,12 +2,14 @@
"{{count}} people connected|one": "{{count}} person connected", "{{count}} people connected|one": "{{count}} person connected",
"{{count}} people connected|other": "{{count}} people connected", "{{count}} people connected|other": "{{count}} people connected",
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended", "{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
"{{name}} (Connecting...)": "{{name}} (Connecting...)",
"{{name}} is presenting": "{{name}} is presenting", "{{name}} is presenting": "{{name}} is presenting",
"{{name}} is talking…": "{{name}} is talking…", "{{name}} is talking…": "{{name}} is talking…",
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call", "{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>", "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>", "<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>", "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>", "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
"Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.", "Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.",
@@ -28,7 +30,6 @@
"Connection lost": "Connection lost", "Connection lost": "Connection lost",
"Copied!": "Copied!", "Copied!": "Copied!",
"Copy and share this call link": "Copy and share this call link", "Copy and share this call link": "Copy and share this call link",
"Copy call link and join later": "Copy call link and join later",
"Create account": "Create account", "Create account": "Create account",
"Debug log": "Debug log", "Debug log": "Debug log",
"Debug log request": "Debug log request", "Debug log request": "Debug log request",

134
public/locales/es/app.json Normal file
View File

@@ -0,0 +1,134 @@
{
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>¿Por qué no mantienes tu cuenta estableciendo una contraseña?</0><1>Podrás mantener tu nombre y establecer un avatar para usarlo en futuras llamadas</1>",
"Press and hold to talk over {{name}}": "Mantén pulsado para hablar por encima de {{name}}",
"Your recent calls": "Tus llamadas recientes",
"WebRTC is not supported or is being blocked in this browser.": "Tu navegador no soporta o está bloqueando WebRTC.",
"This call already exists, would you like to join?": "Esta llamada ya existe, ¿te gustaría unirte?",
"Register": "Registrarse",
"Not registered yet? <2>Create an account</2>": "¿No estás registrado todavía? <2>Crear una cuenta</2>",
"Login to your account": "Iniciar sesión en tu cuenta",
"Camera/microphone permissions needed to join the call.": "Se necesitan los permisos de cámara/micrófono para unirse a la llamada.",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptarás nuestros <2>Términos y condiciones</2>",
"Accept microphone permissions to join the call.": "Acepta el permiso del micrófono para unirte a la llamada.",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones</2>",
"You can't talk at the same time": "No podéis hablar a la vez",
"Yes, join call": "Si, unirse a la llamada",
"Walkie-talkie call name": "Nombre de la llamada Walkie-talkie",
"Walkie-talkie call": "Llamada Walkie-talkie",
"Waiting for other participants…": "Esperando a los otros participantes…",
"Waiting for network": "Esperando a la red",
"Video call name": "Nombre de la videollamada",
"Video call": "Videollamada",
"Video": "Video",
"Version: {{version}}": "Versión: {{version}}",
"Username": "Nombre de usuario",
"User menu": "Menú de usuario",
"User ID": "ID de usuario",
"Unmute microphone": "Desilenciar el micrófono",
"Turn on camera": "Encender la cámara",
"Turn off camera": "Apagar la cámara",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Esto hará que el audio de la persona que hable parezca que viene de dondé esté posicionado en la pantalla. (Función experimental: esto puede afectar a la estabilidad del audio.)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Este sitio está protegido por ReCAPTCHA y se aplica <2>la Política de Privacidad</2> y <6>los Términos de Servicio</6> de Google.<9></9>Al hacer clic en \"Registrar\" aceptarás nuestros <12>Términos y condiciones</12>",
"Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.",
"Talking…": "Hablando…",
"Talk over speaker": "Hablar por encima",
"Take me Home": "Volver al inicio",
"Submitting feedback…": "Enviando comentarios…",
"Submit feedback": "Enviar comentarios",
"Stop sharing screen": "Dejar de compartir pantalla",
"Spotlight": "Foco",
"Speaker {{n}}": "Altavoz {{n}}",
"Speaker": "Altavoz",
"Spatial audio": "Audio espacial",
"Sign out": "Cerrar sesión",
"Sign in": "Iniciar sesión",
"Show call inspector": "Mostrar inspector de llamada",
"Share screen": "Compartir pantalla",
"Settings": "Ajustes",
"Sending…": "Enviando…",
"Sending debug logs…": "Enviando registros de depuración…",
"Send debug logs": "Enviar registros de depuración",
"Select an option": "Selecciona una opción",
"Saving…": "Guardando…",
"Save": "Guardar",
"Return to home screen": "Volver a la pantalla de inicio",
"Remove": "Eliminar",
"Release to stop": "Suelta para parar",
"Release spacebar key to stop": "Suelta la barra espaciadora para parar",
"Registering…": "Registrando…",
"Recaptcha not loaded": "No se ha cargado el Recaptcha",
"Recaptcha dismissed": "Recaptcha cancelado",
"Profile": "Perfil",
"Press and hold to talk": "Mantén pulsado para hablar",
"Press and hold spacebar to talk over {{name}}": "Mantén pulsada la barra espaciadora para hablar por encima de {{name}}",
"Press and hold spacebar to talk": "Mantén pulsada la barra espaciadora para hablar",
"Passwords must match": "Las contraseñas deben coincidir",
"Password": "Contraseña",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Otros usuarios están intentando unirse a la llamada con versiones incompatibles. Estos usuarios deberán asegurarse de que han refrescado sus navegadores:<1>{userLis}</1>",
"Not now, return to home screen": "Ahora no, volver a la pantalla de inicio",
"No": "No",
"Mute microphone": "Silenciar micrófono",
"More menu": "Menú Más",
"More": "Más",
"Microphone permissions needed to join the call.": "Se necesitan permisos del micrófono para unirse a la llamada.",
"Microphone {{n}}": "Micrófono {{n}}",
"Microphone": "Micrófono",
"Login": "Iniciar sesión",
"Logging in…": "Iniciando sesión…",
"Local volume": "Volumen local",
"Loading…": "Cargando…",
"Loading room…": "Cargando sala…",
"Leave": "Abandonar",
"Join existing call?": "¿Unirse a llamada existente?",
"Join call now": "Unirse a la llamada ahora",
"Join call": "Unirse a la llamada",
"Invite people": "Invitar a gente",
"Invite": "Invitar",
"Inspector": "Inspector",
"Incompatible versions!": "¡Versiones incompatibles!",
"Incompatible versions": "Versiones incompatibles",
"Include debug logs": "Incluir registros de depuración",
"Home": "Inicio",
"Having trouble? Help us fix it.": "¿Tienes problemas? Ayúdanos a resolverlos.",
"Grid layout menu": "Menú de distribución de cuadrícula",
"Go": "Empezar",
"Full screen": "Pantalla completa",
"Freedom": "Libre",
"Fetching group call timed out.": "Se ha agotado el tiempo de espera para obtener la llamada grupal.",
"Exit full screen": "Salir de pantalla completa",
"Entering room…": "Entrando a la sala…",
"Download debug logs": "Descargar registros de depuración",
"Display name": "Nombre a mostrar",
"Developer": "Desarrollador",
"Details": "Detalles",
"Description (optional)": "Descripción (opcional)",
"Debug log request": "Petición de registros de depuración",
"Debug log": "Registro de depuración",
"Create account": "Crear cuenta",
"Copy and share this call link": "Copiar y compartir el enlace de la llamada",
"Copied!": "¡Copiado!",
"Connection lost": "Conexión perdida",
"Confirm password": "Confirmar contraseña",
"Close": "Cerrar",
"Change layout": "Cambiar distribución",
"Camera {{n}}": "Cámara {{n}}",
"Camera": "Cámara",
"Call type menu": "Menú de tipo de llamada",
"Call link copied": "Enlace de la llamada copiado",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Otro usuario en esta llamada está teniendo problemas. Para diagnosticar estos problemas nos gustaría recopilar un registro de depuración.",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"Audio": "Audio",
"Avatar": "Avatar",
"Accept camera/microphone permissions to join the call.": "Acepta los permisos de cámara/micrófono para unirte a la llamada.",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, algo ha salido mal.</0><1>Enviar los registros de depuración nos ayudará a localizar el problema.</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Crear una cuenta</0> o <2>Acceder como invitado</2>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Unirse ahora</0><1>Or</1><2>Copiar el enlace y unirse más tarde</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>¿Ya tienes una cuenta?</0><1><0>Iniciar sesión</0> o <2>Acceder como invitado</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Llamada de Walkie-talkie",
"{{name}} is talking…": "{{name}} está hablando…",
"{{name}} is presenting": "{{name}} está presentando",
"{{name}} (Connecting...)": "{{name}} (Conectando...)",
"{{displayName}}, your call is now ended": "{{displayName}}, tu llamada ha finalizado",
"{{count}} people connected|other": "{{count}} personas conectadas",
"{{count}} people connected|one": "{{count}} persona conectada"
}

134
public/locales/et/app.json Normal file
View File

@@ -0,0 +1,134 @@
{
"Accept camera/microphone permissions to join the call.": "Kõnega anna õigused kaamera/mikrofoni kasutamiseks.",
"Accept microphone permissions to join the call.": "Kõnega liitumiseks anna õigused mikrofoni kasutamiseks.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Kas hoopis tahad salasõna seadistada ja sellega oma kasutajakonto alles jätta?</0><1>Siis saad säilitada oma nime ja määrata tunnuspildi, mida saad kasutada tulevastes kõnedes</1>",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, midagi läks valesti.</0><1>Logide saatmine meile aitab meil probleemi lahendada.</1>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Liitu kõnega kohe</0><1> Või</1><2>Kopeeri kõne link ja liitu hiljem</2>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Loo konto</0> Või <2>Sisene külalisena</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>On sul juba konto?</0><1><0>Logi sisse</0> Või <2>Logi sisse külalisena</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - walkie-talkie-kõne",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{nimi}} räägib…",
"{{name}} is presenting": "{{nimi}} esitab",
"{{name}} (Connecting...)": "{{nimi}} (ühendamisel...)",
"{{displayName}}, your call is now ended": "{{displayName}}, sinu kõne on nüüd lõppenud",
"{{count}} people connected|other": "{{count}} osalejat liitunud",
"{{count}} people connected|one": "{{count}} osaleja liitunud",
"Invite people": "Kutsu inimesi",
"Invite": "Kutsu",
"Inspector": "Inspektor",
"Incompatible versions!": "Ühildumatud versioonid!",
"Incompatible versions": "Ühildumatud versioonid",
"Include debug logs": "Lisa veatuvastuslogid",
"Home": "Avavaatesse",
"Having trouble? Help us fix it.": "Kas on probleeme? Aita meil asja parandada.",
"Grid layout menu": "Ruudustikvaate menüü",
"Go": "Jätka",
"Full screen": "Täisekraan",
"Freedom": "Vaba",
"Fetching group call timed out.": "Grupikõne kättesaamine aegus.",
"Exit full screen": "Välju täisekraanivaatest",
"Entering room…": "Ruumi sisenemine…",
"Download debug logs": "Lae alla veatuvastuslogid",
"Display name": "Kuvatav nimi",
"Developer": "Arendaja",
"Details": "Täpsemalt",
"Description (optional)": "Kirjeldus (valikuline)",
"Debug log request": "Veaotsingulogi päring",
"Debug log": "Veaotsingulogi",
"Create account": "Loo konto",
"Copy and share this call link": "Kopeeri ja jaga selle kõne linki",
"Copied!": "Kopeeritud!",
"Connection lost": "Ühendus on katkenud",
"Confirm password": "Kinnita salasõna",
"Close": "Sulge",
"Change layout": "Muuda paigutust",
"Camera/microphone permissions needed to join the call.": "Kõnega liitumiseks vajalikud kaamera/mikrofoni kasutamise load.",
"Camera {{n}}": "Kaamera {{n}}",
"Camera": "Kaamera",
"Call type menu": "Kõnetüübi valik",
"Call link copied": "Kõne link on kopeeritud",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Liitu kõnega“nõustud sa meie <2>kasutustingimustega</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega</2>",
"Avatar": "Tunnuspilt",
"Audio": "Heli",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ühel teisel selles kõnes osalejal on lahenduse kasutamisel tekkinud probleem ning selle põhjuse leidmiseks me sooviksime koguda silumislogisid.",
"Press and hold spacebar to talk": "Rääkimiseks vajuta ja hoia all tühikuklahvi",
"Passwords must match": "Salasõnad ei klapi",
"Password": "Salasõna",
"Not registered yet? <2>Create an account</2>": "Pole veel registreerunud? <2>Loo kasutajakonto</2>",
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
"No": "Ei",
"Mute microphone": "Summuta mikrofon",
"Your recent calls": "Hiljutised kõned",
"You can't talk at the same time": "Üheaegselt ei saa rääkida",
"More menu": "Rohkem valikuid",
"More": "Rohkem",
"Microphone permissions needed to join the call.": "Kõnega liitumiseks on vaja lubada mikrofoni kasutamine.",
"Microphone {{n}}": "Mikrofon {{n}}",
"Microphone": "Mikrofon",
"Login to your account": "Logi oma kontosse sisse",
"Login": "Sisselogimine",
"Logging in…": "Sisselogimine …",
"Local volume": "Kohalik helitugevus",
"Loading…": "Laadimine …",
"Loading room…": "Ruumi laadimine …",
"Leave": "Lahku",
"Join existing call?": "Liitu juba käimasoleva kõnega?",
"Join call now": "Kõnega liitumine",
"Join call": "Kõnega liitumine",
"User ID": "Kasutajatunnus",
"Turn on camera": "Lülita kaamera sisse",
"Turn off camera": "Lülita kaamera välja",
"Submitting feedback…": "Tagasiside saatmine…",
"Take me Home": "Mine avalehele",
"Submit feedback": "Jaga tagasisidet",
"Stop sharing screen": "Lõpeta ekraani jagamine",
"Spotlight": "Rambivalgus",
"Speaker {{n}}": "Kõlar {{n}}",
"Speaker": "Kõlar",
"Spatial audio": "Ruumiline heli",
"Sign out": "Logi välja",
"Sign in": "Logi sisse",
"Show call inspector": "Näita kõneteavet",
"Share screen": "Jaga ekraani",
"Settings": "Seadistused",
"Sending…": "Saatmine…",
"Sending debug logs…": "Veaotsingulogide saatmine…",
"Send debug logs": "Saada veaotsingulogid",
"Select an option": "Vali oma eelistus",
"Saving…": "Salvestamine…",
"Save": "Salvesta",
"Return to home screen": "Tagasi avalehele",
"Remove": "Eemalda",
"Release to stop": "Peatamiseks vabasta klahv",
"Release spacebar key to stop": "Peatamiseks vabasta tühikuklahv",
"Registering…": "Registreerimine…",
"Register": "Registreeru",
"Recaptcha not loaded": "Robotilõks pole laetud",
"Recaptcha dismissed": "Robotilõks on vahele jäetud",
"Profile": "Profiil",
"Press and hold to talk over {{name}}": "{{name}} ülerääkimiseks vajuta ja hoia all",
"Press and hold to talk": "Rääkimiseks vajuta ja hoia all",
"Press and hold spacebar to talk over {{name}}": "{{name}} ülerääkimiseks vajuta ja hoia all tühikuklahvi",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Teised kasutajad üritavad selle kõnega liituda ühildumatuid versioone kasutades. Need kasutajad peaksid oma brauseris lehe uuestilaadimise tegema:<1>{userLis}</1>",
"Waiting for other participants…": "Ootame teiste osalejate lisandumist…",
"Waiting for network": "Ootame võrguühendust",
"Video call name": "Videokõne nimi",
"Video call": "Videokõne",
"Video": "Video",
"Version: {{version}}": "Versioon: {{version}}",
"Username": "Kasutajanimi",
"This call already exists, would you like to join?": "See kõne on juba olemas, kas soovid liituda?",
"Talking…": "Jutt käib…",
"Talk over speaker": "Räägi teisest kõnelejast üle",
"Thanks! We'll get right on it.": "Tänud! Tegeleme sellega esimesel võimalusel.",
"Unmute microphone": "Aktiveeri mikrofon",
"User menu": "Kasutajamenüü",
"Yes, join call": "Jah, liitu kõnega",
"Walkie-talkie call": "Walkie-talkie stiilis kõne",
"Walkie-talkie call name": "Walkie-talkie stiilis kõne nimi",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Muudab kõneleja heli nii, nagu tuleks see sealt, kus on tema pilt ekraanil. (See on katseline funktsionaalsus ja võib mõjutada heli stabiilsust.)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika</2> ja <6>teenusetingimused</6>.<9></9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega</12>"
}

132
public/locales/fa/app.json Normal file
View File

@@ -0,0 +1,132 @@
{
"Your recent calls": "تماس‌های اخیر شما",
"Video call": "تماس تصویری",
"Video": "ویدیو",
"Username": "نام کاربری",
"User ID": "آی دی کاربر",
"Turn on camera": "روشن کردن دوربین",
"Turn off camera": "خاموش کردن دوربین",
"Take me Home": "مرا به خانه ببر",
"Speaker": "بلندگو",
"Sign out": "خروج",
"Sign in": "ورود",
"Settings": "تنظیمات",
"Save": "ذخیره",
"Profile": "پروفایل",
"Password": "رمز عبور",
"No": "خیر",
"Mute microphone": "بی‌صدا کردن میکروفون",
"More": "بیشتر",
"Microphone": "میکروفون",
"Login to your account": "به حساب کاربری خود وارد شوید",
"Login": "ورود",
"Loading…": "بارگزاری…",
"Loading room…": "بارگزاری اتاق…",
"Leave": "خروج",
"Join existing call?": "پیوست به تماس؟",
"Join call now": "الان به تماس بپیوند",
"Join call": "پیوستن به تماس",
"Invite people": "دعوت از افراد",
"Invite": "دعوت",
"Home": "خانه",
"Go": "رفتن",
"Full screen": "تمام صحفه",
"Freedom": "آزادی",
"Exit full screen": "خروج از حالت تمام صفحه",
"Entering room…": "درحال وارد شدن به اتاق…",
"Download debug logs": "دانلود لاگ عیب‌یابی",
"Display name": "نام نمایشی",
"Developer": "توسعه دهنده",
"Details": "جزئیات",
"Description (optional)": "توضیحات (اختیاری)",
"Debug log request": "درخواست لاگ عیب‌یابی",
"Debug log": "لاگ عیب‌یابی",
"Create account": "ساخت حساب کاربری",
"Copy and share this call link": "لینک تماس را کپی کنید و به اشتراک بگذارید",
"Copied!": "کپی شد!",
"Connection lost": "ارتباط قطع شد",
"Confirm password": "تایید رمزعبور",
"Close": "بستن",
"Change layout": "تغییر طرح",
"Camera/microphone permissions needed to join the call.": "برای پیوستن به تماس، دسترسی به دوربین/ میکروفون نیاز است.",
"Camera {{n}}": "دوربین {{n}}",
"Camera": "دوربین",
"Call type menu": "منوی نوع تماس",
"Call link copied": "لینک تماس کپی شد",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "با کلیک بر روی پیوستن به تماس، شما با <2>شرایط و قوانین استفاده</2> موافقت می‌کنید",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "با کلیک بر روی برو، شما با <2>شرایط و قوانین استفاده</2> موافقت می‌کنید",
"Avatar": "آواتار",
"Audio": "صدا",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیب‌یابی را جمع‌آوری کنیم.",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"Accept microphone permissions to join the call.": "پذیرفتن دسترسی به میکروفون برای پیوستن به تماس.",
"Accept camera/microphone permissions to join the call.": "پذیرفتن دسترسی دوربین/ میکروفون برای پیوستن به تماس.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>چرا یک رمز عبور برای حساب کاربری خود تنظیم نمی‌کنید؟</0><1>شما می‌توانید نام خود را حفظ کنید و یک آواتار برای تماس‌های آینده بسازید</1>",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>اوه، مشکلی پیش آمده.</0><1>ثبت کردن لاگ رفع اشکال به پیدا کردن مشکل توسط ما کمک میکند</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>ساخت حساب کاربری</0> Or <2>دسترسی به عنوان میهمان</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>از قبل حساب کاربری دارید؟</0><1><0>ورود</0> Or <2>به عنوان یک میهمان وارد شوید</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - تماس واکی-تاکی",
"{{name}} is talking…": "{{name}} در حال صحبت است…",
"{{name}} is presenting": "{{name}} حاضر است",
"{{displayName}}, your call is now ended": "{{displayName}} تماس شما پایان یافت",
"{{count}} people connected|other": "{{count}} نفر متصل هستند",
"{{count}} people connected|one": "{{count}} فرد متصل هستند",
"Local volume": "حجم داخلی",
"Inspector": "بازرس",
"Incompatible versions!": "نسخه‌های ناسازگار!",
"Incompatible versions": "نسخه‌های ناسازگار",
"Spotlight": "نور افکن",
"Speaker {{n}}": "بلندگو {{n}}",
"Show call inspector": "نمایش بازرس تماس",
"Share screen": "اشتراک گذاری صفحه نمایش",
"Sending…": "در حال ارسال…",
"Sending debug logs…": "در حال ارسال باگ‌های عیب‌یابی…",
"Send debug logs": "ارسال لاگ‌های عیب‌یابی",
"Select an option": "یک گزینه را انتخاب کنید",
"Saving…": "در حال ذخیره…",
"Return to home screen": "برگشت به صفحه اصلی",
"Remove": "حذف",
"Release to stop": "برای توقف رها کنید",
"Release spacebar key to stop": "اسپیس بار را برای توقف رها کنید",
"Registering…": "ثبت‌نام…",
"Register": "ثبت‌نام",
"Recaptcha not loaded": "کپچا بارگیری نشد",
"Recaptcha dismissed": "بازکپچا رد شد",
"Press and hold to talk over {{name}}": "برای صحبت فشار دهید و نگه‌دارید {{name}}",
"Press and hold to talk": "برای صحبت فشار دهید و نگه‌دارید",
"Press and hold spacebar to talk over {{name}}": "برای صحبت کردن دکمه اسپیس بار را فشار دهید و نگه دارید {{name}}",
"Press and hold spacebar to talk": "برای صحبت کردن کلید فاصله را فشار داده و نگه دارید",
"Passwords must match": "رمز عبور باید همخوانی داشته باشد",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "کاربران دیگر تلاش می‌کنند با ورژن‌های ناسازگار به مکالمه بپیوندند. این کاربران باید از بروزرسانی مرورگرشان اطمینان داشته باشند:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "هنوز ثبت‌نام نکرده‌اید؟ <2>ساخت حساب کاربری</2>",
"Not now, return to home screen": "الان نه، به صفحه اصلی برگردید",
"More menu": "تنظیمات بیشتر",
"Microphone permissions needed to join the call.": "برای پیوستن به مکالمه دسترسی به میکروفون نیاز است.",
"Microphone {{n}}": "میکروفون {{n}}",
"Logging in…": "ورود…",
"Include debug logs": "شامل لاگ‌های عیب‌یابی",
"Having trouble? Help us fix it.": "با مشکلی رو به رو شدید؟ به ما کمک کنید رفعش کنیم.",
"Grid layout menu": "منوی طرح‌بندی شبکه‌ای",
"Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.",
"You can't talk at the same time": "شما نمی توانید همزمان تماس بگیرید",
"Yes, join call": "بله، به تماس بپیوندید",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC (ارتباطات رسانه‌ای بلادرنگ مانند انتقال صدا، ویدئو و داده‌) در این مرورگر پشتیبانی نمی‌شود یا در حال مسدود شدن است.",
"Walkie-talkie call name": "نامِ تماسِ واکی-تاکی",
"Walkie-talkie call": "تماسِ واکی-تاکی",
"Waiting for other participants…": "در انتظار برای دیگر شرکت‌کنندگان…",
"Waiting for network": "در انتظار شبکه",
"Video call name": "نامِ تماسِ تصویری",
"Version: {{version}}": "نسخه: {{نسخه}}",
"User menu": "فهرست کاربر",
"Unmute microphone": "میکروفون را باصدا کنید",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "این کار باعث می‌شود صدای بلندگو از جایی که کاشی‌های آن روی صفحه قرار گرفته است به نظر برسد. (ویژگی آزمایشی: این ممکن است بر پایداری صدا تأثیر بگذارد.)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "این سایت توسط ReCAPTCHA محافظت می شود و <2>خط مشی رازداری</2> و <6>شرایط خدمات</6> Google اعمال می شود.<9></9>با کلیک کردن بر روی \"ثبت نام\"، شما با <12 >شرایط و ضوابط </12> ما موافقت می کنید",
"This call already exists, would you like to join?": "این تماس از قبل وجود دارد، می‌خواهید بپیوندید؟",
"Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.",
"Talking…": "در حال صحبت کردن…",
"Talk over speaker": "روی بلندگو صحبت کنید",
"Submitting feedback…": "در حال ارسال بازخورد…",
"Submit feedback": "بازخورد ارائه دهید",
"Stop sharing screen": "توقف اشتراک‌گذاری صفحه نمایش",
"Spatial audio": "صدای فضایی"
}

View File

@@ -20,7 +20,6 @@
"Connection lost": "Connexion interrompue", "Connection lost": "Connexion interrompue",
"Copied!": "Copié !", "Copied!": "Copié !",
"Copy and share this call link": "Copier et partager le lien de cet appel", "Copy and share this call link": "Copier et partager le lien de cet appel",
"Copy call link and join later": "Copier le lien de cet appel et rejoindre plus tard",
"Create account": "Créer un compte", "Create account": "Créer un compte",
"Debug log": "Journal de débogage", "Debug log": "Journal de débogage",
"Debug log request": "Demande dun journal de débogage", "Debug log request": "Demande dun journal de débogage",
@@ -129,5 +128,7 @@
"Speaker": "Intervenant", "Speaker": "Intervenant",
"Invite": "Inviter", "Invite": "Inviter",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>", "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
"Sending debug logs…": "Envoi des journaux de débogage…" "Sending debug logs…": "Envoi des journaux de débogage…",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Rejoindre lappel maintenant</0><1>Ou</1><2>Copier le lien de lappel et rejoindre plus tard</2>",
"{{name}} (Connecting...)": "{{name}} (Connexion…)"
} }

View File

@@ -21,7 +21,6 @@
"Connection lost": "Koneksi hilang", "Connection lost": "Koneksi hilang",
"Copied!": "Disalin!", "Copied!": "Disalin!",
"Copy and share this call link": "Salin dan bagikan tautan panggilan ini", "Copy and share this call link": "Salin dan bagikan tautan panggilan ini",
"Copy call link and join later": "Salin tautan panggilan dan bergabung nanti",
"Create account": "Buat akun", "Create account": "Buat akun",
"Debug log": "Catatan pengawakutuan", "Debug log": "Catatan pengawakutuan",
"Debug log request": "Permintaan catatan pengawakutuan", "Debug log request": "Permintaan catatan pengawakutuan",
@@ -128,5 +127,8 @@
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} sedang mempresentasi", "{{name}} is presenting": "{{name}} sedang mempresentasi",
"{{name}} is talking…": "{{name}} sedang berbicara…", "{{name}} is talking…": "{{name}} sedang berbicara…",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Panggilan protofon" "{{roomName}} - Walkie-talkie call": "{{roomName}} - Panggilan protofon",
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
"{{name}} (Connecting...)": "{{name}} (Menghubungkan...)"
} }

130
public/locales/pl/app.json Normal file
View File

@@ -0,0 +1,130 @@
{
"More menu": "Menu \"więcej\"",
"Login": "Zaloguj się",
"Go": "Kontynuuj",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Warunki</2>",
"{{count}} people connected|other": "{{count}} ludzi połączono",
"Your recent calls": "Twoje ostatnie połączenia",
"You can't talk at the same time": "Nie możesz mówić w tym samym czasie",
"Yes, join call": "Tak, dołącz do połączenia",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC jest niewspierane lub zablokowane w tej przeglądarce.",
"Walkie-talkie call name": "Nazwa połączenia walkie-talkie",
"Walkie-talkie call": "Połączenie walkie-talkie",
"Waiting for other participants…": "Oczekiwanie na pozostałych uczestników…",
"Waiting for network": "Oczekiwanie na sieć",
"Video call name": "Nazwa połączenia wideo",
"Video call": "Połączenie wideo",
"Video": "Wideo",
"Version: {{version}}": "Wersja: {{version}}",
"Username": "Nazwa użytkownika",
"User menu": "Menu użytkownika",
"User ID": "ID użytkownika",
"Unmute microphone": "Wyłącz wyciszenie mikrofonu",
"Turn on camera": "Włącz kamerę",
"Turn off camera": "Wyłącz kamerę",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Sprawi to, że dźwięk mówcy będzie zdawał się dochodzić z jego miejsca na ekranie. (Funkcja eksperymentalna: może mieć wpływ na stabilność dźwięku.)",
"This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
"Thanks! We'll get right on it.": "Dziękujemy! Zaraz się tym zajmiemy.",
"Talking…": "Mówienie…",
"Take me Home": "Zabierz mnie do ekranu startowego",
"Submitting feedback…": "Przesyłanie opinii…",
"Submit feedback": "Prześlij opinię",
"Stop sharing screen": "Zatrzymaj udostępnianie ekranu",
"Spotlight": "Centrum uwagi",
"Speaker {{n}}": "Głośnik {{n}}",
"Speaker": "Głośnik",
"Spatial audio": "Dźwięk przestrzenny",
"Sign out": "Wyloguj się",
"Sign in": "Zaloguj się",
"Show call inspector": "Pokaż inspektora połączenia",
"Share screen": "Udostępnij ekran",
"Settings": "Ustawienia",
"Sending…": "Wysyłanie…",
"Sending debug logs…": "Wysyłanie dzienników debugowania…",
"Send debug logs": "Wyślij dzienniki debugowania",
"Select an option": "Wybierz opcję",
"Saving…": "Zapisywanie…",
"Save": "Zapisz",
"Return to home screen": "Powróć do ekranu domowego",
"Remove": "Usuń",
"Release to stop": "Puść przycisk, aby przestać",
"Release spacebar key to stop": "Puść spację, aby przestać",
"Registering…": "Rejestrowanie…",
"Register": "Zarejestruj",
"Recaptcha not loaded": "Recaptcha nie została załadowana",
"Recaptcha dismissed": "Recaptcha odrzucona",
"Profile": "Profil",
"Press and hold to talk over {{name}}": "Przytrzymaj, aby mówić wraz z {{name}}",
"Press and hold to talk": "Przytrzymaj, aby mówić",
"Press and hold spacebar to talk over {{name}}": "Przytrzymaj spację, aby mówić wraz z {{name}}",
"Press and hold spacebar to talk": "Przytrzymaj spację, aby mówić",
"Passwords must match": "Hasła muszą być identyczne",
"Password": "Hasło",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Inni użytkownicy próbują dołączyć do tego połączenia przy użyciu niekompatybilnych wersji. Powinni oni upewnić się, że odświeżyli stronę w swoich przeglądarkach:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "Nie masz konta? <2>Utwórz je</2>",
"Not now, return to home screen": "Nie teraz, powróć do ekranu domowego",
"No": "Nie",
"Mute microphone": "Wycisz mikrofon",
"More": "Więcej",
"Microphone permissions needed to join the call.": "Aby dołączyć do połączenia, potrzebne są uprawnienia do mikrofonu.",
"Microphone {{n}}": "Mikrofon {{n}}",
"Microphone": "Mikrofon",
"Login to your account": "Zaloguj się do swojego konta",
"Logging in…": "Logowanie…",
"Local volume": "Lokalna głośność",
"Loading…": "Ładowanie…",
"Loading room…": "Ładowanie pokoju…",
"Leave": "Opuść",
"Join existing call?": "Dołączyć do istniejącego połączenia?",
"Join call now": "Dołącz do połączenia teraz",
"Join call": "Dołącz do połączenia",
"Invite people": "Zaproś ludzi",
"Invite": "Zaproś",
"Inspector": "Inspektor",
"Incompatible versions!": "Niekompatybilne wersje!",
"Incompatible versions": "Niekompatybilne wersje",
"Include debug logs": "Dołącz dzienniki debugowania",
"Home": "Strona domowa",
"Having trouble? Help us fix it.": "Masz problem? Pomóż nam go naprawić.",
"Grid layout menu": "Menu układu siatki",
"Full screen": "Pełen ekran",
"Freedom": "Wolność",
"Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.",
"Exit full screen": "Zamknij pełny ekran",
"Entering room…": "Wchodzenie do pokoju…",
"Download debug logs": "Pobierz dzienniki debugowania",
"Display name": "Wyświetlana nazwa",
"Developer": "Deweloper",
"Details": "Szczegóły",
"Description (optional)": "Opis (opcjonalny)",
"Debug log request": "Prośba o dzienniki debugowania",
"Debug log": "Dzienniki debugowania",
"Create account": "Utwórz konto",
"Copy and share this call link": "Skopiuj i podziel się linkiem do połączenia",
"Copied!": "Skopiowano!",
"Connection lost": "Połączenie utracone",
"Confirm password": "Potwierdź hasło",
"Close": "Zamknij",
"Change layout": "Zmień układ",
"Camera/microphone permissions needed to join the call.": "Aby dołączyć do tego połączenia, potrzebne są uprawnienia do kamery/mikrofonu.",
"Camera {{n}}": "Kamera {{n}}",
"Camera": "Kamera",
"Call type menu": "Menu rodzaju połączenia",
"Call link copied": "Skopiowano link do połączenia",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Warunki</2>",
"Avatar": "Awatar",
"Audio": "Dźwięk",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Inny użytkownik w tym połączeniu napotkał problem. Aby lepiej zdiagnozować tę usterkę, chcielibyśmy zebrać dzienniki debugowania.",
"Accept microphone permissions to join the call.": "Przyznaj uprawnienia do mikrofonu aby dołączyć do połączenia.",
"Accept camera/microphone permissions to join the call.": "Przyznaj uprawnienia do kamery/mikrofonu aby dołączyć do połączenia.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ups, coś poszło nie tak.</0><1>Przesłanie dzienników debugowania pomoże nam odnaleźć ten błąd.</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> Albo <2>Dołącz jako gość</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> Albo <2>Dołącz jako gość</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - połączenie walkie-talkie",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{name}} mówi…",
"{{name}} is presenting": "{{name}} prezentuje",
"{{displayName}}, your call is now ended": "{{displayName}}, twoje połączenie zostało zakończone",
"{{count}} people connected|one": "{{count}} osoba połączona"
}

View File

@@ -68,7 +68,7 @@
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ой, что-то пошло не так.</0><1>Отправив журнал отладки, вы поможете нам найти проблемный участок.</1>", "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ой, что-то пошло не так.</0><1>Отправив журнал отладки, вы поможете нам найти проблемный участок.</1>",
"Send debug logs": "Отправить журнал отладки", "Send debug logs": "Отправить журнал отладки",
"Save": "Сохранить", "Save": "Сохранить",
"Return to home screen": "Вернуться в начало", "Return to home screen": "Вернуться в Начало",
"Remove": "Удалить", "Remove": "Удалить",
"Recaptcha not loaded": "Невозможно начать проверку", "Recaptcha not loaded": "Невозможно начать проверку",
"Recaptcha dismissed": "Проверка не пройдена", "Recaptcha dismissed": "Проверка не пройдена",
@@ -78,7 +78,7 @@
"Passwords must match": "Пароли должны совпадать", "Passwords must match": "Пароли должны совпадать",
"Password": "Пароль", "Password": "Пароль",
"Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>", "Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>",
"Not now, return to home screen": "Не сейчас, вернитесь в начало", "Not now, return to home screen": "Не сейчас, вернуться в Начало",
"No": "Нет", "No": "Нет",
"Mute microphone": "Отключить микрофон", "Mute microphone": "Отключить микрофон",
"More": "Больше", "More": "Больше",
@@ -106,11 +106,10 @@
"Fetching group call timed out.": "Истекло время ожидания для группового звонка.", "Fetching group call timed out.": "Истекло время ожидания для группового звонка.",
"Exit full screen": "Выйти из полноэкранного режима", "Exit full screen": "Выйти из полноэкранного режима",
"Display name": "Видимое имя", "Display name": "Видимое имя",
"Developer": "Разработчик", "Developer": "Разработчику",
"Details": "Подробности", "Details": "Подробности",
"Description (optional)": "Описание (необязательно)", "Description (optional)": "Описание (необязательно)",
"Create account": "Создать аккаунт", "Create account": "Создать аккаунт",
"Copy call link and join later": "Скопировать ссылку и присоединиться позже",
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок", "Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
"Copied!": "Скопировано!", "Copied!": "Скопировано!",
"Connection lost": "Соединение потеряно", "Connection lost": "Соединение потеряно",

View File

@@ -18,7 +18,6 @@
"Connection lost": "Bağlantı koptu", "Connection lost": "Bağlantı koptu",
"Copied!": "Kopyalandı", "Copied!": "Kopyalandı",
"Copy and share this call link": "Arama bağlantısını kopyala ve paylaş", "Copy and share this call link": "Arama bağlantısını kopyala ve paylaş",
"Copy call link and join later": "Sonra katılmak üzere bağlantıyı kopyala",
"Create account": "Hesap aç", "Create account": "Hesap aç",
"Debug log": "Hata ayıklama kütüğü", "Debug log": "Hata ayıklama kütüğü",
"Debug log request": "Hata ayıklama kütük istemi", "Debug log request": "Hata ayıklama kütük istemi",

View File

@@ -1 +1,134 @@
{} {
"Loading…": "Завантаження…",
"Your recent calls": "Ваші недавні виклики",
"You can't talk at the same time": "Не можна говорити одночасно",
"Yes, join call": "Так, приєднатися до виклику",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не підтримується або блокується в цьому браузері.",
"Walkie-talkie call name": "Назва виклику-рації",
"Walkie-talkie call": "Виклик-рація",
"Waiting for other participants…": "Очікування на інших учасників…",
"Waiting for network": "Очікування мережі",
"Video call name": "Назва відеовиклику",
"Video call": "Відеовиклик",
"Video": "Відео",
"Version: {{version}}": "Версія: {{version}}",
"Username": "Ім'я користувача",
"User menu": "Меню користувача",
"User ID": "ID користувача",
"Unmute microphone": "Увімкнути мікрофон",
"Turn on camera": "Увімкнути камеру",
"Turn off camera": "Вимкнути камеру",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Це призведе до того, що звук мовця здаватиметься таким, ніби він надходить з того місця, де розміщено його плитку на екрані. (Експериментальна можливість: це може вплинути на стабільність звуку.)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи кнопку «Зареєструватися», ви погоджуєтеся з нашими <12>Умовами та положеннями</12>",
"This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?",
"Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.",
"Talking…": "Говоріть…",
"Talk over speaker": "Говорити через динамік",
"Take me Home": "Перейти до Домівки",
"Submitting feedback…": "Надсилання відгуку…",
"Submit feedback": "Надіслати відгук",
"Stop sharing screen": "Припинити показ екрана",
"Spotlight": "У центрі уваги",
"Speaker {{n}}": "Динамік {{n}}",
"Speaker": "Динамік",
"Spatial audio": "Просторовий звук",
"Sign out": "Вийти",
"Sign in": "Увійти",
"Show call inspector": "Показати інспектора виклику",
"Share screen": "Поділитися екраном",
"Settings": "Налаштування",
"Sending…": "Надсилання…",
"Sending debug logs…": "Надсилання журналу зневадження…",
"Send debug logs": "Надіслати журнал зневадження",
"Select an option": "Вибрати опцію",
"Saving…": "Збереження…",
"Save": "Зберегти",
"Return to home screen": "Повернутися на екран домівки",
"Remove": "Вилучити",
"Release to stop": "Відпустіть, щоб закінчити",
"Release spacebar key to stop": "Відпустіть пробіл, щоб закінчити",
"Registering…": "Реєстрація…",
"Register": "Зареєструватися",
"Recaptcha not loaded": "Recaptcha не завантажено",
"Recaptcha dismissed": "Recaptcha не пройдено",
"Profile": "Профіль",
"Press and hold to talk over {{name}}": "Затисніть, щоб говорити одночасно з {{name}}",
"Press and hold to talk": "Затисніть, щоб говорити",
"Press and hold spacebar to talk over {{name}}": "Щоб говорити одночасно з {{name}}, затисніть пробіл",
"Press and hold spacebar to talk": "Затисніть пробіл, щоб говорити",
"Passwords must match": "Паролі відрізняються",
"Password": "Пароль",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Інші користувачі намагаються приєднатися до цього виклику з несумісних версій. Ці користувачі повинні переконатися, що вони оновили сторінки своїх браузерів:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "Ще не зареєстровані? <2>Створіть обліковий запис</2>",
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
"No": "Ні",
"Mute microphone": "Заглушити мікрофон",
"More menu": "Усе меню",
"More": "Докладніше",
"Microphone permissions needed to join the call.": "Для участі у виклику необхідний дозвіл на користування мікрофоном.",
"Microphone {{n}}": "Мікрофон {{n}}",
"Microphone": "Мікрофон",
"Login to your account": "Увійдіть до свого облікового запису",
"Login": "Увійти",
"Logging in…": "Вхід…",
"Local volume": "Локальна гучність",
"Loading room…": "Завантаження кімнати…",
"Leave": "Вийти",
"Join existing call?": "Приєднатися до наявного виклику?",
"Join call now": "Приєднатися до виклику зараз",
"Join call": "Приєднатися до виклику",
"Invite people": "Запросити людей",
"Invite": "Запросити",
"Inspector": "Інспектор",
"Incompatible versions!": "Несумісні версії!",
"Incompatible versions": "Несумісні версії",
"Include debug logs": "Долучити журнали зневадження",
"Home": "Домівка",
"Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.",
"Grid layout menu": "Меню у вигляді сітки",
"Go": "Далі",
"Full screen": "Повноекранний режим",
"Freedom": "Свобода",
"Fetching group call timed out.": "Вичерпано час очікування групового виклику.",
"Exit full screen": "Вийти з повноекранного режиму",
"Entering room…": "Вхід у кімнату…",
"Download debug logs": "Завантажити журнали зневадження",
"Display name": "Показуване ім'я",
"Developer": "Розробнику",
"Details": "Подробиці",
"Description (optional)": "Опис (необов'язково)",
"Debug log request": "Запит журналу зневадження",
"Debug log": "Журнал зневадження",
"Create account": "Створити обліковий запис",
"Copy and share this call link": "Скопіювати та поділитися цим посиланням на виклик",
"Copied!": "Скопійовано!",
"Connection lost": "З'єднання розірвано",
"Confirm password": "Підтвердити пароль",
"Close": "Закрити",
"Change layout": "Змінити макет",
"Camera/microphone permissions needed to join the call.": "Для приєднання до виклику необхідні дозволи камери/мікрофона.",
"Camera {{n}}": "Камера {{n}}",
"Camera": "Камера",
"Call type menu": "Меню типу виклику",
"Call link copied": "Посилання на виклик скопійовано",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Приєднатися до виклику зараз», ви погодитеся з нашими <2>Умовами та положеннями</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Далі», ви погодитеся з нашими <2>Умовами та положеннями</2>",
"Avatar": "Аватар",
"Audio": "Звук",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал зневадження.",
"Accept microphone permissions to join the call.": "Надайте дозволи на використання мікрофонів для приєднання до виклику.",
"Accept camera/microphone permissions to join the call.": "Надайте дозвіл на використання камери/мікрофона для приєднання до виклику.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?</0><1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів</1>",
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Халепа, щось пішло не так.</0><1>Надсилання журналів зневадження допоможе нам виявити проблему.</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Створити обліковий запис</0> або <2>Отримати доступ як гість</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже маєте обліковий запис?</0><1><0>Увійти</0> Or <2>Отримати доступ як гість</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Виклик-рація",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{name}} балакає…",
"{{name}} is presenting": "{{name}} показує",
"{{displayName}}, your call is now ended": "{{displayName}}, ваш виклик завершено",
"{{count}} people connected|other": "{{count}} під'єдналися",
"{{count}} people connected|one": "{{count}} під'єднується",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
"{{name}} (Connecting...)": "{{name}} (З'єднання...)"
}

View File

@@ -22,6 +22,7 @@ import React, {
createContext, createContext,
useMemo, useMemo,
useContext, useContext,
useRef,
} from "react"; } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
@@ -34,6 +35,7 @@ import {
initClient, initClient,
defaultHomeserver, defaultHomeserver,
CryptoStoreIntegrityError, CryptoStoreIntegrityError,
fallbackICEServerAllowed,
} from "./matrix-utils"; } from "./matrix-utils";
import { widget } from "./widget"; import { widget } from "./widget";
import { translatedError } from "./TranslatedError"; import { translatedError } from "./TranslatedError";
@@ -86,6 +88,7 @@ interface Props {
export const ClientProvider: FC<Props> = ({ children }) => { export const ClientProvider: FC<Props> = ({ children }) => {
const history = useHistory(); const history = useHistory();
const initializing = useRef(false);
const [ const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error }, { loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState, setState,
@@ -99,6 +102,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}); });
useEffect(() => { useEffect(() => {
// In case the component is mounted, unmounted, and remounted quickly (as
// React does in strict mode), we need to make sure not to doubly initialize
// the client
if (initializing.current) return;
initializing.current = true;
const init = async (): Promise< const init = async (): Promise<
Pick<ClientProviderState, "client" | "isPasswordlessUser"> Pick<ClientProviderState, "client" | "isPasswordlessUser">
> => { > => {
@@ -130,6 +139,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
accessToken: access_token, accessToken: access_token,
userId: user_id, userId: user_id,
deviceId: device_id, deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed,
}, },
true true
), ),
@@ -145,6 +155,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
accessToken: access_token, accessToken: access_token,
userId: user_id, userId: user_id,
deviceId: device_id, deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed,
}, },
false // Don't need the crypto store just to log out false // Don't need the crypto store just to log out
); );
@@ -187,7 +198,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
userName: null, userName: null,
error: undefined, error: undefined,
}); });
}); })
.finally(() => (initializing.current = false));
}, []); }, []);
const changePassword = useCallback( const changePassword = useCallback(

View File

@@ -72,12 +72,12 @@ export function Facepile({
{...rest} {...rest}
> >
{participants.slice(0, max).map((member, i) => { {participants.slice(0, max).map((member, i) => {
const avatarUrl = member.user?.avatarUrl; const avatarUrl = member.getMxcAvatarUrl();
return ( return (
<Avatar <Avatar
key={member.userId} key={member.userId}
size={size} size={size}
src={avatarUrl} src={avatarUrl ?? undefined}
fallback={member.name.slice(0, 1).toUpperCase()} fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar} className={styles.avatar}
style={{ left: i * (_size - _overlap) }} style={{ left: i * (_size - _overlap) }}

View File

@@ -1,4 +1,5 @@
<svg width="260" height="30" viewBox="0 0 260 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="260" height="30" viewBox="0 0 260 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<desc>Element Call (Beta)</desc>
<circle cx="15" cy="15" r="13" fill="white"/> <circle cx="15" cy="15" r="13" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 30C23.2843 30 30 23.2843 30 15C30 6.71573 23.2843 0 15 0C6.71573 0 0 6.71573 0 15C0 23.2843 6.71573 30 15 30ZM12.2579 6.98923C12.2579 6.38376 12.7497 5.89292 13.3565 5.89292C17.4687 5.89292 20.8024 9.21967 20.8024 13.3234C20.8024 13.9289 20.3106 14.4197 19.7038 14.4197C19.0971 14.4197 18.6052 13.9289 18.6052 13.3234C18.6052 10.4306 16.2553 8.08554 13.3565 8.08554C12.7497 8.08554 12.2579 7.59471 12.2579 6.98923ZM24.1066 13.3235C24.1066 12.7181 23.6148 12.2272 23.008 12.2272C22.4013 12.2272 21.9094 12.7181 21.9094 13.3235C21.9094 16.2163 19.5595 18.5614 16.6607 18.5614C16.0539 18.5614 15.5621 19.0523 15.5621 19.6577C15.5621 20.2632 16.0539 20.754 16.6607 20.754C20.7729 20.754 24.1066 17.4273 24.1066 13.3235ZM17.7601 23.011C17.7601 23.6164 17.2682 24.1073 16.6615 24.1073C12.5492 24.1073 9.21553 20.7805 9.21553 16.6768C9.21553 16.0713 9.70739 15.5805 10.3141 15.5805C10.9209 15.5805 11.4127 16.0713 11.4127 16.6768C11.4127 19.5696 13.7627 21.9146 16.6615 21.9146C17.2682 21.9146 17.7601 22.4055 17.7601 23.011ZM5.89281 16.6769C5.89281 17.2824 6.38466 17.7732 6.9914 17.7732C7.59813 17.7732 8.08999 17.2824 8.08999 16.6769C8.08999 13.7841 10.4399 11.439 13.3388 11.439C13.9455 11.439 14.4373 10.9482 14.4373 10.3427C14.4373 9.73722 13.9455 9.24639 13.3388 9.24639C9.22647 9.24639 5.89281 12.5731 5.89281 16.6769Z" fill="#0DBD8B"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M15 30C23.2843 30 30 23.2843 30 15C30 6.71573 23.2843 0 15 0C6.71573 0 0 6.71573 0 15C0 23.2843 6.71573 30 15 30ZM12.2579 6.98923C12.2579 6.38376 12.7497 5.89292 13.3565 5.89292C17.4687 5.89292 20.8024 9.21967 20.8024 13.3234C20.8024 13.9289 20.3106 14.4197 19.7038 14.4197C19.0971 14.4197 18.6052 13.9289 18.6052 13.3234C18.6052 10.4306 16.2553 8.08554 13.3565 8.08554C12.7497 8.08554 12.2579 7.59471 12.2579 6.98923ZM24.1066 13.3235C24.1066 12.7181 23.6148 12.2272 23.008 12.2272C22.4013 12.2272 21.9094 12.7181 21.9094 13.3235C21.9094 16.2163 19.5595 18.5614 16.6607 18.5614C16.0539 18.5614 15.5621 19.0523 15.5621 19.6577C15.5621 20.2632 16.0539 20.754 16.6607 20.754C20.7729 20.754 24.1066 17.4273 24.1066 13.3235ZM17.7601 23.011C17.7601 23.6164 17.2682 24.1073 16.6615 24.1073C12.5492 24.1073 9.21553 20.7805 9.21553 16.6768C9.21553 16.0713 9.70739 15.5805 10.3141 15.5805C10.9209 15.5805 11.4127 16.0713 11.4127 16.6768C11.4127 19.5696 13.7627 21.9146 16.6615 21.9146C17.2682 21.9146 17.7601 22.4055 17.7601 23.011ZM5.89281 16.6769C5.89281 17.2824 6.38466 17.7732 6.9914 17.7732C7.59813 17.7732 8.08999 17.2824 8.08999 16.6769C8.08999 13.7841 10.4399 11.439 13.3388 11.439C13.9455 11.439 14.4373 10.9482 14.4373 10.3427C14.4373 9.73722 13.9455 9.24639 13.3388 9.24639C9.22647 9.24639 5.89281 12.5731 5.89281 16.6769Z" fill="#0DBD8B"/>
<path d="M53.5406 17.258H42.8052C42.932 18.3814 43.3397 19.2782 44.0282 19.9486C44.7167 20.6009 45.6227 20.927 46.746 20.927C47.4889 20.927 48.1593 20.7459 48.7572 20.3835C49.3551 20.0211 49.7809 19.5319 50.0346 18.9159H53.296C52.8611 20.3472 52.0458 21.5068 50.8499 22.3947C49.6722 23.2644 48.2771 23.6992 46.6645 23.6992C44.5627 23.6992 42.8596 23.0016 41.555 21.6065C40.2686 20.2114 39.6254 18.4448 39.6254 16.3068C39.6254 14.2231 40.2776 12.4747 41.5822 11.0614C42.8867 9.64814 44.5718 8.94151 46.6373 8.94151C48.7029 8.94151 50.3698 9.63908 51.6381 11.0342C52.9245 12.4112 53.5677 14.1506 53.5677 16.2524L53.5406 17.258ZM46.6373 11.5778C45.6227 11.5778 44.7801 11.8767 44.1098 12.4747C43.4394 13.0726 43.0226 13.8698 42.8596 14.8663H50.3607C50.2158 13.8698 49.8172 13.0726 49.1649 12.4747C48.5126 11.8767 47.6701 11.5778 46.6373 11.5778Z" fill="white"/> <path d="M53.5406 17.258H42.8052C42.932 18.3814 43.3397 19.2782 44.0282 19.9486C44.7167 20.6009 45.6227 20.927 46.746 20.927C47.4889 20.927 48.1593 20.7459 48.7572 20.3835C49.3551 20.0211 49.7809 19.5319 50.0346 18.9159H53.296C52.8611 20.3472 52.0458 21.5068 50.8499 22.3947C49.6722 23.2644 48.2771 23.6992 46.6645 23.6992C44.5627 23.6992 42.8596 23.0016 41.555 21.6065C40.2686 20.2114 39.6254 18.4448 39.6254 16.3068C39.6254 14.2231 40.2776 12.4747 41.5822 11.0614C42.8867 9.64814 44.5718 8.94151 46.6373 8.94151C48.7029 8.94151 50.3698 9.63908 51.6381 11.0342C52.9245 12.4112 53.5677 14.1506 53.5677 16.2524L53.5406 17.258ZM46.6373 11.5778C45.6227 11.5778 44.7801 11.8767 44.1098 12.4747C43.4394 13.0726 43.0226 13.8698 42.8596 14.8663H50.3607C50.2158 13.8698 49.8172 13.0726 49.1649 12.4747C48.5126 11.8767 47.6701 11.5778 46.6373 11.5778Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -20,8 +20,8 @@ limitations under the License.
// dependency references. // dependency references.
import "matrix-js-sdk/src/browser-index"; import "matrix-js-sdk/src/browser-index";
import React from "react"; import React, { StrictMode } from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { createBrowserHistory } from "history"; import { createBrowserHistory } from "history";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing"; import { Integrations } from "@sentry/tracing";
@@ -39,12 +39,23 @@ initRageshake();
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`); console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
const root = createRoot(document.getElementById("root")!);
let fatalError: Error | null = null;
if (!window.isSecureContext) { if (!window.isSecureContext) {
throw new Error( fatalError = new Error(
"This app cannot run in an insecure context. To fix this, access the app " + "This app cannot run in an insecure context. To fix this, access the app " +
"via a local loopback address, or serve it over HTTPS.\n" + "via a local loopback address, or serve it over HTTPS.\n" +
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
); );
} else if (!navigator.mediaDevices) {
fatalError = new Error("Your browser does not support WebRTC.");
}
if (fatalError !== null) {
root.render(fatalError.message);
throw fatalError; // Stop the app early
} }
if (import.meta.env.VITE_CUSTOM_THEME) { if (import.meta.env.VITE_CUSTOM_THEME) {
@@ -138,9 +149,8 @@ i18n
}, },
}); });
ReactDOM.render( root.render(
<React.StrictMode> <StrictMode>
<App history={history} /> <App history={history} />
</React.StrictMode>, </StrictMode>
document.getElementById("root")
); );

View File

@@ -1,5 +1,3 @@
import Olm from "@matrix-org/olm";
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb"; import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
import { MemoryStore } from "matrix-js-sdk/src/store/memory"; import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
@@ -20,10 +18,13 @@ import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room"; import type { Room } from "matrix-js-sdk/src/models/room";
import IndexedDBWorker from "./IndexedDBWorker?worker"; import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getUrlParams } from "./UrlParams"; import { getUrlParams } from "./UrlParams";
import { loadOlm } from "./olm";
export const defaultHomeserver = export const defaultHomeserver =
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ?? (import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
`${window.location.protocol}//${window.location.host}`; `${window.location.protocol}//${window.location.host}`;
export const fallbackICEServerAllowed =
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
export const defaultHomeserverHost = new URL(defaultHomeserver).host; export const defaultHomeserverHost = new URL(defaultHomeserver).host;
@@ -72,12 +73,9 @@ export async function initClient(
clientOptions: ICreateClientOpts, clientOptions: ICreateClientOpts,
restore: boolean restore: boolean
): Promise<MatrixClient> { ): Promise<MatrixClient> {
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10 await loadOlm();
window.OLM_OPTIONS = {};
await Olm.init({ locateFile: () => olmWasmPath });
let indexedDB: IDBFactory; let indexedDB: IDBFactory;
try { try {
indexedDB = window.indexedDB; indexedDB = window.indexedDB;
} catch (e) {} } catch (e) {}
@@ -152,6 +150,7 @@ export async function initClient(
// so we don't want API calls taking ages, we'd rather they just fail. // so we don't want API calls taking ages, we'd rather they just fail.
localTimeoutMs: 5000, localTimeoutMs: 5000,
useE2eForGroupCall: e2eEnabled, useE2eForGroupCall: e2eEnabled,
fallbackICEServerAllowed: fallbackICEServerAllowed,
}); });
try { try {

View File

@@ -48,24 +48,42 @@ export async function findDeviceByName(
* @return The available media devices * @return The available media devices
*/ */
export async function getDevices(): Promise<MediaDeviceInfo[]> { export async function getDevices(): Promise<MediaDeviceInfo[]> {
let stream: MediaStream; // First get the devices without their labels, to learn what kinds of streams
// we can request
let devices: MediaDeviceInfo[];
try { try {
stream = await navigator.mediaDevices.getUserMedia({ devices = await navigator.mediaDevices.enumerateDevices();
audio: true, } catch (error) {
video: true, logger.warn("Unable to refresh WebRTC devices", error);
}); devices = [];
}
let stream: MediaStream | null = null;
try {
if (devices.some((d) => d.kind === "audioinput")) {
// Holding just an audio stream will be enough to get us all device labels
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} else if (devices.some((d) => d.kind === "videoinput")) {
// We have to resort to a video stream
stream = await navigator.mediaDevices.getUserMedia({ video: true });
}
} catch (e) { } catch (e) {
logger.info("Couldn't get media stream for enumerateDevices: failing"); logger.info("Couldn't get media stream for enumerateDevices: failing");
throw e; throw e;
} }
try { if (stream !== null) {
return await navigator.mediaDevices.enumerateDevices(); try {
} catch (error) { return await navigator.mediaDevices.enumerateDevices();
logger.warn("Unable to refresh WebRTC Devices: ", error); } catch (error) {
} finally { logger.warn("Unable to refresh WebRTC devices", error);
for (const track of stream.getTracks()) { } finally {
track.stop(); for (const track of stream.getTracks()) {
track.stop();
}
} }
} }
// If all else failed, continue without device labels
return devices;
} }

29
src/olm.ts Normal file
View File

@@ -0,0 +1,29 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Olm from "@matrix-org/olm";
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
// https://gitlab.matrix.org/matrix-org/olm/-/issues/10
window.OLM_OPTIONS = {};
let olmLoaded: Promise<void> | null = null;
/**
* Loads Olm, if not already loaded.
*/
export const loadOlm = (): Promise<void> =>
(olmLoaded ??= Olm.init({ locateFile: () => olmWasmPath }));

View File

@@ -76,7 +76,6 @@ export function GroupCallView({
toggleScreensharing, toggleScreensharing,
requestingScreenshare, requestingScreenshare,
isScreensharing, isScreensharing,
localScreenshareFeed,
screenshareFeeds, screenshareFeeds,
participants, participants,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
@@ -221,6 +220,7 @@ export function GroupCallView({
client={client} client={client}
roomName={groupCall.room.name} roomName={groupCall.room.name}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
participants={participants}
microphoneMuted={microphoneMuted} microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted} localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted} toggleLocalVideoMuted={toggleLocalVideoMuted}
@@ -230,7 +230,6 @@ export function GroupCallView({
onLeave={onLeave} onLeave={onLeave}
toggleScreensharing={toggleScreensharing} toggleScreensharing={toggleScreensharing}
isScreensharing={isScreensharing} isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds} screenshareFeeds={screenshareFeeds}
roomIdOrAlias={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers} unencryptedEventsFromUsers={unencryptedEventsFromUsers}

View File

@@ -60,6 +60,7 @@ import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
import { widget, ElementWidgetActions } from "../widget"; import { widget, ElementWidgetActions } from "../widget";
import { useJoinRule } from "./useJoinRule"; import { useJoinRule } from "./useJoinRule";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -70,6 +71,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
groupCall: GroupCall; groupCall: GroupCall;
participants: RoomMember[];
roomName: string; roomName: string;
avatarUrl: string; avatarUrl: string;
microphoneMuted: boolean; microphoneMuted: boolean;
@@ -82,14 +84,16 @@ interface Props {
onLeave: () => void; onLeave: () => void;
isScreensharing: boolean; isScreensharing: boolean;
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
localScreenshareFeed: CallFeed;
roomIdOrAlias: string; roomIdOrAlias: string;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean; hideHeader: boolean;
} }
export interface Participant { // Represents something that should get a tile on the layout,
// ie. a user's video feed or a screen share feed.
export interface TileDescriptor {
id: string; id: string;
member: RoomMember;
focused: boolean; focused: boolean;
presenter: boolean; presenter: boolean;
callFeed?: CallFeed; callFeed?: CallFeed;
@@ -99,6 +103,7 @@ export interface Participant {
export function InCallView({ export function InCallView({
client, client,
groupCall, groupCall,
participants,
roomName, roomName,
avatarUrl, avatarUrl,
microphoneMuted, microphoneMuted,
@@ -111,7 +116,6 @@ export function InCallView({
toggleScreensharing, toggleScreensharing,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localScreenshareFeed,
roomIdOrAlias, roomIdOrAlias,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
hideHeader, hideHeader,
@@ -185,39 +189,48 @@ export function InCallView({
}, [setLayout]); }, [setLayout]);
const items = useMemo(() => { const items = useMemo(() => {
const participants: Participant[] = []; const tileDescriptors: TileDescriptor[] = [];
for (const callFeed of userMediaFeeds) { // one tile for each participants, to start with (we want a tile for everyone we
participants.push({ // think should be in the call, even if we don't have a media feed for them yet)
id: callFeed.stream.id, for (const p of participants) {
callFeed, const userMediaFeed = userMediaFeeds.find((f) => f.userId === p.userId);
focused:
screenshareFeeds.length === 0 && callFeed.userId === activeSpeaker, // NB. this assumes that the same user can't join more than once from multiple
isLocal: callFeed.isLocal(), // devices, but the participants are just RoomMembers, so this assumption is baked
// into GroupCall itself.
tileDescriptors.push({
id: p.userId,
member: p,
callFeed: userMediaFeed,
focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker,
isLocal: p.userId === client.getUserId(),
presenter: false, presenter: false,
}); });
} }
for (const callFeed of screenshareFeeds) { // add the screenshares too
const userMediaItem = participants.find( for (const screenshareFeed of screenshareFeeds) {
(item) => item.callFeed.userId === callFeed.userId const userMediaItem = tileDescriptors.find(
(item) => item.member.userId === screenshareFeed.userId
); );
if (userMediaItem) { if (userMediaItem) {
userMediaItem.presenter = true; userMediaItem.presenter = true;
} }
participants.push({ tileDescriptors.push({
id: callFeed.stream.id, id: screenshareFeed.stream.id,
callFeed, member: userMediaItem?.member,
callFeed: screenshareFeed,
focused: true, focused: true,
isLocal: callFeed.isLocal(), isLocal: screenshareFeed.isLocal(),
presenter: false, presenter: false,
}); });
} }
return participants; return tileDescriptors;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]); }, [client, participants, userMediaFeeds, activeSpeaker, screenshareFeeds]);
// The maximised participant: either the participant that the user has // The maximised participant: either the participant that the user has
// manually put in fullscreen, or the focused (active) participant if the // manually put in fullscreen, or the focused (active) participant if the
@@ -237,14 +250,14 @@ export function InCallView({
const renderAvatar = useCallback( const renderAvatar = useCallback(
(roomMember: RoomMember, width: number, height: number) => { (roomMember: RoomMember, width: number, height: number) => {
const avatarUrl = roomMember.user?.avatarUrl; const avatarUrl = roomMember.getMxcAvatarUrl();
const size = Math.round(Math.min(width, height) / 2); const size = Math.round(Math.min(width, height) / 2);
return ( return (
<Avatar <Avatar
key={roomMember.userId} key={roomMember.userId}
size={size} size={size}
src={avatarUrl} src={avatarUrl ?? undefined}
fallback={roomMember.name.slice(0, 1).toUpperCase()} fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar} className={styles.avatar}
/> />
@@ -253,6 +266,8 @@ export function InCallView({
[] []
); );
const prefersReducedMotion = usePrefersReducedMotion();
const renderContent = (): JSX.Element => { const renderContent = (): JSX.Element => {
if (items.length === 0) { if (items.length === 0) {
return ( return (
@@ -280,8 +295,18 @@ export function InCallView({
} }
return ( return (
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}> <VideoGrid
{({ item, ...rest }: { item: Participant; [x: string]: unknown }) => ( items={items}
layout={layout}
disableAnimations={prefersReducedMotion || isSafari}
>
{({
item,
...rest
}: {
item: TileDescriptor;
[x: string]: unknown;
}) => (
<VideoTileContainer <VideoTileContainer
key={item.id} key={item.id}
item={item} item={item}

View File

@@ -19,7 +19,7 @@ import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { PressEvent } from "@react-types/shared"; import { PressEvent } from "@react-types/shared";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import styles from "./LobbyView.module.css"; import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button"; import { Button, CopyButton } from "../button";
@@ -130,24 +130,26 @@ export function LobbyView({
audioOutput={audioOutput} audioOutput={audioOutput}
/> />
)} )}
<Button <Trans>
ref={joinCallButtonRef} <Button
className={styles.copyButton} ref={joinCallButtonRef}
size="lg" className={styles.copyButton}
disabled={state !== GroupCallState.LocalCallFeedInitialized} size="lg"
onPress={onEnter} disabled={state !== GroupCallState.LocalCallFeedInitialized}
> onPress={onEnter}
Join call now >
</Button> Join call now
<Body>Or</Body> </Button>
<CopyButton <Body>Or</Body>
variant="secondaryCopy" <CopyButton
value={getRoomUrl(roomIdOrAlias)} variant="secondaryCopy"
className={styles.copyButton} value={getRoomUrl(roomIdOrAlias)}
copiedMessage={t("Call link copied")} className={styles.copyButton}
> copiedMessage={t("Call link copied")}
{t("Copy call link and join later")} >
</CopyButton> Copy call link and join later
</CopyButton>
</Trans>
</div> </div>
{!isEmbedded && ( {!isEmbedded && (
<Body className={styles.joinRoomFooter}> <Body className={styles.joinRoomFooter}>

View File

@@ -22,6 +22,7 @@ import styles from "./PTTButton.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { useEventTarget } from "../useEvents"; import { useEventTarget } from "../useEvents";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
interface Props { interface Props {
enabled: boolean; enabled: boolean;
@@ -159,8 +160,14 @@ export const PTTButton: React.FC<Props> = ({
// TODO: We will need to disable this for a global PTT hotkey to work // TODO: We will need to disable this for a global PTT hotkey to work
useEventTarget(window, "blur", unhold); useEventTarget(window, "blur", unhold);
const prefersReducedMotion = usePrefersReducedMotion();
const { shadow } = useSpring({ const { shadow } = useSpring({
shadow: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6, immediate: prefersReducedMotion,
shadow: prefersReducedMotion
? activeSpeakerUserId
? 17
: 0
: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6,
config: { config: {
clamp: true, clamp: true,
tension: 300, tension: 300,

View File

@@ -27,9 +27,11 @@ import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IWidgetApiRequest } from "matrix-widget-api";
import { usePageUnload } from "./usePageUnload"; import { usePageUnload } from "./usePageUnload";
import { TranslatedError, translatedError } from "../TranslatedError"; import { TranslatedError, translatedError } from "../TranslatedError";
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
export interface UseGroupCallReturnType { export interface UseGroupCallReturnType {
state: GroupCallState; state: GroupCallState;
@@ -49,8 +51,7 @@ export interface UseGroupCallReturnType {
requestingScreenshare: boolean; requestingScreenshare: boolean;
isScreensharing: boolean; isScreensharing: boolean;
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
localScreenshareFeed: CallFeed; localDesktopCapturerSourceId: string; // XXX: This looks unused?
localDesktopCapturerSourceId: string;
participants: RoomMember[]; participants: RoomMember[];
hasLocalParticipant: boolean; hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
@@ -66,7 +67,6 @@ interface State {
microphoneMuted: boolean; microphoneMuted: boolean;
localVideoMuted: boolean; localVideoMuted: boolean;
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
localScreenshareFeed: CallFeed;
localDesktopCapturerSourceId: string; localDesktopCapturerSourceId: string;
isScreensharing: boolean; isScreensharing: boolean;
requestingScreenshare: boolean; requestingScreenshare: boolean;
@@ -87,7 +87,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
localVideoMuted, localVideoMuted,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localScreenshareFeed,
localDesktopCapturerSourceId, localDesktopCapturerSourceId,
participants, participants,
hasLocalParticipant, hasLocalParticipant,
@@ -105,7 +104,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
localVideoMuted: false, localVideoMuted: false,
isScreensharing: false, isScreensharing: false,
screenshareFeeds: [], screenshareFeeds: [],
localScreenshareFeed: null,
localDesktopCapturerSourceId: null, localDesktopCapturerSourceId: null,
requestingScreenshare: false, requestingScreenshare: false,
participants: [], participants: [],
@@ -133,7 +131,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
microphoneMuted: groupCall.isMicrophoneMuted(), microphoneMuted: groupCall.isMicrophoneMuted(),
localVideoMuted: groupCall.isLocalVideoMuted(), localVideoMuted: groupCall.isLocalVideoMuted(),
isScreensharing: groupCall.isScreensharing(), isScreensharing: groupCall.isScreensharing(),
localScreenshareFeed: groupCall.localScreenshareFeed,
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId, localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
screenshareFeeds: [...groupCall.screenshareFeeds], screenshareFeeds: [...groupCall.screenshareFeeds],
participants: [...groupCall.participants], participants: [...groupCall.participants],
@@ -170,12 +167,11 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
function onLocalScreenshareStateChanged( function onLocalScreenshareStateChanged(
isScreensharing: boolean, isScreensharing: boolean,
localScreenshareFeed: CallFeed, _localScreenshareFeed: CallFeed,
localDesktopCapturerSourceId: string localDesktopCapturerSourceId: string
): void { ): void {
updateState({ updateState({
isScreensharing, isScreensharing,
localScreenshareFeed,
localDesktopCapturerSourceId, localDesktopCapturerSourceId,
}); });
} }
@@ -226,7 +222,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
microphoneMuted: groupCall.isMicrophoneMuted(), microphoneMuted: groupCall.isMicrophoneMuted(),
localVideoMuted: groupCall.isLocalVideoMuted(), localVideoMuted: groupCall.isLocalVideoMuted(),
isScreensharing: groupCall.isScreensharing(), isScreensharing: groupCall.isScreensharing(),
localScreenshareFeed: groupCall.localScreenshareFeed,
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId, localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
screenshareFeeds: [...groupCall.screenshareFeeds], screenshareFeeds: [...groupCall.screenshareFeeds],
participants: [...groupCall.participants], participants: [...groupCall.participants],
@@ -301,16 +296,84 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted()); groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted());
}, [groupCall]); }, [groupCall]);
const toggleScreensharing = useCallback(() => { const toggleScreensharing = useCallback(async () => {
updateState({ requestingScreenshare: true }); if (!groupCall.isScreensharing()) {
// toggling on
updateState({ requestingScreenshare: true });
groupCall try {
.setScreensharingEnabled(!groupCall.isScreensharing(), { audio: true }) await groupCall.setScreensharingEnabled(true, {
.then(() => { audio: true,
throwOnFail: true,
});
updateState({ requestingScreenshare: false }); updateState({ requestingScreenshare: false });
}); } catch (e) {
// this will fail in Electron because getDisplayMedia just throws a permission
// error, so if we have a widget API, try requesting via that.
if (widget) {
const reply = await widget.api.transport.send(
ElementWidgetActions.ScreenshareRequest,
{}
);
if (!reply.pending) {
updateState({ requestingScreenshare: false });
}
}
}
} else {
// toggling off
groupCall.setScreensharingEnabled(false);
}
}, [groupCall]); }, [groupCall]);
const onScreenshareStart = useCallback(
async (ev: CustomEvent<IWidgetApiRequest>) => {
updateState({ requestingScreenshare: false });
const data = ev.detail.data as unknown as ScreenshareStartData;
await groupCall.setScreensharingEnabled(true, {
desktopCapturerSourceId: data.desktopCapturerSourceId as string,
audio: !data.desktopCapturerSourceId,
});
await widget.api.transport.reply(ev.detail, {});
},
[groupCall]
);
const onScreenshareStop = useCallback(
async (ev: CustomEvent<IWidgetApiRequest>) => {
updateState({ requestingScreenshare: false });
await groupCall.setScreensharingEnabled(false);
await widget.api.transport.reply(ev.detail, {});
},
[groupCall]
);
useEffect(() => {
if (widget) {
widget.lazyActions.on(
ElementWidgetActions.ScreenshareStart,
onScreenshareStart
);
widget.lazyActions.on(
ElementWidgetActions.ScreenshareStop,
onScreenshareStop
);
return () => {
widget.lazyActions.off(
ElementWidgetActions.ScreenshareStart,
onScreenshareStart
);
widget.lazyActions.off(
ElementWidgetActions.ScreenshareStop,
onScreenshareStop
);
};
}
}, [onScreenshareStart, onScreenshareStop]);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@@ -342,7 +405,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
requestingScreenshare, requestingScreenshare,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localScreenshareFeed,
localDesktopCapturerSourceId, localDesktopCapturerSourceId,
participants, participants,
hasLocalParticipant, hasLocalParticipant,

View File

@@ -160,7 +160,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
if ( if (
// @ts-ignore // @ts-ignore
mediaHandler.videoInput !== videoInput || (mediaHandler.videoInput && mediaHandler.videoInput !== videoInput) ||
// @ts-ignore // @ts-ignore
mediaHandler.audioInput !== audioInput mediaHandler.audioInput !== audioInput
) { ) {

View File

@@ -38,7 +38,7 @@ export function TabContainer<T extends object>(
<div className={classNames(styles.tabContainer, props.className)}> <div className={classNames(styles.tabContainer, props.className)}>
<ul {...tabListProps} ref={ref} className={styles.tabList}> <ul {...tabListProps} ref={ref} className={styles.tabList}>
{[...state.collection].map((item) => ( {[...state.collection].map((item) => (
<Tab item={item} state={state} /> <Tab item={item} state={state} key={item.key} />
))} ))}
</ul> </ul>
<TabPanel key={state.selectedItem?.key} state={state} /> <TabPanel key={state.selectedItem?.key} state={state} />

View File

@@ -24,6 +24,11 @@
color: var(--links); color: var(--links);
} }
.link:hover {
text-decoration: underline;
opacity: initial;
}
.primary { .primary {
color: var(--accent); color: var(--accent);
} }

View File

@@ -18,8 +18,7 @@ import { useEffect } from "react";
export function usePageTitle(title: string): void { export function usePageTitle(title: string): void {
useEffect(() => { useEffect(() => {
const productName = const productName = import.meta.env.VITE_PRODUCT_NAME || "Element Call";
import.meta.env.VITE_PRODUCT_NAME || "Matrix Video Chat";
document.title = title ? `${productName} | ${title}` : productName; document.title = title ? `${productName} | ${title}` : productName;
}, [title]); }, [title]);
} }

View File

@@ -0,0 +1,42 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useRef, useState } from "react";
import { useEventTarget } from "./useEvents";
/**
* @returns Whether the user has requested reduced motion.
*/
export const usePrefersReducedMotion = () => {
const mediaQuery = useRef<MediaQueryList>();
if (mediaQuery.current === undefined)
mediaQuery.current = matchMedia("(prefers-reduced-motion)");
const [prefersReducedMotion, setPrefersReducedMotion] = useState(
mediaQuery.current.matches
);
useEventTarget(
mediaQuery.current!,
"change",
useCallback(
() => setPrefersReducedMotion(mediaQuery.current!.matches),
[setPrefersReducedMotion]
)
);
return prefersReducedMotion;
};

View File

@@ -16,7 +16,7 @@ limitations under the License.
import React, { FC, useEffect, useRef } from "react"; import React, { FC, useEffect, useRef } from "react";
import { Participant } from "../room/InCallView"; import { TileDescriptor } from "../room/InCallView";
import { useCallFeed } from "./useCallFeed"; import { useCallFeed } from "./useCallFeed";
import { useMediaStreamTrackCount } from "./useMediaStream"; import { useMediaStreamTrackCount } from "./useMediaStream";
@@ -24,7 +24,7 @@ import { useMediaStreamTrackCount } from "./useMediaStream";
// only way to a hook on an array // only way to a hook on an array
interface AudioForParticipantProps { interface AudioForParticipantProps {
item: Participant; item: TileDescriptor;
audioContext: AudioContext; audioContext: AudioContext;
audioDestination: AudioNode; audioDestination: AudioNode;
} }
@@ -78,7 +78,7 @@ export const AudioForParticipant: FC<AudioForParticipantProps> = ({
}; };
interface AudioContainerProps { interface AudioContainerProps {
items: Participant[]; items: TileDescriptor[];
audioContext: AudioContext; audioContext: AudioContext;
audioDestination: AudioNode; audioDestination: AudioNode;
} }

View File

@@ -16,11 +16,12 @@ limitations under the License.
import React, { useState } from "react"; import React, { useState } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import { RoomMember } from "matrix-js-sdk";
import { VideoGrid, useVideoGridLayout } from "./VideoGrid"; import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
import { VideoTile } from "./VideoTile"; import { VideoTile } from "./VideoTile";
import { Button } from "../button"; import { Button } from "../button";
import { Participant } from "../room/InCallView"; import { TileDescriptor } from "../room/InCallView";
export default { export default {
title: "VideoGrid", title: "VideoGrid",
@@ -33,10 +34,11 @@ export const ParticipantsTest = () => {
const { layout, setLayout } = useVideoGridLayout(false); const { layout, setLayout } = useVideoGridLayout(false);
const [participantCount, setParticipantCount] = useState(1); const [participantCount, setParticipantCount] = useState(1);
const items: Participant[] = useMemo( const items: TileDescriptor[] = useMemo(
() => () =>
new Array(participantCount).fill(undefined).map((_, i) => ({ new Array(participantCount).fill(undefined).map((_, i) => ({
id: (i + 1).toString(), id: (i + 1).toString(),
member: new RoomMember("!fake:room.id", `@user${i}:fake.dummy`),
focused: false, focused: false,
presenter: false, presenter: false,
})), })),
@@ -77,6 +79,7 @@ export const ParticipantsTest = () => {
key={item.id} key={item.id}
name={`User ${item.id}`} name={`User ${item.id}`}
disableSpeakingIndicator={items.length < 3} disableSpeakingIndicator={items.length < 3}
hasFeed={true}
{...rest} {...rest}
/> />
)} )}

View File

@@ -23,7 +23,7 @@ import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/typ
import styles from "./VideoGrid.module.css"; import styles from "./VideoGrid.module.css";
import { Layout } from "../room/GridLayoutMenu"; import { Layout } from "../room/GridLayoutMenu";
import { Participant } from "../room/InCallView"; import { TileDescriptor } from "../room/InCallView";
interface TilePosition { interface TilePosition {
x: number; x: number;
@@ -36,7 +36,7 @@ interface TilePosition {
interface Tile { interface Tile {
key: Key; key: Key;
order: number; order: number;
item: Participant; item: TileDescriptor;
remove: boolean; remove: boolean;
focused: boolean; focused: boolean;
presenter: boolean; presenter: boolean;
@@ -120,7 +120,7 @@ function getTilePositions(
layout: Layout layout: Layout
): TilePosition[] { ): TilePosition[] {
if (layout === "freedom") { if (layout === "freedom") {
if (tileCount === 2 && !hasPresenter) { if (tileCount === 2 && !hasPresenter && focusedTileCount === 0) {
return getOneOnOneLayoutTilePositions( return getOneOnOneLayoutTilePositions(
gridWidth, gridWidth,
gridHeight, gridHeight,
@@ -654,10 +654,17 @@ function getSubGridPositions(
// Sets the 'order' property on tiles based on the layout param and // Sets the 'order' property on tiles based on the layout param and
// other properties of the tiles, eg. 'focused' and 'presenter' // other properties of the tiles, eg. 'focused' and 'presenter'
function reorderTiles(tiles: Tile[], layout: Layout) { function reorderTiles(tiles: Tile[], layout: Layout) {
// We use a special layout for 1:1 to always put the local tile first.
// We only do this if there are two tiles (obviously) and exactly one
// of them is local: during startup we can have tiles from other users
// but not our own, due to the order they're added, so without this we
// can assign multiple remote tiles order '1' and this persists through
// subsequent reorders because we preserve the order of the tiles.
if ( if (
layout === "freedom" && layout === "freedom" &&
tiles.length === 2 && tiles.length === 2 &&
!tiles.some((t) => t.presenter) tiles.filter((t) => t.item.isLocal).length === 1 &&
!tiles.some((t) => t.presenter || t.focused)
) { ) {
// 1:1 layout // 1:1 layout
tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1)); tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
@@ -693,12 +700,12 @@ interface ChildrenProperties extends ReactDOMAttributes {
}; };
width: number; width: number;
height: number; height: number;
item: Participant; item: TileDescriptor;
[index: string]: unknown; [index: string]: unknown;
} }
interface VideoGridProps { interface VideoGridProps {
items: Participant[]; items: TileDescriptor[];
layout: Layout; layout: Layout;
disableAnimations?: boolean; disableAnimations?: boolean;
children: (props: ChildrenProperties) => React.ReactNode; children: (props: ChildrenProperties) => React.ReactNode;
@@ -999,7 +1006,7 @@ export function VideoGrid({
let newTiles = tiles; let newTiles = tiles;
if (tiles.length === 2 && !tiles.some((t) => t.presenter)) { if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable // We're in 1:1 mode, so only the local tile should be draggable
if (!dragTile.item.isLocal) return; if (!dragTile.item.isLocal) return;

View File

@@ -26,6 +26,7 @@ import { AudioButton, FullscreenButton } from "../button/Button";
interface Props { interface Props {
name: string; name: string;
hasFeed: Boolean;
speaking?: boolean; speaking?: boolean;
audioMuted?: boolean; audioMuted?: boolean;
videoMuted?: boolean; videoMuted?: boolean;
@@ -47,6 +48,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
( (
{ {
name, name,
hasFeed,
speaking, speaking,
audioMuted, audioMuted,
videoMuted, videoMuted,
@@ -70,9 +72,10 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
const { t } = useTranslation(); const { t } = useTranslation();
const toolbarButtons: JSX.Element[] = []; const toolbarButtons: JSX.Element[] = [];
if (!isLocal) { if (hasFeed && !isLocal) {
toolbarButtons.push( toolbarButtons.push(
<AudioButton <AudioButton
key="localVolume"
className={styles.button} className={styles.button}
volume={localVolume} volume={localVolume}
onPress={onOptionsPress} onPress={onOptionsPress}
@@ -82,6 +85,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
if (screenshare) { if (screenshare) {
toolbarButtons.push( toolbarButtons.push(
<FullscreenButton <FullscreenButton
key="fullscreen"
className={styles.button} className={styles.button}
fullscreen={fullscreen} fullscreen={fullscreen}
onPress={onFullscreen} onPress={onFullscreen}
@@ -90,6 +94,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
} }
} }
const caption = hasFeed ? name : t("{{name}} (Connecting...)", { name });
return ( return (
<animated.div <animated.div
className={classNames(styles.videoTile, className, { className={classNames(styles.videoTile, className, {
@@ -120,7 +126,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
<div className={classNames(styles.infoBubble, styles.memberName)}> <div className={classNames(styles.infoBubble, styles.memberName)}>
{audioMuted && !videoMuted && <MicMutedIcon />} {audioMuted && !videoMuted && <MicMutedIcon />}
{videoMuted && <VideoMutedIcon />} {videoMuted && <VideoMutedIcon />}
<span title={name}>{name}</span> <span title={caption}>{caption}</span>
</div> </div>
))} ))}
<video ref={mediaRef} playsInline disablePictureInPicture /> <video ref={mediaRef} playsInline disablePictureInPicture />

View File

@@ -25,10 +25,10 @@ import { useRoomMemberName } from "./useRoomMemberName";
import { VideoTile } from "./VideoTile"; import { VideoTile } from "./VideoTile";
import { VideoTileSettingsModal } from "./VideoTileSettingsModal"; import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
import { Participant } from "../room/InCallView"; import { TileDescriptor } from "../room/InCallView";
interface Props { interface Props {
item: Participant; item: TileDescriptor;
width?: number; width?: number;
height?: number; height?: number;
getAvatar: ( getAvatar: (
@@ -41,7 +41,7 @@ interface Props {
disableSpeakingIndicator: boolean; disableSpeakingIndicator: boolean;
maximised: boolean; maximised: boolean;
fullscreen: boolean; fullscreen: boolean;
onFullscreen: (item: Participant) => void; onFullscreen: (item: TileDescriptor) => void;
} }
export function VideoTileContainer({ export function VideoTileContainer({
@@ -65,11 +65,10 @@ export function VideoTileContainer({
speaking, speaking,
stream, stream,
purpose, purpose,
member,
} = useCallFeed(item.callFeed); } = useCallFeed(item.callFeed);
const { rawDisplayName } = useRoomMemberName(member); const { rawDisplayName } = useRoomMemberName(item.member);
const [tileRef, mediaRef] = useSpatialMediaStream( const [tileRef, mediaRef] = useSpatialMediaStream(
stream, stream ?? null,
audioContext, audioContext,
audioDestination, audioDestination,
isLocal, isLocal,
@@ -99,9 +98,10 @@ export function VideoTileContainer({
videoMuted={videoMuted} videoMuted={videoMuted}
screenshare={purpose === SDPStreamMetadataPurpose.Screenshare} screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
name={rawDisplayName} name={rawDisplayName}
hasFeed={Boolean(item.callFeed)}
ref={tileRef} ref={tileRef}
mediaRef={mediaRef} mediaRef={mediaRef}
avatar={getAvatar && getAvatar(member, width, height)} avatar={getAvatar && getAvatar(item.member, width, height)}
onOptionsPress={onOptionsPress} onOptionsPress={onOptionsPress}
localVolume={localVolume} localVolume={localVolume}
maximised={maximised} maximised={maximised}
@@ -109,7 +109,7 @@ export function VideoTileContainer({
onFullscreen={onFullscreenCallback} onFullscreen={onFullscreenCallback}
{...rest} {...rest}
/> />
{videoTileSettingsModalState.isOpen && !maximised && ( {videoTileSettingsModalState.isOpen && !maximised && item.callFeed && (
<VideoTileSettingsModal <VideoTileSettingsModal
{...videoTileSettingsModalProps} {...videoTileSettingsModalProps}
feed={item.callFeed} feed={item.callFeed}

View File

@@ -18,7 +18,7 @@ import { RefObject, useEffect } from "react";
export function useAudioOutputDevice( export function useAudioOutputDevice(
mediaRef: RefObject<MediaElement>, mediaRef: RefObject<MediaElement>,
audioOutputDevice: string audioOutputDevice: string | undefined
): void { ): void {
useEffect(() => { useEffect(() => {
if ( if (

View File

@@ -16,23 +16,23 @@ limitations under the License.
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed"; import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
interface CallFeedState { interface CallFeedState {
member: RoomMember; callFeed: CallFeed | undefined;
isLocal: boolean; isLocal: boolean;
speaking: boolean; speaking: boolean;
videoMuted: boolean; videoMuted: boolean;
audioMuted: boolean; audioMuted: boolean;
localVolume: number; localVolume: number;
disposed: boolean; disposed: boolean | undefined;
stream: MediaStream; stream: MediaStream | undefined;
purpose: SDPStreamMetadataPurpose; purpose: SDPStreamMetadataPurpose | undefined;
} }
function getCallFeedState(callFeed: CallFeed): CallFeedState {
function getCallFeedState(callFeed: CallFeed | undefined): CallFeedState {
return { return {
member: callFeed ? callFeed.getMember() : null, callFeed,
isLocal: callFeed ? callFeed.isLocal() : false, isLocal: callFeed ? callFeed.isLocal() : false,
speaking: callFeed ? callFeed.isSpeaking() : false, speaking: callFeed ? callFeed.isSpeaking() : false,
videoMuted: callFeed ? callFeed.isVideoMuted() : true, videoMuted: callFeed ? callFeed.isVideoMuted() : true,
@@ -44,7 +44,7 @@ function getCallFeedState(callFeed: CallFeed): CallFeedState {
}; };
} }
export function useCallFeed(callFeed: CallFeed): CallFeedState { export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
const [state, setState] = useState<CallFeedState>(() => const [state, setState] = useState<CallFeedState>(() =>
getCallFeedState(callFeed) getCallFeedState(callFeed)
); );

View File

@@ -17,27 +17,27 @@ limitations under the License.
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Participant } from "../room/InCallView"; import { TileDescriptor } from "../room/InCallView";
import { useEventTarget } from "../useEvents"; import { useEventTarget } from "../useEvents";
import { useCallFeed } from "./useCallFeed"; import { useCallFeed } from "./useCallFeed";
export function useFullscreen(ref: React.RefObject<HTMLElement>): { export function useFullscreen(ref: React.RefObject<HTMLElement>): {
toggleFullscreen: (participant: Participant) => void; toggleFullscreen: (participant: TileDescriptor) => void;
fullscreenParticipant: Participant | null; fullscreenParticipant: TileDescriptor | null;
} { } {
const [fullscreenParticipant, setFullscreenParticipant] = const [fullscreenParticipant, setFullscreenParticipant] =
useState<Participant | null>(null); useState<TileDescriptor | null>(null);
const { disposed } = useCallFeed(fullscreenParticipant?.callFeed); const { disposed } = useCallFeed(fullscreenParticipant?.callFeed);
const toggleFullscreen = useCallback( const toggleFullscreen = useCallback(
(participant: Participant) => { (tileDes: TileDescriptor) => {
if (fullscreenParticipant) { if (fullscreenParticipant) {
document.exitFullscreen(); document.exitFullscreen();
setFullscreenParticipant(null); setFullscreenParticipant(null);
} else { } else {
try { try {
ref.current.requestFullscreen(); ref.current.requestFullscreen();
setFullscreenParticipant(participant); setFullscreenParticipant(tileDes);
} catch (error) { } catch (error) {
console.warn("Failed to fullscreen:", error); console.warn("Failed to fullscreen:", error);
} }

View File

@@ -33,29 +33,39 @@ declare global {
} }
export const useMediaStreamTrackCount = ( export const useMediaStreamTrackCount = (
stream: MediaStream stream: MediaStream | null
): [number, number] => { ): [number, number] => {
const latestAudioTrackCount = stream ? stream.getAudioTracks().length : 0;
const latestVideoTrackCount = stream ? stream.getVideoTracks().length : 0;
const [audioTrackCount, setAudioTrackCount] = useState( const [audioTrackCount, setAudioTrackCount] = useState(
stream.getAudioTracks().length stream ? stream.getAudioTracks().length : 0
); );
const [videoTrackCount, setVideoTrackCount] = useState( const [videoTrackCount, setVideoTrackCount] = useState(
stream.getVideoTracks().length stream ? stream.getVideoTracks().length : 0
); );
const tracksChanged = useCallback(() => { const tracksChanged = useCallback(() => {
setAudioTrackCount(stream.getAudioTracks().length); setAudioTrackCount(stream ? stream.getAudioTracks().length : 0);
setVideoTrackCount(stream.getVideoTracks().length); setVideoTrackCount(stream ? stream.getVideoTracks().length : 0);
}, [stream]); }, [stream]);
useEventTarget(stream, "addtrack", tracksChanged); useEventTarget(stream, "addtrack", tracksChanged);
useEventTarget(stream, "removetrack", tracksChanged); useEventTarget(stream, "removetrack", tracksChanged);
if (
latestAudioTrackCount !== audioTrackCount ||
latestVideoTrackCount !== videoTrackCount
) {
tracksChanged();
}
return [audioTrackCount, videoTrackCount]; return [audioTrackCount, videoTrackCount];
}; };
export const useMediaStream = ( export const useMediaStream = (
stream: MediaStream, stream: MediaStream | null,
audioOutputDevice: string, audioOutputDevice: string | null,
mute = false, mute = false,
localVolume?: number localVolume?: number
): RefObject<MediaElement> => { ): RefObject<MediaElement> => {
@@ -76,7 +86,9 @@ export const useMediaStream = (
if (stream) { if (stream) {
mediaEl.muted = mute; mediaEl.muted = mute;
mediaEl.srcObject = stream; mediaEl.srcObject = stream;
mediaEl.play(); mediaEl.play().catch((e) => {
if (e.name !== "AbortError") throw e;
});
// Unmuting the tab in Safari causes all video elements to be individually // Unmuting the tab in Safari causes all video elements to be individually
// unmuted, so we need to reset the mute state here to prevent audio loops // unmuted, so we need to reset the mute state here to prevent audio loops
@@ -146,7 +158,7 @@ const createLoopback = async (stream: MediaStream): Promise<MediaStream> => {
await loopbackConn.setRemoteDescription(offer); await loopbackConn.setRemoteDescription(offer);
const answer = await loopbackConn.createAnswer(); const answer = await loopbackConn.createAnswer();
// Rewrite SDP to be stereo and (variable) max bitrate // Rewrite SDP to be stereo and (variable) max bitrate
const parsedSdp = parseSdp(answer.sdp); const parsedSdp = parseSdp(answer.sdp!);
parsedSdp.media.forEach((m) => parsedSdp.media.forEach((m) =>
m.fmtp.forEach( m.fmtp.forEach(
(f) => (f.config += `;stereo=1;cbr=0;maxaveragebitrate=510000;`) (f) => (f.config += `;stereo=1;cbr=0;maxaveragebitrate=510000;`)
@@ -194,11 +206,11 @@ export const useAudioContext = (): [
} }
}, []); }, []);
return [context.current, destination.current, audioRef]; return [context.current!, destination.current!, audioRef];
}; };
export const useSpatialMediaStream = ( export const useSpatialMediaStream = (
stream: MediaStream, stream: MediaStream | null,
audioContext: AudioContext, audioContext: AudioContext,
audioDestination: AudioNode, audioDestination: AudioNode,
mute = false, mute = false,
@@ -207,7 +219,7 @@ export const useSpatialMediaStream = (
const tileRef = useRef<HTMLDivElement>(); const tileRef = useRef<HTMLDivElement>();
const [spatialAudio] = useSpatialAudio(); const [spatialAudio] = useSpatialAudio();
// We always handle audio separately form the video element // We always handle audio separately form the video element
const mediaRef = useMediaStream(stream, undefined, true, undefined); const mediaRef = useMediaStream(stream, null, true);
const [audioTrackCount] = useMediaStreamTrackCount(stream); const [audioTrackCount] = useMediaStreamTrackCount(stream);
const gainNodeRef = useRef<GainNode>(); const gainNodeRef = useRef<GainNode>();
@@ -228,7 +240,7 @@ export const useSpatialMediaStream = (
}); });
} }
if (!sourceRef.current) { if (!sourceRef.current) {
sourceRef.current = audioContext.createMediaStreamSource(stream); sourceRef.current = audioContext.createMediaStreamSource(stream!);
} }
const tile = tileRef.current; const tile = tileRef.current;
@@ -240,12 +252,12 @@ export const useSpatialMediaStream = (
const bounds = tile.getBoundingClientRect(); const bounds = tile.getBoundingClientRect();
const windowSize = Math.max(window.innerWidth, window.innerHeight); const windowSize = Math.max(window.innerWidth, window.innerHeight);
// Position the source relative to its placement in the window // Position the source relative to its placement in the window
pannerNodeRef.current.positionX.value = pannerNodeRef.current!.positionX.value =
(bounds.x + bounds.width / 2) / windowSize - 0.5; (bounds.x + bounds.width / 2) / windowSize - 0.5;
pannerNodeRef.current.positionY.value = pannerNodeRef.current!.positionY.value =
(bounds.y + bounds.height / 2) / windowSize - 0.5; (bounds.y + bounds.height / 2) / windowSize - 0.5;
// Put the source in front of the listener // Put the source in front of the listener
pannerNodeRef.current.positionZ.value = -2; pannerNodeRef.current!.positionZ.value = -2;
}; };
gainNode.gain.value = localVolume; gainNode.gain.value = localVolume;

View File

@@ -30,6 +30,21 @@ export enum ElementWidgetActions {
HangupCall = "im.vector.hangup", HangupCall = "im.vector.hangup",
TileLayout = "io.element.tile_layout", TileLayout = "io.element.tile_layout",
SpotlightLayout = "io.element.spotlight_layout", SpotlightLayout = "io.element.spotlight_layout",
// Element Call -> host requesting to start a screenshare
// (ie. expects a ScreenshareStart once the user has picked a source)
// Element Call -> host requesting to start a screenshare
// (ie. expects a ScreenshareStart once the user has picked a source)
// replies with { pending } where pending is true if the host has asked
// the user to choose a window and false if not (ie. if the host isn't
// running within Electron)
ScreenshareRequest = "io.element.screenshare_request",
// host -> Element Call telling EC to start screen sharing with
// the given source
ScreenshareStart = "io.element.screenshare_start",
// host -> Element Call telling EC to stop screen sharing, or that
// the user cancelled when selecting a source after a ScreenshareRequest
ScreenshareStop = "io.element.screenshare_stop",
} }
export interface JoinCallData { export interface JoinCallData {
@@ -37,6 +52,10 @@ export interface JoinCallData {
videoInput: string | null; videoInput: string | null;
} }
export interface ScreenshareStartData {
desktopCapturerSourceId: string;
}
interface WidgetHelpers { interface WidgetHelpers {
api: WidgetApi; api: WidgetApi;
lazyActions: LazyEventEmitter; lazyActions: LazyEventEmitter;
@@ -68,6 +87,8 @@ export const widget: WidgetHelpers | null = (() => {
ElementWidgetActions.HangupCall, ElementWidgetActions.HangupCall,
ElementWidgetActions.TileLayout, ElementWidgetActions.TileLayout,
ElementWidgetActions.SpotlightLayout, ElementWidgetActions.SpotlightLayout,
ElementWidgetActions.ScreenshareStart,
ElementWidgetActions.ScreenshareStop,
].forEach((action) => { ].forEach((action) => {
api.on(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => { api.on(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault(); ev.preventDefault();
@@ -87,6 +108,7 @@ export const widget: WidgetHelpers | null = (() => {
if (!baseUrl) throw new Error("Base URL must be supplied"); if (!baseUrl) throw new Error("Base URL must be supplied");
// These are all the event types the app uses // These are all the event types the app uses
const sendRecvEvent = ["org.matrix.rageshake_request"];
const sendState = [ const sendState = [
{ eventType: EventType.GroupCallPrefix }, { eventType: EventType.GroupCallPrefix },
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId }, { eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
@@ -112,6 +134,8 @@ export const widget: WidgetHelpers | null = (() => {
const client = createRoomWidgetClient( const client = createRoomWidgetClient(
api, api,
{ {
sendEvent: sendRecvEvent,
receiveEvent: sendRecvEvent,
sendState, sendState,
receiveState, receiveState,
sendToDevice: sendRecvToDevice, sendToDevice: sendRecvToDevice,

View File

@@ -0,0 +1,46 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { render, RenderResult } from "@testing-library/react";
import { CallList } from "../../src/home/CallList";
import { MatrixClient } from "matrix-js-sdk";
import { GroupCallRoom } from "../../src/home/useGroupCallRooms";
import { MemoryRouter } from "react-router-dom";
import { ClientProvider } from "../../src/ClientContext";
describe("CallList", () => {
const renderComponent = (rooms: GroupCallRoom[]): RenderResult => {
return render(
<ClientProvider>
<MemoryRouter>
<CallList client={{} as MatrixClient} rooms={rooms} />
</MemoryRouter>
</ClientProvider>
);
};
it("should show room", async () => {
const rooms = [
{ roomName: "Room #1", roomId: "!roomId" },
] as GroupCallRoom[];
const result = renderComponent(rooms);
expect(result.queryByText("Room #1")).toBeTruthy();
});
});

1
test/mocks/olmMock.ts Normal file
View File

@@ -0,0 +1 @@
module.exports = { loadOlm: jest.fn(async () => {}) }

1
test/mocks/workerMock.ts Normal file
View File

@@ -0,0 +1 @@
module.exports = jest.fn();

View File

@@ -17,5 +17,10 @@
} }
] ]
}, },
"include": ["./src/**/*.ts", "./src/**/*.tsx"] "include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./test/**/*.ts",
"./test/**/*.tsx"
]
} }

View File

@@ -31,7 +31,7 @@ export default defineConfig(({ mode }) => {
svgrPlugin(), svgrPlugin(),
htmlTemplate.default({ htmlTemplate.default({
data: { data: {
title: env.VITE_PRODUCT_NAME || "Matrix Video Chat", title: env.VITE_PRODUCT_NAME || "Element Call",
}, },
}), }),
], ],

1873
yarn.lock

File diff suppressed because it is too large Load Diff