Compare commits

..

157 Commits

Author SHA1 Message Date
David Baker
b44ab47a1a Report more errors to sentry
Mostly from ErrorView
2023-07-21 11:58:21 +01:00
Robin
0d89f497aa Merge pull request #1264 from robintown/settings-tab-order
Put the settings tabs back in the right order
2023-07-21 00:40:26 -04:00
Robin Townsend
8903930d56 Respond to review feedback 2023-07-21 00:00:04 -04:00
Robin Townsend
922d66d66b Merge branch 'livekit' into settings-tab-order 2023-07-20 23:54:47 -04:00
David Baker
e4f2d86fcc Merge pull request #1182 from vector-im/dbkr/docker-use-container
Use docker container for docker compose file
2023-07-20 18:33:46 +01:00
David Baker
a311c6ea3d Merge pull request #1272 from vector-im/dbkr/debug_logs
Enable debug logging
2023-07-19 14:04:58 +01:00
David Baker
50ba23b7ea Revert unintentional change 2023-07-18 16:59:51 +01:00
Timo
b7f5069e98 set video enabled even if tracks are not yet initialized (#1273)
This could fix "muted on join issues" but could introduce issues where the buttons show unmuted even if no device is available.

Signed-off-by: Timo K <toger5@hotmail.de>
2023-07-18 17:41:54 +02:00
David Baker
b6c6a6215e Merge pull request #1271 from vector-im/dbkr/fix_embedded
Fix embedded mode
2023-07-18 13:25:24 +01:00
David Baker
4eb9326bd2 Enable debug logging
Fixes https://github.com/vector-im/element-call/issues/1267
2023-07-17 19:33:29 +01:00
David Baker
c15b2a62d5 Fix jsdoc 2023-07-17 19:22:22 +01:00
David Baker
f28cf0ce69 Fix embedded mode
This was trying to get the room alias, which causes the config to be
read. We don't need the room alias here though, so pass the flag to
not return it.
2023-07-17 19:16:28 +01:00
David Baker
05505a6177 Merge pull request #1263 from vector-im/dbkr/jwt-service-more-logging
Add more logs to JWT service code
2023-07-17 10:14:32 +01:00
Robin
b2be3575f2 Merge pull request #1265 from robintown/avatar-setting
Fix avatar field in profile settings being hidden
2023-07-15 20:43:40 -04:00
Robin
04d4d9af75 Merge pull request #1261 from robintown/fix-registration
Fix registration not logging you into the new account
2023-07-15 20:42:54 -04:00
Šimon Brandner
5d3eed033d Merge pull request #1177 from vector-im/SimonBrandner/feat/friendly-url 2023-07-15 09:51:54 +02:00
Šimon Brandner
723819b3d6 Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/friendly-url
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-15 09:48:08 +02:00
Robin Townsend
2d3d58cdc7 Fix avatar field in profile settings being hidden
It was being hidden when you didn't yet have an avatar set, which made it impossible to ever set one.
2023-07-14 14:48:50 -04:00
Robin Townsend
72eca769a4 Put the settings tabs back in the right order 2023-07-14 14:08:42 -04:00
David Baker
be340ca4e4 Add more logs to JWT service code 2023-07-14 18:42:31 +01:00
Robin Townsend
3c30a5812b Fix registration not logging you into the new account 2023-07-14 12:41:21 -04:00
Robin
8cee8c8779 Merge pull request #1252 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-07-13 11:11:47 -04:00
Jozef Gaal
606a4e4bfc Translated using Weblate (Slovak)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-07-13 14:16:27 +00:00
Jeff Huang
69426176aa Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-07-13 14:16:27 +00:00
Avery
5f23c3b9c6 Translated using Weblate (Spanish)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/es/
2023-07-13 14:16:27 +00:00
Priit Jõerüüt
ed3c11da5a Translated using Weblate (Estonian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-07-13 14:16:27 +00:00
Ihor Hordiichuk
873d852609 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-07-13 14:16:27 +00:00
Glandos
285ae01b1f Translated using Weblate (French)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-07-13 14:16:27 +00:00
Linerly
e36839f1ce Translated using Weblate (Indonesian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-07-13 14:16:27 +00:00
Vri
64becad7ed Translated using Weblate (German)
Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-07-13 14:16:27 +00:00
Timo
cac06f9852 Remove unecassary state for lobby device initialization (#1238)
* remove unecassary state

Signed-off-by: Timo K <toger5@hotmail.de>

* hotfix

Signed-off-by: Timo K <toger5@hotmail.de>

* remove video/audioAvailableAndEnabled
this is not required anymore since we disable the button.

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-07-13 11:20:24 +02:00
David Baker
a3ce8384db Merge pull request #1250 from vector-im/dbkr/merge_hotfixes
Merge v0.4.2 hotfixes
2023-07-12 18:11:48 +01:00
David Baker
e264a71d1e Merge v0.4.2 hotfixes 2023-07-12 17:57:54 +01:00
Robin
89c326f0bb Merge pull request #1240 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-07-12 11:51:14 -04:00
Daniel Abramov
eab2bc8ab5 Merge pull request #1173 from vector-im/lint-fixes
Enable stricter lints
2023-07-12 17:18:33 +02:00
Daniel Abramov
9820773136 A hack to fix problems with Matrix JS SDK
See https://github.com/matrix-org/matrix-js-sdk/pull/3593#issuecomment-1632725177
for more context on why it was necessary.
2023-07-12 16:11:01 +01:00
David Baker
e4a96c8c0b Merge pull request #1246 from vector-im/dbkr/jwt_url
Update JWT service URL
2023-07-12 15:16:06 +01:00
Šimon Brandner
bb2b60951e Merge pull request #1243 from vector-im/SimonBrandner/other/basic-ssl 2023-07-12 16:08:59 +02:00
David Baker
d081dc0013 Update JWT service URL 2023-07-12 15:01:16 +01:00
Šimon Brandner
62d4084dcf Add support for accessing the web server from other devices
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-12 13:55:21 +02:00
Weblate
439fb23470 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/
2023-07-11 23:00:16 +00:00
David Baker
e8404d3514 Merge pull request #1239 from vector-im/dbkr/sfo
Spell SFU correctly
2023-07-11 18:19:21 +01:00
Daniel Abramov
c87e06244e Update matrix-js-sdk to fix crypto issues 2023-07-11 17:46:17 +01:00
David Baker
8754651f5c Spell SFU correctly 2023-07-11 16:49:29 +01:00
Daniel Abramov
6bbd92a2bf Fix missing i18n 2023-07-11 16:02:58 +01:00
Daniel Abramov
f3de341a4b Remove unnecessary usage of a roomIdOrAlias
In most cases the `roomId` was used despite the name of the variable.
2023-07-11 16:02:58 +01:00
Daniel Abramov
7f9157bae1 Fix lint errors that originate from JS SDK 2023-07-11 16:02:58 +01:00
Daniel Abramov
6d6e88c234 Fix unit tests after the lints 2023-07-11 16:02:58 +01:00
Daniel Abramov
91352fd08d Disable a lint for any for now in UserMenu 2023-07-11 16:02:58 +01:00
Daniel Abramov
c9edfd9df4 Fix the CI pipeline command used for linting 2023-07-11 16:02:58 +01:00
Daniel Abramov
5c559bdca6 Re-generate i18 2023-07-11 16:02:58 +01:00
Daniel Abramov
aaa9b7761c Use widget with ! instead of ?
It cannot be `null` in the mentioned contexts as discussed with Robin.
2023-07-11 16:02:58 +01:00
Daniel Abramov
259ef27bd0 Properly model the state of a group call load 2023-07-11 16:02:58 +01:00
Daniel Abramov
647599a39e Use useMemo in a proper way in ClientContext 2023-07-11 16:02:58 +01:00
Daniel Abramov
6998555ad9 Show UserMenu even if no avatar available 2023-07-11 16:02:58 +01:00
Daniel Abramov
b68cd54049 Properly use sizeToClassName for a LinkButton 2023-07-11 16:02:58 +01:00
Daniel Abramov
2293cbf1f0 Use memoized callbacks when passing to components 2023-07-11 16:02:58 +01:00
Daniel Abramov
caa3a7e8d0 Make useProfile() use of undefined explicit 2023-07-11 16:02:58 +01:00
Daniel Abramov
333b7cc041 Use more concise syntax for optional function call 2023-07-11 16:02:58 +01:00
Daniel Abramov
e6047f8583 Remove redundant code when resolving an avatar 2023-07-11 16:02:58 +01:00
Daniel Abramov
0105162ffa Enable strict lints
An attempt to fix https://github.com/vector-im/element-call/issues/1132
2023-07-11 16:02:58 +01:00
Šimon Brandner
d86d3de95e Merge pull request #1232 from vector-im/SimonBrandner/feat/volume-control 2023-07-11 16:14:54 +02:00
Šimon Brandner
968a62be62 Merge pull request #1237 from vector-im/SimonBrandner/other/update-lk-cli 2023-07-11 16:03:59 +02:00
Šimon Brandner
a5e921876f Update livekit-client
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-11 15:59:47 +02:00
David Baker
28f06bdab3 Merge pull request #1234 from vector-im/dbkr/js_sdk_bump_getopenidtoken
Bump js-sdk for getOpenIdToken() in embedded mode
2023-07-11 13:30:15 +01:00
David Baker
68635313d4 Bump js-sdk for getOpenIdToken() in embedded mode
For https://github.com/matrix-org/matrix-js-sdk/pull/3571
2023-07-11 13:24:10 +01:00
Šimon Brandner
b0bafb1904 Merge pull request #1230 from vector-im/SimonBrandner/fix/settings-overflow 2023-07-11 12:08:54 +02:00
Šimon Brandner
08aebf05e9 Delint
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-11 12:08:06 +02:00
Šimon Brandner
7994d09590 Add user-media volume control
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-11 12:04:01 +02:00
Šimon Brandner
cb6174cf7d Fix settings tab list scroll-bars
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-11 10:52:27 +02:00
David Baker
46ad46323e Merge pull request #1225 from vector-im/dbkr/lk_url_matroyshka
Add the livekit URL in matroyshka mode
2023-07-11 09:48:01 +01:00
Šimon Brandner
8f8659726a Merge pull request #1223 from vector-im/SimonBrandner/fix/muted-audio 2023-07-10 18:01:50 +02:00
David Baker
f6383fb4b5 Add comment 2023-07-10 16:04:06 +01:00
David Baker
4c6535243d Fix types 2023-07-10 15:54:04 +01:00
Šimon Brandner
b814efc21f Add a comment
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 16:51:33 +02:00
Šimon Brandner
1b7d1e87c7 Merge pull request #1226 from vector-im/SimonBrandner/fix/e2ee 2023-07-10 16:05:16 +02:00
Šimon Brandner
316968e6ea i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 16:02:58 +02:00
Šimon Brandner
c0a2c18ec4 Fix E2EE disclaimer
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 15:58:46 +02:00
David Baker
ca0e843d80 Add the livekit URL in matroyshka mode
This is a bit of a hack, as commented. The way to make it less of a
hack is there too.

Fixes embedded mode, although users will get a permission prompt
without https://github.com/matrix-org/matrix-react-sdk/pull/11209.

Fixes https://github.com/vector-im/element-call/issues/1201
2023-07-10 13:36:07 +01:00
Šimon Brandner
cc2808a1da Fix audio being muted when joining a call
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 14:27:19 +02:00
Šimon Brandner
d367921db9 Merge pull request #1216 from vector-im/SimonBrandner/feat/settings-embed-prof 2023-07-10 12:55:33 +02:00
Element Translate Bot
14ce19872d Translations update from Weblate (#1213)
* Translated using Weblate (German)

Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (118 of 118 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/

---------

Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
2023-07-10 12:40:30 +02:00
Šimon Brandner
aa885b4158 Merge pull request #1217 from vector-im/SimonBrandner/feat/screenshare-audio 2023-07-10 12:36:02 +02:00
Timo
7b19980a83 Disable mute button if media is not yet available. (#1218)
---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-07-10 12:26:47 +02:00
Šimon Brandner
a3220afc32 Merge pull request #1219 from vector-im/SimonBrandner/feat/video-res 2023-07-10 12:00:59 +02:00
Šimon Brandner
0e8aa9dea6 Adjust simulcast options for video
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 11:55:12 +02:00
Šimon Brandner
91e8d559b9 Support screen-sharing audio
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 11:27:06 +02:00
Šimon Brandner
9ecbfbffc7 Don't show profile settings in embedded mode
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-10 11:13:57 +02:00
Robin
df04dc6065 Merge pull request #1206 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-07-07 13:33:24 -04:00
Šimon Brandner
3c560ed126 Merge pull request #1209 from vector-im/SimonBrandner/feat/lk-url-fallback 2023-07-07 18:18:58 +02:00
Daniel Abramov
5c5aa27a20 Merge pull request #1207 from vector-im/release-preparations
Release preparations
2023-07-07 16:10:33 +02:00
Šimon Brandner
124c6223e4 Fallback to config LiveKit URL if there is no URL coming from the js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-07 16:08:30 +02:00
Robin
2eb548de9f Merge pull request #1192 from robintown/expand-collapse-inverse
Make the expand and collapse interactions inverses of one another
2023-07-07 09:36:25 -04:00
Daniel Abramov
19a2494b9b Fix screen-sharing on Chrome
- Disable temporal layers
- Make 30 fps screen-sharing
2023-07-07 14:27:01 +01:00
Timo
9be9250124 Combined permission request with newer livekit sdk version (#1200)
---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-07-07 14:41:29 +02:00
Šimon Brandner
cc95ed7c30 Merge pull request #1208 from vector-im/SimonBrandner/fix/js-v 2023-07-07 14:22:14 +02:00
Šimon Brandner
a6496626da Fix js-sdk version
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-07 14:17:09 +02:00
Daniel Abramov
008d22a2a0 Use unique and stable room names for LiveKit
Fixes #1165.
2023-07-07 12:36:53 +01:00
Weblate
70c4edc761 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/
2023-07-07 10:37:04 +00:00
Jozef Gaal
f3c12f98cc Translated using Weblate (Slovak)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-07-07 10:37:04 +00:00
Jeff Huang
8ddb1c4201 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-07-07 10:37:04 +00:00
Ihor Hordiichuk
e466270cb1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-07-07 10:37:04 +00:00
Glandos
f48314bd9b Translated using Weblate (French)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-07-07 10:37:04 +00:00
Linerly
23b1a28790 Translated using Weblate (Indonesian)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-07-07 10:37:04 +00:00
Šimon Brandner
e194d56894 Merge pull request #1199 from vector-im/SimonBrandner/feat/eula-config 2023-07-07 11:01:19 +02:00
Šimon Brandner
3effa330f1 Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/eula-config
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-07 10:57:53 +02:00
Šimon Brandner
bb1206dd2f Merge pull request #1193 from vector-im/SimonBrandner/feat/e2ee-banner 2023-07-07 10:53:40 +02:00
Šimon Brandner
7a47d0504d Size improvement numero dos
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-07 10:14:04 +02:00
Šimon Brandner
b9e15ab992 Fix sizing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-07 10:11:27 +02:00
Šimon Brandner
e52b3e6d53 Add EULA config
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 18:48:25 +02:00
Robin
44ce140f54 Merge pull request #1197 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-07-06 11:20:17 -04:00
Robin
e481a34383 Merge pull request #1191 from robintown/better-double-click
Improve the double click detection
2023-07-06 10:35:18 -04:00
Linerly
31fcf0634f Translated using Weblate (Indonesian)
Currently translated at 98.2% (115 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-07-06 12:02:22 +00:00
Weblate
d70374119f 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/
2023-07-06 10:17:06 +00:00
Dimitris Vagiakakos
faeb2ae395 Translated using Weblate (Greek)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/el/
2023-07-06 10:17:05 +00:00
Theo
2aae25d3b5 Translated using Weblate (Greek)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/el/
2023-07-06 10:17:05 +00:00
Šimon Brandner
4c62759a6f Merge pull request #1196 from vector-im/SimonBrandner/fix/useEffect 2023-07-06 12:15:56 +02:00
Šimon Brandner
3d57d63f7f Don't unnecessarily use useEffect
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 12:12:28 +02:00
Šimon Brandner
607e1b7b43 Fix getRoomUrl()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 12:02:39 +02:00
Šimon Brandner
c7258776c3 Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/friendly-url 2023-07-06 11:59:13 +02:00
Šimon Brandner
c8bf869367 Merge pull request #1194 from vector-im/SimonBrandner/feat/eula 2023-07-06 11:37:43 +02:00
Šimon Brandner
3b49fa079b i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 11:36:34 +02:00
Šimon Brandner
4a90a6d64c Add E2EE lock
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 11:10:43 +02:00
Šimon Brandner
3cef00b6b6 Add E2EE banner
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 11:10:00 +02:00
Šimon Brandner
0646f327df Change from TOC to EULA
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 08:43:00 +02:00
Šimon Brandner
0d72e3ae9e Add LockOff icon
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 08:36:18 +02:00
Šimon Brandner
a63dc637ab Add subtle primary color
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-06 08:35:53 +02:00
Robin
77c698f8ed Merge pull request #1185 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-07-06 01:03:59 -04:00
Robin Townsend
3ac98c8865 Make the expand and collapse interactions inverses of one another
For the most part, at least. If the edge cases where they differ still feel weird, I can iterate on this further.

The diff is unfortunately a bit impenetrable, because I had to change both the fillGaps and cycleTileSize core algorithms used by the big grid layout. But: the main change of significance is the addition of a function vacateArea, which clears out an area within the grid in a specific way that mirrors the motion performed by fillGaps.
2023-07-06 00:59:14 -04:00
Robin Townsend
1c7110e4c9 Improve the double click detection
So that it doesn't cause unnecessary renders, and interprets a series of three clicks as a double-click followed by a single click, rather than two overlapping double-clicks. (That behavior felt odd to me during testing of NewVideoGrid, which is why I picked up this small change.)
2023-07-06 00:40:15 -04:00
raspin0
12f1d32934 Translated using Weblate (Polish)
Currently translated at 100.0% (117 of 117 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-07-05 15:47:43 +00:00
David Baker
aab08d2bf1 Remove trailing slash from livekit service URL 2023-07-05 13:45:06 +01:00
David Baker
1f02f14db6 Merge pull request #1188 from vector-im/dbkr/new_lk_svc_url
New livekit service URL
2023-07-05 13:38:51 +01:00
David Baker
41371de506 New livekit service URL 2023-07-05 13:33:33 +01:00
Šimon Brandner
5e8cad0fc7 Merge pull request #1159 from vector-im/SimonBrandner/feat/url 2023-07-05 14:29:01 +02:00
David Baker
dda9163a1e Merge pull request #1187 from vector-im/dbkr/fix_cd
Fix netlify deployment
2023-07-05 13:28:53 +01:00
David Baker
c73c4d171d Fix netlify deployment
Turns out the create deployment stateb really needs the env,
even though it's empty.
2023-07-05 13:22:57 +01:00
Šimon Brandner
e7234ef8be Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/url 2023-07-05 14:20:05 +02:00
David Baker
5c8d976029 Merge pull request #1186 from vector-im/revert-1183-revert-1166-dbkr/openid
Re-apply "Support for getting SFU config using OIDC"
2023-07-05 13:17:12 +01:00
David Baker
130b70756a Revert "Revert "Support for getting SFU config using OIDC"" 2023-07-05 13:12:37 +01:00
David Baker
0bcec1efbd Merge pull request #1184 from vector-im/dbkr/update_more_branch_names
Update the livekit branch name in more places
2023-07-05 09:04:21 +01:00
David Baker
fe96c4a455 Update the livekit branch name in more places
Fixes the CI to get the config from the right place.

Also removes reference to an env that's already been deleted.
2023-07-04 20:25:51 +01:00
David Baker
077f5b9510 Merge pull request #1183 from vector-im/revert-1166-dbkr/openid
Revert "Support for getting SFU config using OIDC"
2023-07-04 19:24:53 +01:00
David Baker
29553c1151 Revert "Support for getting SFU config using OIDC" 2023-07-04 19:21:35 +01:00
Šimon Brandner
d67ce2e051 Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/url 2023-07-04 20:13:09 +02:00
Šimon Brandner
792067ef22 Update js-sd
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-04 17:44:41 +02:00
Šimon Brandner
ce1671a117 Merge remote-tracking branch 'upstream/dbkr/openid' into SimonBrandner/feat/url
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-04 17:43:40 +02:00
Šimon Brandner
26657a5de9 Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/url 2023-07-04 16:26:54 +02:00
Šimon Brandner
8fbcc06cd8 Don't use the whole Location
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 20:05:08 +02:00
Šimon Brandner
0bba620451 Further id vs alias
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 19:32:25 +02:00
Šimon Brandner
b08044c06d More fixes of id vs alias
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 19:30:38 +02:00
Šimon Brandner
e1ef18093f Fix alias vs id + participants bug
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 19:23:26 +02:00
Šimon Brandner
fdc41aaa50 Use friendly URLs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 16:20:19 +02:00
Šimon Brandner
11785fc243 Avoid redirects
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 15:40:06 +02:00
Šimon Brandner
f0c9a26242 Add jest-mock
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-07-03 14:03:29 +02:00
Šimon Brandner
30ba12ccb8 Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-06-27 17:31:54 +02:00
Šimon Brandner
3a2cee581e Get LK info from the js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-06-27 17:28:50 +02:00
120 changed files with 2747 additions and 1465 deletions

View File

@@ -25,3 +25,4 @@ LIVEKIT_SECRET="secret"
# VITE_THEME_SYSTEM=#21262c
# VITE_THEME_BACKGROUND=#15191e
# VITE_THEME_BACKGROUND_85=#15191ed9
# VITE_THEME_SUBTLE_PRIMARY=#26282D

View File

@@ -1,6 +1,16 @@
module.exports = {
plugins: ["matrix-org"],
extends: ["plugin:matrix-org/react", "plugin:matrix-org/a11y", "prettier"],
extends: [
"prettier",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:matrix-org/typescript",
],
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
project: ["./tsconfig.json"],
},
env: {
browser: true,
node: true,

View File

@@ -19,6 +19,6 @@ jobs:
- name: i18n
run: "yarn run i18n:check"
- name: ESLint
run: "yarn run lint:js"
run: "yarn run lint:eslint"
- name: Type check
run: "yarn run lint:types"

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
deployments: write
# Important: the 'branches' filter above will match the 'livekit-experiment' branch on forks,
# Important: the 'branches' filter above will match the 'livekit' branch on forks,
# so we need to check the head repo too in order to not run on PRs from forks
# We check the branch name again too just for completeness
# (Is there a nicer way to see if a PR is from a fork?)
@@ -53,10 +53,10 @@ jobs:
- name: Add redirects file
# We fetch from github directly as we don't bother checking out the repo
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/livekit-experiment/config/netlify_redirects > dist/_redirects
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/livekit/config/netlify_redirects > dist/_redirects
- name: Add config file
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/livekit-experiment/config/element_io_preview.json > dist/config.json
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/livekit/config/element_io_preview.json > dist/config.json
- name: Deploy to Netlify
id: netlify

View File

@@ -4,5 +4,6 @@
"base_url": "https://call.ems.host",
"server_name": "call.ems.host"
}
}
},
"eula": "https://static.element.io/legal/online-EULA.pdf"
}

View File

@@ -6,7 +6,7 @@
}
},
"livekit": {
"livekit_service_url": "https://lk-jwt-service.lab.element.dev"
"livekit_service_url": "https://livekit-jwt.call.element.dev"
},
"posthog": {
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",

View File

@@ -9,8 +9,9 @@
"build-storybook": "build-storybook",
"prettier:check": "prettier -c .",
"prettier:format": "prettier -w .",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src",
"lint": "yarn lint:types && yarn lint:eslint",
"lint:eslint": "eslint --max-warnings 0 src",
"lint:eslint-fix": "eslint --max-warnings 0 src --fix",
"lint:types": "tsc",
"i18n": "node_modules/i18next-parser/bin/cli.js",
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
@@ -19,7 +20,7 @@
},
"dependencies": {
"@juggle/resize-observer": "^3.3.1",
"@livekit/components-react": "^1.0.3",
"@livekit/components-react": "^1.0.7",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/context-zone": "^1.9.1",
@@ -46,8 +47,10 @@
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@types/grecaptcha": "^3.0.4",
"@types/react-router-dom": "^5.3.3",
"@types/sdp-transform": "^2.4.5",
"@use-gesture/react": "^10.2.11",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1",
"classnames": "^2.3.1",
"color-hash": "^2.0.1",
@@ -55,9 +58,9 @@
"i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"livekit-client": "^1.9.7",
"livekit-client": "1.12.0",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#af10b0c44b4a427c8d2224bfd6feb03a12cfd27e",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
@@ -69,13 +72,15 @@
"react-dom": "18",
"react-i18next": "^11.18.6",
"react-json-view": "^1.21.3",
"react-router": "6",
"react-router-dom": "^5.2.0",
"react-use-clipboard": "^1.0.7",
"react-use-measure": "^2.1.1",
"sdp-transform": "^2.14.1",
"tinyqueue": "^2.0.3",
"unique-names-generator": "^4.6.0"
"unique-names-generator": "^4.6.0",
"uuid": "9",
"@types/uuid": "9",
"@types/content-type": "^1.1.5"
},
"devDependencies": {
"@babel/core": "^7.16.5",
@@ -101,6 +106,7 @@
"identity-obj-proxy": "^3.0.0",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.3.1",
"jest-mock": "^29.5.0",
"prettier": "^2.6.2",
"sass": "^1.42.1",
"storybook-builder-vite": "^0.1.12",

View File

@@ -5,8 +5,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.",
"Audio": "Звук",
"Avatar": "Аватар",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия</2>",
"Call link copied": "Връзка към разговора бе копирана",
"Call type menu": "Меню \"тип на разговора\"",
"Camera": "Камера",
@@ -75,7 +73,6 @@
"Take me Home": "Отиди в Начало",
"Thanks! We'll get right on it.": "Благодарим! Веднага ще се заемем.",
"This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Този сайт се предпазва от ReCAPTCHA и важат <2>Политиката за поверителност</2> и <6>Условията за ползване на услугата</6> на Google.<9></9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия</12>",
"Turn off camera": "Изключи камерата",
"Turn on camera": "Включи камерата",
"Unmute microphone": "Включи микрофона",

View File

@@ -62,7 +62,6 @@
"Inspector": "Insepktor",
"Incompatible versions!": "Nekompatibilní verze!",
"Incompatible versions": "Nekompatibilní verze",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Tato stárnka je chráněna pomocí ReCAPTCHA a Google <2>zásad ochrany osobních údajů</2> a <6>podmínky služby</6> platí.<9></9>Kliknutím na \"Registrovat\", souhlasíte s <12>Pravidly a podmínkami</12>",
"Walkie-talkie call name": "Jméno vysílačkového hovoru",
"Walkie-talkie call": "Vysílačkový hovor",
"{{names}}, {{name}}": "{{names}}, {{name}}",
@@ -90,8 +89,6 @@
"Create account": "Vytvořit účet",
"Copy": "Kopírovat",
"Call type menu": "Menu typu hovoru",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions</2>",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?</0><1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory </1>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Připojit se</0><1>Or</1><2>Zkopírovat odkaz a připojit se později</2>",

View File

@@ -5,8 +5,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.",
"Audio": "Audio",
"Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Los 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 „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
"Call link copied": "Anruflink kopiert",
"Call type menu": "Anruftyp Menü",
"Camera": "Kamera",
@@ -74,7 +72,6 @@
"Take me Home": "Zurück zur Startseite",
"Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.",
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Diese Website wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6> von Google.<9></9>Indem Du auf „Registrieren“ klickst, stimmst du unseren <12>Geschäftsbedingungen</12> zu",
"Turn off camera": "Kamera ausschalten",
"Turn on camera": "Kamera einschalten",
"Unmute microphone": "Mikrofon aktivieren",
@@ -115,5 +112,9 @@
"{{count}} stars|one": "{{count}} Stern",
"<0>Thanks for your feedback!</0>": "<0>Danke für deine Rückmeldung!</0>",
"{{displayName}} is presenting": "{{displayName}} präsentiert",
"Show connection stats": "Verbindungsstatistiken zeigen"
"Show connection stats": "Verbindungsstatistiken zeigen",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Los gehts“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen."
}

View File

@@ -11,7 +11,7 @@
"Remove": "Αφαίρεση",
"Registering…": "Εγγραφή…",
"Not registered yet? <2>Create an account</2>": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό</2>",
"Login to your account": "Συνδεθείτε στο λογαριασμό σας",
"Login to your account": "Συνδεθείτε στον λογαριασμό σας",
"Logging in…": "Σύνδεση…",
"Invite people": "Προσκαλέστε άτομα",
"Invite": "Πρόσκληση",
@@ -71,5 +71,46 @@
"Close": "Κλείσιμο",
"Change layout": "Αλλαγή διάταξης",
"Camera": "Κάμερα",
"Audio": "Ήχος"
"Audio": "Ήχος",
"Send debug logs": "Αποστολή αρχείων καταγραφής",
"Recaptcha dismissed": "Το recaptcha απορρίφθηκε",
"<0>Thanks for your feedback!</0>": "<0>Ευχαριστώ για τα σχόλιά σας!</0>",
"Call type menu": "Μενού είδους κλήσης",
"Local volume": "Τοπική ένταση",
"Home": "Αρχική",
"Show connection stats": "Εμφάνιση στατιστικών σύνδεσης",
"Unmute microphone": "Κατάργηση σίγασης μικροφώνου",
"Take me Home": "Μετάβαση στην Αρχική",
"{{displayName}} is presenting": "{{displayName}} παρουσιάζει",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Συμμετοχή στην κλήση τώρα</0><1>Or</1><2>Αντιγραφή συνδέσμου κλήσης και συμμετοχή αργότερα</2>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Θα θέλαμε να ακούσουμε τα σχόλιά σας ώστε να βελτιώσουμε την εμπειρία σας.</0>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Γιατί να μην ολοκληρώσετε με τη δημιουργία ενός κωδικού πρόσβασης για τη διατήρηση του λογαριασμού σας;</0><1>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.</1>",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ένας άλλος χρήστης σε αυτή την κλήση έχει ένα πρόβλημα. Για την καλύτερη διάγνωση αυτών των προβλημάτων θα θέλαμε να συλλέξουμε ένα αρχείο καταγραφής σφαλμάτων.",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Συμμετέχοντας σε αυτή τη δοκιμαστική έκδοση, συναινείτε στη συλλογή ανώνυμων δεδομένων, τα οποία χρησιμοποιούμε για τη βελτίωση του προϊόντος. Μπορείτε να βρείτε περισσότερες πληροφορίες σχετικά με το ποια δεδομένα καταγράφουμε στην <2>Πολιτική απορρήτου</2> και στην <5>Πολιτική cookies</5>.",
"Grid layout menu": "Μενού διάταξης πλέγματος",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}</1>",
"Thanks! We'll get right on it.": "Ευχαριστούμε! Θα το ερευνήσουμε αμέσως.",
"Expose developer settings in the settings window.": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
"Feedback": "Ανατροφοδότηση",
"Submitting…": "Υποβολή…",
"Thanks, we received your feedback!": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!",
"{{count}} stars|other": "{{count}} αστέρια",
"{{count}} stars|one": "{{count}} αστέρι",
"{{displayName}}, your call has ended.": "{{displayName}}, η κλήση σας τερματίστηκε.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.</0>",
"How did it go?": "Πώς σας φάνηκε;",
"Include debug logs": "Να συμπεριληφθούν αρχεία καταγραφής",
"Recaptcha not loaded": "Το Recaptcha δεν φορτώθηκε",
"Debug log": "Αρχείο καταγραφής",
"Developer": "Προγραμματιστής",
"Download debug logs": "Λήψη αρχείων καταγραφής",
"Sending debug logs…": "Αποστολή αρχείων καταγραφής…",
"Submit": "Υποβολή",
"Your feedback": "Τα σχόλιά σας",
"Fetching group call timed out.": "Η ομαδική κλήση έληξε από τέλος χρόνου.",
"Freedom": "Ελευθερία",
"Spotlight": "Spotlight",
"Element Call Home": "Element Κεντρική Οθόνη Κλήσεων"
}

View File

@@ -16,8 +16,8 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.",
"Audio": "Audio",
"Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "By clicking \"Go\", you agree to our <2>Terms and conditions</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.",
"Call link copied": "Call link copied",
"Call type menu": "Call type menu",
@@ -37,6 +37,7 @@
"Display name": "Display name",
"Download debug logs": "Download debug logs",
"Element Call Home": "Element Call Home",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
"Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback",
@@ -99,7 +100,7 @@
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.",
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>",
"This 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>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
"Turn off camera": "Turn off camera",
"Turn on camera": "Turn on camera",
"Unmute microphone": "Unmute microphone",

View File

@@ -6,8 +6,6 @@
"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",
"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>",
"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>",
"Yes, join call": "Si, unirse a la llamada",
"Walkie-talkie call name": "Nombre de la llamada Walkie-talkie",
"Walkie-talkie call": "Llamada Walkie-talkie",
@@ -21,7 +19,6 @@
"Unmute microphone": "Desilenciar el micrófono",
"Turn on camera": "Encender la cámara",
"Turn off camera": "Apagar la cámara",
"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.",
"Take me Home": "Volver al inicio",
"Submit feedback": "Enviar comentarios",
@@ -101,5 +98,23 @@
"Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
"Developer Settings": "Ajustes de desarrollador",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Al participar en esta beta, consientes a la recogida de datos anónimos, los cuales usaremos para mejorar el producto. Puedes encontrar más información sobre que datos recogemos en nuestra <2>Política de privacidad</2> y en nuestra <5>Política sobre Cookies</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta."
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta.",
"{{displayName}} is presenting": "{{displayName}} está presentando",
"<0>Thanks for your feedback!</0>": "<0>¡Gracias por tus comentarios!</0>",
"How did it go?": "¿Cómo ha ido?",
"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>End User Licensing Agreement (EULA)</12>": "Este sitio está protegido por ReCAPTCHA y se aplican la <2>Política de Privacidad</2> y los <6>Términos de Servicio de Google.<9></9>Al hacer clic en \"Registrar\", aceptas nuestro <12>Contrato de Licencia de Usuario Final (CLUF)</12>",
"Show connection stats": "Mostrar estadísticas de conexión",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call no está encriptado de extremo a extremo de manera temporal mientras probamos la escalabilidad del servicio.",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Al hacer clic en \"Comenzar\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
"Thanks, we received your feedback!": "¡Gracias, hemos recibido tus comentarios!",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Nos encantaría conocer tu opinión para que podamos mejorar tu experiencia</0>",
"Feedback": "Danos tu opinión",
"Submit": "Enviar",
"{{count}} stars|one": "{{count}} estrella",
"{{count}} stars|other": "{{count}} estrellas",
"{{displayName}}, your call has ended.": "{{displayName}}, tu llamada ha finalizado.",
"Submitting…": "Enviando…",
"Your feedback": "Tus comentarios"
}

View File

@@ -32,14 +32,12 @@
"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.",
"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 registered yet? <2>Create an account</2>": "Sa pole veel registreerunud? <2>Loo kasutajakonto</2>",
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
"No": "Ei",
"Mute microphone": "Summuta mikrofon",
@@ -53,7 +51,7 @@
"Loading…": "Laadimine …",
"Leave": "Lahku",
"Join existing call?": "Liitu juba käimasoleva kõnega?",
"Join call now": "Kõnega liitumine",
"Join call now": "Liitu kõnega kohe",
"Join call": "Kõnega liitumine",
"Turn on camera": "Lülita kaamera sisse",
"Turn off camera": "Lülita kaamera välja",
@@ -93,7 +91,6 @@
"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 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>",
"Element Call Home": "Element Call Home",
"Copy": "Kopeeri",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.</0>",
@@ -115,5 +112,9 @@
"<0>Thanks for your feedback!</0>": "<0>Täname Sind tagasiside eest!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Meie rakenduse paremaks muutmiseks me hea meelega ootame Sinu arvamusi.</0>",
"Show connection stats": "Näita ühenduse statistikat",
"{{displayName}} is presenting": "{{displayName}} on esitlemas"
"{{displayName}} is presenting": "{{displayName}} on esitlemas",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika</2> ning <6>Teenusetingimused</6>.<9></9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Seni kuni me testime skaleeritavust, siis Element Call ajutiselt pole läbivalt krüptitud."
}

View File

@@ -45,8 +45,6 @@
"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.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیب‌یابی را جمع‌آوری کنیم.",
@@ -88,7 +86,6 @@
"Version: {{version}}": "نسخه: {{نسخه}}",
"User menu": "فهرست کاربر",
"Unmute microphone": "ناخموشی میکروفون",
"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.": "با تشکر! ما به درستی آن را انجام خواهیم داد.",
"Submit feedback": "بازخورد ارائه دهید",

View File

@@ -4,8 +4,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.",
"Audio": "Audio",
"Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Commencer » vous acceptez nos <2>conditions dutilisation</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Rejoindre lappel » vous acceptez nos <2>conditions dutilisation</2>",
"Call link copied": "Lien de lappel copié",
"Call type menu": "Menu de type dappel",
"Camera": "Caméra",
@@ -88,7 +86,6 @@
"Unmute microphone": "Allumer le micro",
"Turn on camera": "Allumer la caméra",
"Turn off camera": "Couper la caméra",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions dutilisation</6> de Google sappliquent.<9></9>En cliquant sur « Senregistrer » vous acceptez également nos <12>conditions dutilisation</12>",
"Speaker": "Intervenant",
"Invite": "Inviter",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
@@ -115,5 +112,9 @@
"<0>Thanks for your feedback!</0>": "<0>Merci pour votre commentaire !</0>",
"How did it go?": "Comment cela sest-il passé ?",
"{{displayName}} is presenting": "{{displayName}} est à lécran",
"Show connection stats": "Afficher les statistiques de la connexion"
"Show connection stats": "Afficher les statistiques de la connexion",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "En cliquant sur « Rejoindre lappel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "En cliquant sur « Commencer », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions dutilisation</6> de Google sappliquent.<9></9>En cliquant sur « Senregistrer » vous acceptez également notre <12>Contrat de Licence Utilisateur Final (CLUF)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nest temporairement plus chiffré de bout en bout le temps de tester lextensibilité."
}

View File

@@ -5,8 +5,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.",
"Audio": "Audio",
"Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan</2> kami",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan</2> kami",
"Call link copied": "Tautan panggilan disalin",
"Call type menu": "Menu jenis panggilan",
"Camera": "Kamera",
@@ -75,7 +73,6 @@
"Take me Home": "Bawa saya ke Beranda",
"Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.",
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan</12> kami",
"Turn off camera": "Matikan kamera",
"Turn on camera": "Nyalakan kamera",
"Unmute microphone": "Suarakan mikrofon",
@@ -93,7 +90,7 @@
"Your recent calls": "Panggilan Anda terkini",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"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>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung ke panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
"Element Call Home": "Beranda Element Call",
"Copy": "Salin",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.</0>",
@@ -105,7 +102,7 @@
"Feedback": "Masukan",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
"Submit": "Kirim",
"Submitting…": "Mengirim",
"Submitting…": "Mengirim",
"Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!",
"Your feedback": "Masukan Anda",
"{{displayName}}, your call has ended.": "{{displayName}}, panggilan Anda telah berakhir.",
@@ -115,5 +112,9 @@
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Kami ingin mendengar masukan Anda supaya kami bisa meningkatkan pengalaman Anda.</0>",
"Show connection stats": "Tampilkan statistik koneksi",
"{{displayName}} is presenting": "{{displayName}} sedang menampilkan",
"{{count}} stars|other": "{{count}} bintang"
"{{count}} stars|other": "{{count}} bintang",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Situs ini dilindungi oleh reCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9></9>Dengan mengeklik \"Daftar\", Anda menyetujui <12>Perjanjian Lisensi Pengguna Akhir (EULA)</12> kami",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call sementara tidak dienkripsi secara ujung ke ujung selagi kami menguji skalabilitas."
}

View File

@@ -5,8 +5,6 @@
"<0>Oops, something's gone wrong.</0>": "<0>何かがうまく行きませんでした。</0>",
"Camera": "カメラ",
"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": "音声",
"Confirm password": "パスワードを確認",

View File

@@ -1,7 +1,6 @@
{
"Login": "Zaloguj się",
"Go": "Przejdź",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Zasady i warunki</2>",
"Your recent calls": "Twoje ostatnie połączenia",
"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.",
@@ -84,7 +83,6 @@
"Camera": "Kamera",
"Call type menu": "Menu typu 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>Zasady i 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.",
@@ -99,7 +97,6 @@
"Expose developer settings in the settings window.": "Wyświetl opcje programisty w oknie ustawień.",
"Element Call Home": "Strona główna Element Call",
"Developer Settings": "Opcje programisty",
"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>": "Ta strona jest chroniona przez ReCAPTCHA, więc obowiązują na niej <2>Polityka prywatności</2> i <6>Warunki świadczenia usług</6> Google.<9></9>Klikając \"Zarejestruj się\", zgadzasz się na nasze <12>Warunki świadczenia usług</12>",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Uczestnicząc w tej becie, upoważniasz nas do zbierania anonimowych danych, które wykorzystamy do ulepszenia produktu. Dowiedz się więcej na temat danych, które zbieramy w naszej <2>Polityce prywatności</2> i <5>Polityce ciasteczek</5>.",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.",
@@ -113,5 +110,7 @@
"{{displayName}}, your call has ended.": "{{displayName}}, Twoje połączenie zostało zakończone.",
"<0>Thanks for your feedback!</0>": "<0>Dziękujemy za Twoją opinię!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Z przyjemnością wysłuchamy Twojej opinii, aby poprawić Twoje doświadczenia.</0>",
"How did it go?": "Jak poszło?"
"How did it go?": "Jak poszło?",
"{{displayName}} is presenting": "{{displayName}} prezentuje",
"Show connection stats": "Pokaż statystyki połączenia"
}

View File

@@ -4,7 +4,6 @@
"Logging in…": "Вход…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"Waiting for other participants…": "Ожидание других участников…",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Этот сайт защищён ReCAPTCHA от Google, ознакомьтесь с их <2>Политикой конфиденциальности</2> и <6>Пользовательским соглашением</6>.<9></9>Нажимая \"Зарегистрироваться\", вы также принимаете наши <12>Положения и условия</12>.",
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
"Submit feedback": "Отправить отзыв",
@@ -12,8 +11,6 @@
"Select an option": "Выберите вариант",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Другие пользователи пытаются присоединиться с неподдерживаемых версий программы. Этим участникам надо перезагрузить браузер: <1>{userLis}</1>",
"Grid layout menu": "Меню \"Расположение сеткой\"",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Присоединиться сейчас\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>",

View File

@@ -65,7 +65,6 @@
"Unmute microphone": "Zrušiť stlmenie mikrofónu",
"Turn on camera": "Zapnúť kameru",
"Turn off camera": "Vypnúť kameru",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov</2> a <6>Podmienky poskytovania služieb</6> spoločnosti Google.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" vyjadrujete súhlas s našimi <12>Podmienkami poskytovania služieb</12>",
"This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?",
"Speaker": "Reproduktor",
"Sign out": "Odhlásiť sa",
@@ -86,8 +85,6 @@
"Camera": "Kamera",
"Call type menu": "Ponuka typu hovoru",
"Call link copied": "Odkaz na hovor skopírovaný",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pripojiť sa k hovoru\" súhlasíte s našimi <2>Podmienkami</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na tlačidlo \"Prejsť\" súhlasíte s našimi <2>Podmienkami</2>",
"Avatar": "Obrázok",
"Audio": "Audio",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.",
@@ -115,5 +112,9 @@
"<0>Thanks for your feedback!</0>": "<0> Ďakujeme za vašu spätnú väzbu!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0> Radi si vypočujeme vašu spätnú väzbu, aby sme mohli zlepšiť vaše skúsenosti.</0>",
"{{displayName}} is presenting": "{{displayName}} prezentuje",
"Show connection stats": "Zobraziť štatistiky pripojenia"
"Show connection stats": "Zobraziť štatistiky pripojenia",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google</2> a <6>Podmienky poskytovania služieb</6>.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" súhlasíte s našou <12>Licenčnou zmluvou s koncovým používateľom (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nie je dočasne šifrovaný, kým testujeme škálovateľnosť."
}

View File

@@ -3,8 +3,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.",
"Audio": "Ses",
"Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "\"Git\"e tıklayarak,<2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
"Call link copied": "Arama bağlantısı kopyalandı",
"Call type menu": "Arama tipi menüsü",
"Camera": "Kamera",

View File

@@ -15,7 +15,6 @@
"Unmute microphone": "Увімкнути мікрофон",
"Turn on camera": "Увімкнути камеру",
"Turn off camera": "Вимкнути камеру",
"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.": "Дякуємо! Ми зараз же візьмемося за це.",
"Take me Home": "Перейти до Домівки",
@@ -84,8 +83,6 @@
"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.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал налагодження.",
@@ -115,5 +112,9 @@
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.</0>",
"How did it go?": "Вам усе сподобалось?",
"{{displayName}} is presenting": "{{displayName}} представляє",
"Show connection stats": "Показати стан з'єднання"
"Show connection stats": "Показати стан з'єднання",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Натискаючи \"Далі\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи \"Зареєструватися\", ви погоджуєтеся з нашою <12>Ліцензійною угодою з кінцевим користувачем (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Виклики Element тимчасово не захищаються наскрізним шифруванням, поки ми тестуємо масштабованість."
}

View File

@@ -76,7 +76,6 @@
"This call already exists, would you like to join?": "Cuộc gọi đã tồn tại, bạn có muốn tham gia không?",
"Recaptcha not loaded": "Chưa tải được Recaptcha",
"Debug log request": "Yêu cầu nhật ký gỡ lỗi",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Khi nhấn vào \"Tham gia cuộc gọi\", bạn đồng ý với <2>Điều khoản và điều kiện</2> của chúng tôi",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Một người dùng khác trong cuộc gọi đang gặp vấn đề. Để có thể chẩn đoán tốt hơn chúng tôi muốn thu thập nhật ký gỡ lỗi.",
"<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>Tại sao lại không hoàn thiện bằng cách đặt mật khẩu để giữ tài khoản của bạn?</0><1>Bạn sẽ có thể giữ tên và đặt ảnh đại diện cho những cuộc gọi tiếp theo.</1>",
"<0>Oops, something's gone wrong.</0>": "<0>Ối, có cái gì đó sai.</0>",

View File

@@ -14,7 +14,6 @@
"Unmute microphone": "取消麦克风静音",
"Turn on camera": "开启摄像头",
"Turn off camera": "关闭摄像头",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "本网站受reCaptcha保护并适用Google<2>隐私政策</2>和<6>服务条款</6>。<9></9>点击\"注册\"则表明您同意我们的<12>条款和条件</12>",
"This call already exists, would you like to join?": "该通话已存在,你想加入吗?",
"Thanks! We'll get right on it.": "谢谢!我们会马上去做的。",
"Take me Home": "返回主页",
@@ -89,12 +88,10 @@
"Copied!": "已复制!",
"Confirm password": "确认密码",
"Close": "关闭",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "点击开始则代表同意我们的<2>条款和条件<2>",
"Change layout": "更改布局",
"Camera": "摄像头",
"Call type menu": "通话类型菜单",
"Call link copied": "链接已复制",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "点击“现在加入”则表示同意我们的<2>条款与条件<2>",
"Avatar": "头像",
"<0>Oops, something's gone wrong.</0>": "<0>哎哟,出问题了。</0>",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": ""

View File

@@ -22,7 +22,6 @@
"Unmute microphone": "取消麥克風靜音",
"Turn on camera": "開啟相機",
"Turn off camera": "關閉相機",
"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>": "此網站使用Google 驗證碼技術保護,適用<2>隱私條款</2> 與<6>條款與細則</6> 。<9></9>按下「註冊」,表示您同意我們的<12>條款與細則</12>",
"This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?",
"Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。",
"Take me Home": "帶我回主畫面",
@@ -94,8 +93,6 @@
"Camera": "相機",
"Call type menu": "通話類型選單",
"Call link copied": "已複製通話連結",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "當您按下「加入通話」,您也同時同意了我們的條款與細則",
"By clicking \"Go\", you agree to our <2>Terms and conditions</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.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。",
@@ -115,5 +112,9 @@
"{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。",
"How did it go?": "進展如何?",
"{{displayName}} is presenting": "{{displayName}} 正在展示",
"Show connection stats": "顯示連線統計資料"
"Show connection stats": "顯示連線統計資料",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "點擊「前往」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "此網站被 ReCAPTCHA 保護,並適用 Google 的<2>隱私權政策</2>與<6>服務條款</6>。<9></9>點擊「註冊」即表示您同意我們的<12>終端使用者授權協議 (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "在我們測試可擴展性時Element Call 暫時未進行端到端加密。"
}

View File

@@ -18,12 +18,12 @@ import { Suspense, useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as Sentry from "@sentry/react";
import { OverlayProvider } from "@react-aria/overlays";
import { History } from "history";
import { HomePage } from "./home/HomePage";
import { LoginPage } from "./auth/LoginPage";
import { RegisterPage } from "./auth/RegisterPage";
import { RoomPage } from "./room/RoomPage";
import { RoomRedirect } from "./room/RoomRedirect";
import { ClientProvider } from "./ClientContext";
import { usePageFocusStyle } from "./usePageFocusStyle";
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
@@ -51,6 +51,8 @@ export default function App({ history }: AppProps) {
const errorPage = <CrashView />;
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Router history={history}>
{loaded ? (
<Suspense fallback={null}>
@@ -68,14 +70,11 @@ export default function App({ history }: AppProps) {
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
<RoomPage />
</SentryRoute>
</Switch>
</OverlayProvider>

View File

@@ -16,7 +16,6 @@ limitations under the License.
import { useMemo, CSSProperties, HTMLAttributes, FC } from "react";
import classNames from "classnames";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { getAvatarUrl } from "./matrix-utils";
import { useClient } from "./ClientContext";
@@ -59,9 +58,6 @@ function hashStringToArrIndex(str: string, arrLength: number) {
return sum % arrLength;
}
const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
interface Props extends HTMLAttributes<HTMLDivElement> {
bgKey?: string;
src?: string;
@@ -99,10 +95,10 @@ export const Avatar: FC<Props> = ({
[size]
);
const resolvedSrc = useMemo(
() => resolveAvatarSrc(client, src, sizePx),
[client, src, sizePx]
);
const resolvedSrc = useMemo(() => {
if (!client || !src || !sizePx) return undefined;
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
}, [client, src, sizePx]);
const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex(

22
src/Banner.module.css Normal file
View File

@@ -0,0 +1,22 @@
/*
Copyright 2023 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.
*/
.banner {
flex: 1;
border-radius: 8px;
padding: 16px;
background-color: var(--subtle-primary);
}

27
src/Banner.tsx Normal file
View File

@@ -0,0 +1,27 @@
/*
Copyright 2023 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 { ReactNode } from "react";
import styles from "./Banner.module.css";
interface Props {
children: ReactNode;
}
export const Banner = ({ children }: Props) => {
return <div className={styles.banner}>{children}</div>;
};

View File

@@ -20,9 +20,9 @@ import {
useEffect,
useState,
createContext,
useMemo,
useContext,
useRef,
useMemo,
} from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
@@ -31,9 +31,9 @@ import { useTranslation } from "react-i18next";
import { ErrorView } from "./FullScreenView";
import {
initClient,
CryptoStoreIntegrityError,
fallbackICEServerAllowed,
initClient,
} from "./matrix-utils";
import { widget } from "./widget";
import {
@@ -47,173 +47,141 @@ import { Config } from "./config/Config";
declare global {
interface Window {
matrixclient: MatrixClient;
isPasswordlessUser: boolean;
passwordlessUser: boolean;
}
}
export interface Session {
user_id: string;
device_id: string;
access_token: string;
export type ClientState = ValidClientState | ErrorState;
export type ValidClientState = {
state: "valid";
authenticated?: AuthenticatedClient;
setClient: (params?: SetClientParams) => void;
};
export type AuthenticatedClient = {
client: MatrixClient;
isPasswordlessUser: boolean;
changePassword: (password: string) => Promise<void>;
logout: () => void;
};
export type ErrorState = {
state: "error";
error: Error;
};
export type SetClientParams = {
client: MatrixClient;
session: Session;
};
const ClientContext = createContext<ClientState | undefined>(undefined);
export const useClientState = () => useContext(ClientContext);
export function useClient(): {
client?: MatrixClient;
setClient?: (params?: SetClientParams) => void;
} {
let client;
let setClient;
const clientState = useClientState();
if (clientState?.state === "valid") {
client = clientState.authenticated?.client;
setClient = clientState.setClient;
}
return { client, setClient };
}
// Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields.
export function useClientLegacy(): {
client?: MatrixClient;
setClient?: (params?: SetClientParams) => void;
passwordlessUser: boolean;
tempPassword?: string;
loading: boolean;
authenticated: boolean;
logout?: () => void;
error?: Error;
} {
const clientState = useClientState();
let client;
let setClient;
let passwordlessUser = false;
let loading = true;
let error;
let authenticated = false;
let logout;
if (clientState?.state === "valid") {
client = clientState.authenticated?.client;
setClient = clientState.setClient;
passwordlessUser = clientState.authenticated?.isPasswordlessUser ?? false;
loading = false;
authenticated = client !== undefined;
logout = clientState.authenticated?.logout;
} else if (clientState?.state === "error") {
error = clientState.error;
loading = false;
}
return {
client,
setClient,
passwordlessUser,
loading,
authenticated,
logout,
error,
};
}
const loadChannel =
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
const loadSession = (): Session => {
const data = localStorage.getItem("matrix-auth-store");
if (data) return JSON.parse(data);
return null;
};
const saveSession = (session: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
const clearSession = () => localStorage.removeItem("matrix-auth-store");
interface ClientState {
loading: boolean;
isAuthenticated: boolean;
isPasswordlessUser: boolean;
client: MatrixClient;
userName: string;
changePassword: (password: string) => Promise<void>;
logout: () => void;
setClient: (client: MatrixClient, session: Session) => void;
error?: Error;
}
const ClientContext = createContext<ClientState>(null);
type ClientProviderState = Omit<
ClientState,
"changePassword" | "logout" | "setClient"
> & { error?: Error };
interface Props {
children: JSX.Element;
}
export const ClientProvider: FC<Props> = ({ children }) => {
const history = useHistory();
const initializing = useRef(false);
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState,
] = useState<ClientProviderState>({
loading: true,
isAuthenticated: false,
isPasswordlessUser: false,
client: undefined,
userName: null,
error: undefined,
});
const [initClientState, setInitClientState] = useState<
InitResult | undefined
>(undefined);
const initializing = useRef(false);
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
// the client.
if (initializing.current) return;
initializing.current = true;
const init = async (): Promise<
Pick<ClientProviderState, "client" | "isPasswordlessUser">
> => {
if (widget) {
// We're inside a widget, so let's engage *matryoshka mode*
logger.log("Using a matryoshka client");
return {
client: await widget.client,
isPasswordlessUser: false,
};
} else {
// We're running as a standalone application
try {
const session = loadSession();
if (!session) return { client: undefined, isPasswordlessUser: false };
logger.log("Using a standalone client");
/* eslint-disable camelcase */
const { user_id, device_id, access_token, passwordlessUser } =
session;
try {
return {
client: await initClient(
{
baseUrl: Config.defaultHomeserverUrl(),
accessToken: access_token,
userId: user_id,
deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed,
},
true
),
isPasswordlessUser: passwordlessUser,
};
} catch (err) {
if (err instanceof CryptoStoreIntegrityError) {
// We can't use this session anymore, so let's log it out
try {
const client = await initClient(
{
baseUrl: Config.defaultHomeserverUrl(),
accessToken: access_token,
userId: user_id,
deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed,
},
false // Don't need the crypto store just to log out
);
await client.logout(true);
} catch (err_) {
logger.warn(
"The previous session was lost, and we couldn't log it out, " +
"either"
);
}
}
throw err;
}
/* eslint-enable camelcase */
} catch (err) {
clearSession();
throw err;
loadClient()
.then((maybeClient) => {
if (!maybeClient) {
logger.error("Failed to initialize client");
return;
}
}
};
init()
.then(({ client, isPasswordlessUser }) => {
setState({
client,
loading: false,
isAuthenticated: Boolean(client),
isPasswordlessUser,
userName: client?.getUserIdLocalpart(),
error: undefined,
});
})
.catch((err) => {
logger.error(err);
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
error: undefined,
});
setInitClientState(maybeClient);
})
.catch((err) => logger.error(err))
.finally(() => (initializing.current = false));
}, []);
const changePassword = useCallback(
async (password: string) => {
const { tempPassword, ...session } = loadSession();
const session = loadSession();
if (!initClientState?.client || !session) {
return;
}
await client.setPassword(
await initClientState.client.setPassword(
{
type: "m.login.password",
identifier: {
@@ -221,73 +189,56 @@ export const ClientProvider: FC<Props> = ({ children }) => {
user: session.user_id,
},
user: session.user_id,
password: tempPassword,
password: session.tempPassword,
},
password
);
saveSession({ ...session, passwordlessUser: false });
setState({
client,
loading: false,
isAuthenticated: true,
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
error: undefined,
setInitClientState({
client: initClientState.client,
passwordlessUser: false,
});
},
[client]
[initClientState?.client]
);
const setClient = useCallback(
(newClient: MatrixClient, session: Session) => {
if (client && client !== newClient) {
client.stopClient();
(clientParams?: SetClientParams) => {
const oldClient = initClientState?.client;
const newClient = clientParams?.client;
if (oldClient && oldClient !== newClient) {
oldClient.stopClient();
}
if (newClient) {
saveSession(session);
setState({
client: newClient,
loading: false,
isAuthenticated: true,
isPasswordlessUser: session.passwordlessUser,
userName: newClient.getUserIdLocalpart(),
error: undefined,
if (clientParams) {
saveSession(clientParams.session);
setInitClientState({
client: clientParams.client,
passwordlessUser: clientParams.session.passwordlessUser,
});
} else {
clearSession();
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
error: undefined,
});
setInitClientState(undefined);
}
},
[client]
[initClientState?.client]
);
const logout = useCallback(async () => {
const client = initClientState?.client;
if (!client) {
return;
}
await client.logout(true);
await client.clearStores();
clearSession();
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: true,
userName: "",
error: undefined,
});
setInitClientState(undefined);
history.push("/");
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
}, [history, client]);
}, [history, initClientState?.client]);
const { t } = useTranslation();
@@ -299,61 +250,140 @@ export const ClientProvider: FC<Props> = ({ children }) => {
if (!widget) loadChannel?.postMessage({});
}, []);
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
undefined
);
useEventTarget(
loadChannel,
"message",
useCallback(() => {
client?.stopClient();
setState((prev) => ({
...prev,
error: translatedError(
"This application has been opened in another tab.",
t
),
}));
}, [client, setState, t])
initClientState?.client.stopClient();
setAlreadyOpenedErr(
translatedError("This application has been opened in another tab.", t)
);
}, [initClientState?.client, setAlreadyOpenedErr, t])
);
const context = useMemo<ClientState>(
() => ({
loading,
isAuthenticated,
isPasswordlessUser,
client,
changePassword,
logout,
userName,
setClient,
error: undefined,
}),
[
loading,
isAuthenticated,
isPasswordlessUser,
client,
changePassword,
logout,
userName,
setClient,
]
);
const state: ClientState = useMemo(() => {
if (alreadyOpenedErr) {
return { state: "error", error: alreadyOpenedErr };
}
let authenticated = undefined;
if (initClientState) {
authenticated = {
client: initClientState.client,
isPasswordlessUser: initClientState.passwordlessUser,
changePassword,
logout,
};
}
return { state: "valid", authenticated, setClient };
}, [alreadyOpenedErr, changePassword, initClientState, logout, setClient]);
useEffect(() => {
window.matrixclient = client;
window.isPasswordlessUser = isPasswordlessUser;
if (!initClientState) {
return;
}
window.matrixclient = initClientState.client;
window.passwordlessUser = initClientState.passwordlessUser;
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
}, [client, isPasswordlessUser]);
}, [initClientState]);
if (error) {
return <ErrorView error={error} />;
if (alreadyOpenedErr) {
return <ErrorView error={alreadyOpenedErr} />;
}
return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
<ClientContext.Provider value={state}>{children}</ClientContext.Provider>
);
};
export const useClient = () => useContext(ClientContext);
type InitResult = {
client: MatrixClient;
passwordlessUser: boolean;
};
async function loadClient(): Promise<InitResult> {
if (widget) {
// We're inside a widget, so let's engage *matryoshka mode*
logger.log("Using a matryoshka client");
const client = await widget.client;
return {
client,
passwordlessUser: false,
};
} else {
// We're running as a standalone application
try {
const session = loadSession();
if (!session) {
throw new Error("No session stored");
}
logger.log("Using a standalone client");
/* eslint-disable camelcase */
const { user_id, device_id, access_token, passwordlessUser } = session;
const initClientParams = {
baseUrl: Config.defaultHomeserverUrl()!,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed,
livekitServiceURL: Config.get().livekit!.livekit_service_url,
};
try {
const client = await initClient(initClientParams, true);
return {
client,
passwordlessUser,
};
} catch (err) {
if (err instanceof CryptoStoreIntegrityError) {
// We can't use this session anymore, so let's log it out
try {
const client = await initClient(initClientParams, false); // Don't need the crypto store just to log out)
await client.logout(true);
} catch (err) {
logger.warn(
"The previous session was lost, and we couldn't log it out, " +
err +
"either"
);
}
}
throw err;
}
/* eslint-enable camelcase */
} catch (err) {
clearSession();
throw err;
}
}
}
export interface Session {
user_id: string;
device_id: string;
access_token: string;
passwordlessUser: boolean;
tempPassword?: string;
}
const clearSession = () => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
const loadSession = (): Session | undefined => {
const data = localStorage.getItem("matrix-auth-store");
if (!data) {
return undefined;
}
return JSON.parse(data);
};

23
src/E2EEBanner.module.css Normal file
View File

@@ -0,0 +1,23 @@
/*
Copyright 2023 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.
*/
.e2eeBanner {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: var(--font-size-caption);
}

35
src/E2EEBanner.tsx Normal file
View File

@@ -0,0 +1,35 @@
/*
Copyright 2023 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 { Trans } from "react-i18next";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
export const E2EEBanner = () => {
return (
<Banner>
<div className={styles.e2eeBanner}>
<LockOffIcon width={24} height={24} />
<Trans>
Element Call is temporarily not end-to-end encrypted while we test
scalability.
</Trans>
</div>
</Banner>
);
};

28
src/E2EELock.module.css Normal file
View File

@@ -0,0 +1,28 @@
/*
Copyright 2023 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.
*/
.e2eeLock {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 8px;
border-radius: 100%;
background-color: var(--subtle-primary);
}

58
src/E2EELock.tsx Normal file
View File

@@ -0,0 +1,58 @@
/*
Copyright 2023 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 { useTranslation } from "react-i18next";
import { useCallback } from "react";
import { useObjectRef } from "@react-aria/utils";
import { useButton } from "@react-aria/button";
import styles from "./E2EELock.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { TooltipTrigger } from "./Tooltip";
export const E2EELock = () => {
const { t } = useTranslation();
const tooltip = useCallback(
() =>
t(
"Element Call is temporarily not end-to-end encrypted while we test scalability."
),
[t]
);
return (
<TooltipTrigger placement="right" tooltip={tooltip}>
<Icon />
</TooltipTrigger>
);
};
/**
* This component is a bit of hack - for some reason for the TooltipTrigger to
* work, it needs to contain a component which uses the useButton hook; please
* note that for some reason this also needs to be a separate component and we
* cannot just use the useButton hook inside the E2EELock.
*/
const Icon = () => {
const buttonRef = useObjectRef<HTMLDivElement>();
const { buttonProps } = useButton({}, buttonRef);
return (
<div ref={buttonRef} className={styles.e2eeLock} {...buttonProps}>
<LockOffIcon />
</div>
);
};

View File

@@ -47,8 +47,8 @@ export function Facepile({
}: Props) {
const { t } = useTranslation();
const _size = sizes.get(size);
const _overlap = overlapMap[size];
const _size = sizes.get(size)!;
const _overlap = overlapMap[size]!;
const title = useMemo(() => {
return members.reduce<string | null>(

View File

@@ -18,6 +18,7 @@ import { ReactNode, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom";
import classNames from "classnames";
import { Trans, useTranslation } from "react-i18next";
import * as Sentry from "@sentry/react";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import { LinkButton, Button } from "./button";
@@ -58,6 +59,7 @@ export function ErrorView({ error }: ErrorViewProps) {
useEffect(() => {
console.error(error);
Sentry.captureException(error);
}, [error]);
const onReload = useCallback(() => {

View File

@@ -36,15 +36,16 @@ export function ListBox<T>({
listBoxRef,
...rest
}: ListBoxProps<T>) {
const ref = useRef<HTMLUListElement>();
if (!listBoxRef) listBoxRef = ref;
const ref = useRef<HTMLUListElement>(null);
const { listBoxProps } = useListBox(rest, state, listBoxRef);
const listRef = listBoxRef ?? ref;
const { listBoxProps } = useListBox(rest, state, listRef);
return (
<ul
{...listBoxProps}
ref={listBoxRef}
ref={listRef}
className={classNames(styles.listBox, className)}
>
{[...state.collection].map((item) => (
@@ -66,7 +67,7 @@ interface OptionProps<T> {
}
function Option<T>({ item, state, className }: OptionProps<T>) {
const ref = useRef();
const ref = useRef(null);
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key },
state,
@@ -83,7 +84,11 @@ function Option<T>({ item, state, className }: OptionProps<T>) {
const origPointerUp = optionProps.onPointerUp;
delete optionProps.onPointerUp;
optionProps.onClick = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
origPointerUp(e as unknown as PointerEvent<HTMLElement>);
},
[origPointerUp]

View File

@@ -26,7 +26,7 @@ import styles from "./Menu.module.css";
interface MenuProps<T> extends AriaMenuOptions<T> {
className?: String;
onClose?: () => void;
onClose: () => void;
onAction: (value: Key) => void;
label?: string;
}
@@ -39,7 +39,7 @@ export function Menu<T extends object>({
...rest
}: MenuProps<T>) {
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
const menuRef = useRef();
const menuRef = useRef(null);
const { menuProps } = useMenu<T>(rest, state, menuRef);
return (
@@ -69,7 +69,7 @@ interface MenuItemProps<T> {
}
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
const ref = useRef();
const ref = useRef(null);
const { menuItemProps } = useMenuItem(
{
key: item.key,

View File

@@ -55,7 +55,7 @@ export function Modal({
...rest
}: ModalProps) {
const { t } = useTranslation();
const modalRef = useRef();
const modalRef = useRef(null);
const { overlayProps, underlayProps } = useOverlay(
{ ...rest, onClose },
modalRef
@@ -63,7 +63,7 @@ export function Modal({
usePreventScroll();
const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog(rest, modalRef);
const closeButtonRef = useRef();
const closeButtonRef = useRef(null);
const { buttonProps: closeButtonProps } = useButton(
{
onPress: () => onClose(),

View File

@@ -36,6 +36,9 @@ export function SequenceDiagramViewerPage() {
const [debugLog, setDebugLog] = useState<DebugLog>();
const [selectedUserId, setSelectedUserId] = useState<string>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const onChangeDebugLog = useCallback((e) => {
if (e.target.files && e.target.files.length > 0) {
e.target.files[0].text().then((text: string) => {
@@ -55,7 +58,7 @@ export function SequenceDiagramViewerPage() {
onChange={onChangeDebugLog}
/>
</FieldRow>
{debugLog && (
{debugLog && selectedUserId && (
<SequenceDiagramViewer
localUserId={debugLog.localUserId}
selectedUserId={selectedUserId}

View File

@@ -74,7 +74,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
const tooltipTriggerProps = { delay: 250, ...rest };
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
const triggerRef = useObjectRef<HTMLElement>(ref);
const overlayRef = useRef();
const overlayRef = useRef<HTMLDivElement>(null);
const { triggerProps, tooltipProps } = useTooltipTrigger(
tooltipTriggerProps,
tooltipState,

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 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.
@@ -17,6 +17,8 @@ limitations under the License.
import { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { Config } from "./config/Config";
interface UrlParams {
roomAlias: string | null;
roomId: string | null;
@@ -88,19 +90,46 @@ interface UrlParams {
/**
* Gets the app parameters for the current URL.
* @param query The URL query string
* @param fragment The URL fragment string
* @param ignoreRoomAlias If true, does not try to parse a room alias from the URL
* @param search The URL search string
* @param pathname The URL path name
* @param hash The URL hash
* @returns The app parameters encoded in the URL
*/
export const getUrlParams = (
query: string = window.location.search,
fragment: string = window.location.hash
ignoreRoomAlias?: boolean,
search = window.location.search,
pathname = window.location.pathname,
hash = window.location.hash
): UrlParams => {
const fragmentQueryStart = fragment.indexOf("?");
let roomAlias: string | undefined;
if (!ignoreRoomAlias) {
if (hash === "") {
roomAlias = pathname.substring(1); // Strip the "/"
// Delete "/room/" and "?", if present
if (roomAlias.startsWith("room/")) {
roomAlias = roomAlias.substring("room/".length);
}
// Add "#", if not present
if (!roomAlias.startsWith("#")) {
roomAlias = `#${roomAlias}`;
}
} else {
roomAlias = hash;
}
// Add server part, if not present
if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
}
}
const fragmentQueryStart = hash.indexOf("?");
const fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
);
const queryParams = new URLSearchParams(query);
const queryParams = new URLSearchParams(search);
// Normally, URL params should be encoded in the fragment so as to avoid
// leaking them to the server. However, we also check the normal query
@@ -114,16 +143,10 @@ export const getUrlParams = (
...queryParams.getAll(name),
];
// The part of the fragment before the ?
const fragmentRoute =
fragmentQueryStart === -1
? fragment
: fragment.substring(0, fragmentQueryStart);
const fontScale = parseFloat(getParam("fontScale") ?? "");
return {
roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null,
roomAlias: !roomAlias || roomAlias.includes("!") ? null : roomAlias,
roomId: getParam("roomId"),
viaServers: getAllParams("via"),
isEmbedded: hasParam("embed"),
@@ -149,6 +172,9 @@ export const getUrlParams = (
* @returns The app parameters for the current URL
*/
export const useUrlParams = (): UrlParams => {
const { hash, search } = useLocation();
return useMemo(() => getUrlParams(search, hash), [search, hash]);
const { search, pathname, hash } = useLocation();
return useMemo(
() => getUrlParams(false, search, pathname, hash),
[search, pathname, hash]
);
};

View File

@@ -36,7 +36,7 @@ interface UserMenuProps {
isAuthenticated: boolean;
isPasswordlessUser: boolean;
displayName: string;
avatarUrl: string;
avatarUrl?: string;
onAction: (value: string) => void;
}
@@ -119,21 +119,24 @@ export function UserMenu({
)}
</Button>
</TooltipTrigger>
{(props) => (
<Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label, dataTestid }) => (
<Item key={key} textValue={label}>
<Icon
width={24}
height={24}
className={styles.menuIcon}
data-testid={dataTestid}
/>
<Body overflowEllipsis>{label}</Body>
</Item>
))}
</Menu>
)}
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(props: any) => (
<Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label, dataTestid }) => (
<Item key={key} textValue={label}>
<Icon
width={24}
height={24}
className={styles.menuIcon}
data-testid={dataTestid}
/>
<Body overflowEllipsis>{label}</Body>
</Item>
))}
</Menu>
)
}
</PopoverMenuTrigger>
);
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useClient } from "./ClientContext";
import { useClientLegacy } from "./ClientContext";
import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./settings/SettingsModal";
@@ -30,8 +30,7 @@ interface Props {
export function UserMenuContainer({ preventNavigation = false }: Props) {
const location = useLocation();
const history = useHistory();
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
useClient();
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
@@ -49,7 +48,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
modalState.open();
break;
case "logout":
logout();
logout?.();
break;
case "login":
history.push("/login", { state: { from: location } });
@@ -59,19 +58,18 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
[history, location, logout, modalState]
);
const userName = client?.getUserIdLocalpart() ?? "";
return (
<>
<UserMenu
preventNavigation={preventNavigation}
isAuthenticated={isAuthenticated}
isPasswordlessUser={isPasswordlessUser}
isAuthenticated={authenticated}
isPasswordlessUser={passwordlessUser}
avatarUrl={avatarUrl}
onAction={onAction}
displayName={
displayName || (userName ? userName.replace("@", "") : undefined)
}
displayName={displayName || (userName ? userName.replace("@", "") : "")}
/>
{modalState.isOpen && (
{modalState.isOpen && client && (
<SettingsModal
client={client}
defaultTab={defaultSettingsTab}

View File

@@ -98,7 +98,7 @@ export class PosthogAnalytics {
// set true during the constructor if posthog config is present, otherwise false
private static internalInstance: PosthogAnalytics | null = null;
private identificationPromise: Promise<void>;
private identificationPromise?: Promise<void>;
private readonly enabled: boolean = false;
private anonymity = Anonymity.Disabled;
private platformSuperProperties = {};
@@ -255,7 +255,9 @@ export class PosthogAnalytics {
} catch (e) {
// The above could fail due to network requests, but not essential to starting the application,
// so swallow it.
logger.log("Unable to identify user for tracking" + e.toString());
logger.log(
"Unable to identify user for tracking" + (e as Error)?.toString()
);
}
if (analyticsID) {
this.posthog.identify(analyticsID);
@@ -366,7 +368,7 @@ export class PosthogAnalytics {
if (anonymity === Anonymity.Pseudonymous) {
this.setRegistrationType(
window.matrixclient.isGuest() || window.isPasswordlessUser
window.matrixclient.isGuest() || window.passwordlessUser
? RegistrationType.Guest
: RegistrationType.Registered
);

View File

@@ -22,10 +22,10 @@ limitations under the License.
// Array.prototype.findLastIndex
export function findLastIndex<T>(
array: T[],
predicate: (item: T) => boolean
predicate: (item: T, index: number) => boolean
): number | null {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i;
if (predicate(array[i], i)) return i;
}
return null;
@@ -34,5 +34,11 @@ export function findLastIndex<T>(
/**
* Counts the number of elements in an array that satsify the given predicate.
*/
export const count = <T>(array: T[], predicate: (item: T) => boolean): number =>
array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0);
export const count = <T>(
array: T[],
predicate: (item: T, index: number) => boolean
): number =>
array.reduce(
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
0
);

View File

@@ -35,8 +35,8 @@ export const LoginPage: FC = () => {
const { setClient } = useClient();
const login = useInteractiveLogin();
const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable
const usernameRef = useRef<HTMLInputElement>();
const passwordRef = useRef<HTMLInputElement>();
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const history = useHistory();
const location = useLocation();
const [loading, setLoading] = useState(false);
@@ -49,12 +49,27 @@ export const LoginPage: FC = () => {
e.preventDefault();
setLoading(true);
if (!homeserver || !usernameRef.current || !passwordRef.current) {
setError(Error("Login parameters are undefined"));
setLoading(false);
return;
}
login(homeserver, usernameRef.current.value, passwordRef.current.value)
.then(([client, session]) => {
setClient(client, session);
if (!setClient) {
return;
}
if (location.state && location.state.from) {
history.push(location.state.from);
setClient({ client, session });
const locationState = location.state;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (locationState && locationState.from) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
history.push(locationState.from);
} else {
history.push("/");
}

View File

@@ -30,7 +30,7 @@ import { Trans, useTranslation } from "react-i18next";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button";
import { useClient } from "../ClientContext";
import { useClientLegacy } from "../ClientContext";
import { useInteractiveRegistration } from "./useInteractiveRegistration";
import styles from "./LoginPage.module.css";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
@@ -45,17 +45,17 @@ export const RegisterPage: FC = () => {
const { t } = useTranslation();
usePageTitle(t("Register"));
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
useClient();
const confirmPasswordRef = useRef<HTMLInputElement>();
const { loading, authenticated, passwordlessUser, client, setClient } =
useClientLegacy();
const confirmPasswordRef = useRef<HTMLInputElement>(null);
const history = useHistory();
const location = useLocation();
const [registering, setRegistering] = useState(false);
const [error, setError] = useState<Error>();
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { recaptchaKey, register } = useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const onSubmitRegisterForm = useCallback(
@@ -76,10 +76,11 @@ export const RegisterPage: FC = () => {
userName,
password,
userName,
recaptchaResponse
recaptchaResponse,
passwordlessUser
);
if (client && isPasswordlessUser) {
if (client && client?.groupCallEventHandler && passwordlessUser) {
// Migrate the user's rooms
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
const roomId = groupCall.room.roomId;
@@ -87,7 +88,11 @@ export const RegisterPage: FC = () => {
try {
await newClient.joinRoom(roomId);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (error.errcode === "M_LIMIT_EXCEEDED") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await sleep(error.data.retry_after_ms);
await newClient.joinRoom(roomId);
} else {
@@ -98,13 +103,17 @@ export const RegisterPage: FC = () => {
}
}
setClient(newClient, session);
setClient?.({ client: newClient, session });
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
};
submit()
.then(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (location.state?.from) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
history.push(location.state?.from);
} else {
history.push("/");
@@ -120,7 +129,7 @@ export const RegisterPage: FC = () => {
register,
location,
history,
isPasswordlessUser,
passwordlessUser,
reset,
execute,
client,
@@ -137,10 +146,10 @@ export const RegisterPage: FC = () => {
}, [password, passwordConfirmation, t]);
useEffect(() => {
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
if (!loading && authenticated && !passwordlessUser && !registering) {
history.push("/");
}
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
}, [loading, history, authenticated, passwordlessUser, registering]);
if (loading) {
return <LoadingView />;
@@ -211,7 +220,9 @@ export const RegisterPage: FC = () => {
apply.
<br />
By clicking "Register", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Link href={Config.get().eula}>
End User Licensing Agreement (EULA)
</Link>
</Trans>
</Caption>
{error && (

View File

@@ -41,8 +41,10 @@ export const useInteractiveLogin = () =>
},
password,
}),
stateUpdated: null,
requestEmailToken: null,
stateUpdated: (...args) => {},
requestEmailToken: (...args): Promise<{ sid: string }> => {
return Promise.resolve({ sid: "" });
},
});
// XXX: This claims to return an IAuthData which contains none of these

View File

@@ -22,29 +22,33 @@ import { initClient } from "../matrix-utils";
import { Session } from "../ClientContext";
import { Config } from "../config/Config";
export const useInteractiveRegistration = (): [
string,
string,
(
export const useInteractiveRegistration = (): {
privacyPolicyUrl?: string;
recaptchaKey?: string;
register: (
username: string,
password: string,
displayName: string,
recaptchaResponse: string,
passwordlessUser?: boolean
) => Promise<[MatrixClient, Session]>
] => {
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>();
const [recaptchaKey, setRecaptchaKey] = useState<string>();
passwordlessUser: boolean
) => Promise<[MatrixClient, Session]>;
} => {
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
undefined
);
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
undefined
);
const authClient = useRef<MatrixClient>();
if (!authClient.current) {
authClient.current = createClient({
baseUrl: Config.defaultHomeserverUrl(),
baseUrl: Config.defaultHomeserverUrl()!,
});
}
useEffect(() => {
authClient.current.registerRequest({}).catch((error) => {
authClient.current!.registerRequest({}).catch((error) => {
setPrivacyPolicyUrl(
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
);
@@ -58,12 +62,12 @@ export const useInteractiveRegistration = (): [
password: string,
displayName: string,
recaptchaResponse: string,
passwordlessUser?: boolean
passwordlessUser: boolean
): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({
matrixClient: authClient.current,
matrixClient: authClient.current!,
doRequest: (auth) =>
authClient.current.registerRequest({
authClient.current!.registerRequest({
username,
password,
auth: auth || undefined,
@@ -84,7 +88,9 @@ export const useInteractiveRegistration = (): [
});
}
},
requestEmailToken: null,
requestEmailToken: (...args) => {
return Promise.resolve({ sid: "dummy" });
},
});
// XXX: This claims to return an IAuthData which contains none of these
@@ -95,7 +101,7 @@ export const useInteractiveRegistration = (): [
const client = await initClient(
{
baseUrl: Config.defaultHomeserverUrl(),
baseUrl: Config.defaultHomeserverUrl()!,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
@@ -117,7 +123,7 @@ export const useInteractiveRegistration = (): [
session.tempPassword = password;
}
const user = client.getUser(client.getUserId());
const user = client.getUser(client.getUserId()!)!;
user.setRawDisplayName(displayName);
user.setDisplayName(displayName);
@@ -126,5 +132,5 @@ export const useInteractiveRegistration = (): [
[]
);
return [privacyPolicyUrl, recaptchaKey, register];
return { privacyPolicyUrl, recaptchaKey, register };
};

View File

@@ -34,7 +34,7 @@ interface RecaptchaPromiseRef {
reject: (error: Error) => void;
}
export const useRecaptcha = (sitekey: string) => {
export const useRecaptcha = (sitekey?: string) => {
const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>();
@@ -68,9 +68,9 @@ export const useRecaptcha = (sitekey: string) => {
}
}, [recaptchaId, sitekey]);
const execute = useCallback(() => {
const execute = useCallback((): Promise<string> => {
if (!sitekey) {
return Promise.resolve(null);
return Promise.resolve("");
}
if (!window.grecaptcha) {

View File

@@ -23,19 +23,23 @@ import { generateRandomName } from "../auth/generateRandomName";
import { useRecaptcha } from "../auth/useRecaptcha";
interface UseRegisterPasswordlessUserType {
privacyPolicyUrl: string;
privacyPolicyUrl?: string;
registerPasswordlessUser: (displayName: string) => Promise<void>;
recaptchaId: string;
recaptchaId?: string;
}
export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
const { setClient } = useClient();
const [privacyPolicyUrl, recaptchaKey, register] =
const { privacyPolicyUrl, recaptchaKey, register } =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const registerPasswordlessUser = useCallback(
async (displayName: string) => {
if (!setClient) {
throw new Error("No client context");
}
try {
const recaptchaResponse = await execute();
const userName = generateRandomName();
@@ -46,7 +50,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
recaptchaResponse,
true
);
setClient(client, session);
setClient({ client, session });
} catch (e) {
reset();
throw e;

View File

@@ -61,6 +61,10 @@ limitations under the License.
outline: auto;
}
.toolbarButton:disabled {
opacity: 0.55;
}
.toolbarButton,
.toolbarButtonSecondary {
width: 50px;

View File

@@ -77,6 +77,7 @@ interface Props {
children: Element[];
onPress: (e: PressEvent) => void;
onPressStart: (e: PressEvent) => void;
disabled: boolean;
// TODO: add all props for <Button>
[index: string]: unknown;
}

View File

@@ -46,7 +46,7 @@ export function LinkButton({
<Link
className={classNames(
variantToClassName[variant || "secondary"],
sizeToClassName[size],
size ? sizeToClassName[size] : [],
className
)}
to={to}

View File

@@ -45,11 +45,11 @@ export class Config {
// Convenience accessors
public static defaultHomeserverUrl(): string | undefined {
return Config.get().default_server_config["m.homeserver"].base_url;
return Config.get().default_server_config?.["m.homeserver"].base_url;
}
public static defaultServerName(): string | undefined {
return Config.get().default_server_config["m.homeserver"].server_name;
return Config.get().default_server_config?.["m.homeserver"].server_name;
}
public config?: ResolvedConfigOptions;

View File

@@ -66,6 +66,11 @@ export interface ConfigOptions {
features?: {
feature_group_calls_without_video_and_audio: boolean;
};
/**
* A link to the end-user license agreement (EULA)
*/
eula: string;
}
// Overrides members from ConfigOptions that are always provided by the
@@ -86,4 +91,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
server_name: "localhost",
},
},
eula: "https://static.element.io/legal/online-EULA.pdf",
};

View File

@@ -35,13 +35,13 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
return (
<>
<div className={styles.callList}>
{rooms.map(({ roomId, roomName, avatarUrl, participants }) => (
{rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => (
<CallTile
key={roomId}
key={roomAlias}
client={client}
name={roomName}
avatarUrl={avatarUrl}
roomId={roomId}
roomAlias={roomAlias}
participants={participants}
disableFacepile={disableFacepile}
/>
@@ -59,7 +59,7 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
interface CallTileProps {
name: string;
avatarUrl: string;
roomId: string;
roomAlias: string;
participants: RoomMember[];
client: MatrixClient;
disableFacepile?: boolean;
@@ -67,14 +67,17 @@ interface CallTileProps {
function CallTile({
name,
avatarUrl,
roomId,
roomAlias,
participants,
client,
disableFacepile,
}: CallTileProps) {
return (
<div className={styles.callTile}>
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
<Link
to={`/${roomAlias.substring(1).split(":")[0]}`}
className={styles.callTileLink}
>
<Avatar
size={Size.LG}
bgKey={name}
@@ -86,7 +89,7 @@ function CallTile({
<Body overflowEllipsis fontWeight="semiBold">
{name}
</Body>
<Caption overflowEllipsis>{getRoomUrl(roomId)}</Caption>
<Caption overflowEllipsis>{getRoomUrl(roomAlias)}</Caption>
{participants && !disableFacepile && (
<Facepile
className={styles.facePile}
@@ -100,7 +103,7 @@ function CallTile({
<CopyButton
className={styles.copyButton}
variant="icon"
value={getRoomUrl(roomId)}
value={getRoomUrl(roomAlias)}
/>
</div>
);

View File

@@ -42,6 +42,12 @@ interface Props {
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
const { t } = useTranslation();
const onAction = (key: React.Key) => {
setCallType(key.toString() as CallType);
};
const onClose = () => {};
return (
<PopoverMenuTrigger placement="bottom">
<Button variant="dropdown" className={commonStyles.headline}>
@@ -52,7 +58,12 @@ export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
</Headline>
</Button>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label={t("Call type menu")} onAction={setCallType}>
<Menu
{...props}
label={t("Call type menu")}
onAction={onAction}
onClose={onClose}
>
<Item key={CallType.Video} textValue={t("Video call")}>
<VideoIcon />
<span>{t("Video call")}</span>

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { useTranslation } from "react-i18next";
import { useClient } from "../ClientContext";
import { useClientState } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { UnauthenticatedView } from "./UnauthenticatedView";
import { RegisteredView } from "./RegisteredView";
@@ -26,16 +26,18 @@ export function HomePage() {
const { t } = useTranslation();
usePageTitle(t("Home"));
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
useClient();
const clientState = useClientState();
if (loading) {
if (!clientState) {
return <LoadingView />;
} else if (error) {
return <ErrorView error={error} />;
} else if (clientState.state === "error") {
return <ErrorView error={clientState.error} />;
} else {
return isAuthenticated ? (
<RegisteredView isPasswordlessUser={isPasswordlessUser} client={client} />
return clientState.authenticated ? (
<RegisteredView
isPasswordlessUser={clientState.authenticated.isPasswordlessUser}
client={clientState.authenticated.client}
/>
) : (
<UnauthenticatedView />
);

View File

@@ -39,6 +39,7 @@ import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import { useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
interface Props {
client: MatrixClient;
@@ -69,10 +70,10 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
setError(undefined);
setLoading(true);
const [roomIdOrAlias] = await createRoom(client, roomName, ptt);
const [roomAlias] = await createRoom(client, roomName, ptt);
if (roomIdOrAlias) {
history.push(`/room/${roomIdOrAlias}`);
if (roomAlias) {
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
}
}
@@ -146,6 +147,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
<AnalyticsNotice />
</Caption>
)}
<E2EEBanner />
{error && (
<FieldRow className={styles.fieldRow}>
<ErrorMessage error={error} />

View File

@@ -41,6 +41,8 @@ import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
@@ -48,8 +50,7 @@ export const UnauthenticatedView: FC = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics();
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { recaptchaKey, register } = useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const { modalState, modalProps } = useModalTriggerState();
@@ -78,16 +79,21 @@ export const UnauthenticatedView: FC = () => {
true
);
let roomIdOrAlias: string;
let roomAlias: string;
try {
[roomIdOrAlias] = await createRoom(client, roomName, ptt);
[roomAlias] = await createRoom(client, roomName, ptt);
} catch (error) {
if (!setClient) {
throw error;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (error.errcode === "M_ROOM_IN_USE") {
setOnFinished(() => {
setClient(client, session);
setClient({ client, session });
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
const [, serverName] = client.getUserId().split(":");
history.push(`/room/#${aliasLocalpart}:${serverName}`);
history.push(`/${aliasLocalpart}`);
});
setLoading(false);
@@ -99,8 +105,12 @@ export const UnauthenticatedView: FC = () => {
}
// Only consider the registration successful if we managed to create the room, too
setClient(client, session);
history.push(`/room/${roomIdOrAlias}`);
if (!setClient) {
throw new Error("setClient is undefined");
}
setClient({ client, session });
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
}
submit().catch((error) => {
@@ -165,9 +175,12 @@ export const UnauthenticatedView: FC = () => {
<Caption className={styles.notice}>
<Trans>
By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Link href={Config.get().eula}>
End User Licensing Agreement (EULA)
</Link>
</Trans>
</Caption>
<E2EEBanner />
{error && (
<FieldRow>
<ErrorMessage error={error} />
@@ -200,7 +213,7 @@ export const UnauthenticatedView: FC = () => {
</Body>
</footer>
</div>
{modalState.isOpen && (
{modalState.isOpen && onFinished && (
<JoinExistingCallModal onJoin={onFinished} {...modalProps} />
)}
</>

View File

@@ -22,7 +22,7 @@ import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEv
import { useState, useEffect } from "react";
export interface GroupCallRoom {
roomId: string;
roomAlias: string;
roomName: string;
avatarUrl: string;
room: Room;
@@ -42,7 +42,7 @@ function getLastTs(client: MatrixClient, r: Room) {
return ts;
}
const myUserId = client.getUserId();
const myUserId = client.getUserId()!;
if (r.getMyMembership() !== "join") {
const membershipEvent = r.currentState.getStateEvents(
@@ -79,26 +79,32 @@ function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
}
export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const [rooms, setRooms] = useState([]);
const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
useEffect(() => {
function updateRooms() {
if (!client.groupCallEventHandler) {
return;
}
const groupCalls = client.groupCallEventHandler.groupCalls.values();
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
const sortedRooms = sortRooms(client, rooms);
const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias
const sortedRooms = sortRooms(client, filteredRooms);
const items = sortedRooms.map((room) => {
const groupCall = client.getGroupCallForRoom(room.roomId);
const groupCall = client.getGroupCallForRoom(room.roomId)!;
return {
roomId: room.getCanonicalAlias() || room.roomId,
roomAlias: room.getCanonicalAlias(),
roomName: room.name,
avatarUrl: room.getMxcAvatarUrl(),
avatarUrl: room.getMxcAvatarUrl()!,
room,
groupCall,
participants: [...groupCall.participants],
participants: [...groupCall!.participants.keys()],
};
});
setRooms(items);
setRooms(items as GroupCallRoom[]);
}
updateRooms();

4
src/icons/LockOff.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.00003 14.6665C3.63336 14.6665 3.31947 14.5359 3.05836 14.2748C2.79725 14.0137 2.6667 13.6998 2.6667 13.3332V6.6665C2.6667 6.29984 2.79725 5.98595 3.05836 5.72484C3.19921 5.58399 3.35541 5.48113 3.52697 5.41626L0.888621 2.77791C0.628272 2.51756 0.628272 2.09545 0.888622 1.8351C1.14897 1.57475 1.57108 1.57475 1.83143 1.8351L4.6667 4.67037V4.66267L13.3334 13.3293V13.3332L13.3334 13.337L14.1648 14.1685C14.4251 14.4288 14.4251 14.8509 14.1648 15.1113C13.9044 15.3716 13.4823 15.3716 13.222 15.1113L12.6247 14.514C12.437 14.6157 12.2288 14.6665 12 14.6665H4.00003Z" fill="#808994"/>
<path d="M13.3334 11.4437V6.6665C13.3334 6.29984 13.2028 5.98595 12.9417 5.72484C12.6806 5.46373 12.3667 5.33317 12 5.33317H11.3334V3.99984C11.3334 3.07762 11.0084 2.2915 10.3584 1.6415C9.70836 0.991504 8.92225 0.666504 8.00003 0.666504C7.07781 0.666504 6.2917 0.991504 5.6417 1.6415C5.25683 2.02637 4.9859 2.45896 4.82892 2.93927L6.00003 4.11038V3.99984C6.00003 3.44428 6.19447 2.97206 6.58336 2.58317C6.97225 2.19428 7.44447 1.99984 8.00003 1.99984C8.55559 1.99984 9.02781 2.19428 9.4167 2.58317C9.80558 2.97206 10 3.44428 10 3.99984V5.33317H7.22282L13.3334 11.4437Z" fill="#808994"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -52,6 +52,7 @@ limitations under the License.
--background: #15191e;
--background-85: rgba(23, 25, 28, 0.85);
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
--subtle-primary: #26282d;
}
@font-face {

View File

@@ -55,7 +55,7 @@ export class Initializer {
languageDetector.addDetector({
name: "urlFragment",
// Look for a language code in the URL's fragment
lookup: () => getUrlParams().lang ?? undefined,
lookup: () => getUrlParams(true).lang ?? undefined,
});
i18n
@@ -133,10 +133,14 @@ export class Initializer {
"--background-85",
import.meta.env.VITE_THEME_BACKGROUND_85 as string
);
style.setProperty(
"--subtle-primary",
import.meta.env.VITE_THEME_SUBTLE_PRIMARY as string
);
}
// Custom fonts
const { fonts, fontScale } = getUrlParams();
const { fonts, fontScale } = getUrlParams(true);
if (fontScale !== null) {
document.documentElement.style.setProperty(
"--font-scale",
@@ -225,5 +229,6 @@ export class Initializer {
resolve();
}
}
private initPromise: Promise<void> | null;
private initPromise?: Promise<void>;
}

View File

@@ -15,10 +15,14 @@ limitations under the License.
*/
import { useObjectRef } from "@react-aria/utils";
import { AllHTMLAttributes, ChangeEvent, useEffect } from "react";
import { useCallback } from "react";
import { useState } from "react";
import { forwardRef } from "react";
import {
AllHTMLAttributes,
useEffect,
useCallback,
useState,
forwardRef,
ChangeEvent,
} from "react";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
@@ -30,7 +34,7 @@ import styles from "./AvatarInputField.module.css";
interface Props extends AllHTMLAttributes<HTMLInputElement> {
id: string;
label: string;
avatarUrl: string;
avatarUrl: string | undefined;
displayName: string;
onRemoveAvatar: () => void;
}
@@ -43,7 +47,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
const { t } = useTranslation();
const [removed, setRemoved] = useState(false);
const [objUrl, setObjUrl] = useState<string>(null);
const [objUrl, setObjUrl] = useState<string | undefined>(undefined);
const fileInputRef = useObjectRef(ref);
@@ -52,11 +56,11 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
const onChange = (e: Event) => {
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
if (inputEvent.target.files.length > 0) {
if (inputEvent.target.files && inputEvent.target.files.length > 0) {
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
setRemoved(false);
} else {
setObjUrl(null);
setObjUrl(undefined);
}
};
@@ -77,7 +81,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
<div className={styles.avatarContainer}>
<Avatar
size={Size.XL}
src={removed ? null : objUrl || avatarUrl}
src={removed ? undefined : objUrl || avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
<input

View File

@@ -82,7 +82,7 @@ interface InputFieldProps {
defaultValue?: string;
placeholder?: string;
defaultChecked?: boolean;
onChange?: (event: ChangeEvent) => void;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}
export const InputField = forwardRef<
@@ -119,6 +119,8 @@ export const InputField = forwardRef<
>
{prefix && <span>{prefix}</span>}
{type === "textarea" ? (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<textarea
id={id}
ref={ref as ForwardedRef<HTMLTextAreaElement>}

View File

@@ -34,7 +34,7 @@ export function SelectInput(props: Props): JSX.Element {
const { t } = useTranslation();
const state = useSelectState(props);
const ref = useRef();
const ref = useRef(null);
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
props,
state,

View File

@@ -22,6 +22,7 @@ import {
useState,
} from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { GroupCall } from "matrix-js-sdk";
import {
OpenIDClientParts,
@@ -32,21 +33,16 @@ import { ErrorView, LoadingView } from "../FullScreenView";
interface Props {
client: OpenIDClientParts;
livekitServiceURL: string;
groupCall: GroupCall;
roomName: string;
children: ReactNode;
}
const SFUConfigContext = createContext<SFUConfig>(undefined);
const SFUConfigContext = createContext<SFUConfig | undefined>(undefined);
export const useSFUConfig = () => useContext(SFUConfigContext);
export function OpenIDLoader({
client,
livekitServiceURL,
roomName,
children,
}: Props) {
export function OpenIDLoader({ client, groupCall, roomName, children }: Props) {
const [state, setState] = useState<
SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed
>({ kind: "loading" });
@@ -56,16 +52,16 @@ export function OpenIDLoader({
try {
const result = await getSFUConfigWithOpenID(
client,
livekitServiceURL,
groupCall,
roomName
);
setState({ kind: "loaded", sfuConfig: result });
} catch (e) {
logger.error("Failed to fetch SFU config: ", e);
setState({ kind: "failed", error: e });
setState({ kind: "failed", error: e as Error });
}
})();
}, [client, livekitServiceURL, roomName]);
}, [client, groupCall, roomName]);
switch (state.kind) {
case "loading":

View File

@@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "matrix-js-sdk";
import { GroupCall, IOpenIDToken, MatrixClient } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/src/logger";
import { Config } from "../config/Config";
export interface SFUConfig {
url: string;
jwt: string;
@@ -30,25 +32,90 @@ export type OpenIDClientParts = Pick<
export async function getSFUConfigWithOpenID(
client: OpenIDClientParts,
livekitServiceURL: string,
groupCall: GroupCall,
roomName: string
): Promise<SFUConfig> {
const openIdToken = await client.getOpenIdToken();
logger.debug("Got openID token", openIdToken);
const res = await fetch(livekitServiceURL + "/sfu/get", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
room: roomName,
openid_token: openIdToken,
device_id: client.getDeviceId(),
}),
});
if (!res.ok) {
throw new Error("SFO Config fetch failed with status code " + res.status);
// if the call has a livekit service URL, try it.
if (groupCall.livekitServiceURL) {
try {
logger.info(
`Trying to get JWT from call's configured URL of ${groupCall.livekitServiceURL}...`
);
const sfuConfig = await getLiveKitJWT(
client,
groupCall.livekitServiceURL,
roomName,
openIdToken
);
logger.info(`Got JWT from call state event URL.`);
return sfuConfig;
} catch (e) {
logger.warn(
`Failed to get JWT from group call's configured URL of ${groupCall.livekitServiceURL}.`,
e
);
}
}
// otherwise, try our configured one and, if it works, update the call's service URL in the state event
// NB. This wuill update it for everyone so we may end up with multiple clients updating this when they
// join at similar times, but we don't have a huge number of options here.
const urlFromConf = Config.get().livekit!.livekit_service_url;
logger.info(`Trying livekit service URL from our config: ${urlFromConf}...`);
try {
const sfuConfig = await getLiveKitJWT(
client,
urlFromConf,
roomName,
openIdToken
);
logger.info(
`Got JWT, updating call livekit service URL with: ${urlFromConf}...`
);
try {
await groupCall.updateLivekitServiceURL(urlFromConf);
logger.info(`Call livekit service URL updated.`);
} catch (e) {
logger.warn(
`Failed to update call livekit service URL: continuing anyway.`
);
}
return sfuConfig;
} catch (e) {
logger.error("Failed to get JWT from URL defined in Config.", e);
throw e;
}
}
async function getLiveKitJWT(
client: OpenIDClientParts,
livekitServiceURL: string,
roomName: string,
openIDToken: IOpenIDToken
): Promise<SFUConfig> {
try {
const res = await fetch(livekitServiceURL + "/sfu/get", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
room: roomName,
openid_token: openIDToken,
device_id: client.getDeviceId(),
}),
});
if (!res.ok) {
throw new Error("SFU Config fetch failed with status code " + res.status);
}
return await res.json();
} catch (e) {
throw new Error("SFU Config fetch failed with exception " + e);
}
return await res.json();
}

View File

@@ -14,16 +14,12 @@ const defaultLiveKitPublishOptions: TrackPublishDefaults = {
red: true,
forceStereo: false,
simulcast: true,
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h216] as VideoPreset[],
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360] as VideoPreset[],
screenShareEncoding: ScreenSharePresets.h1080fps30.encoding,
screenShareSimulcastLayers: [
new VideoPreset(1920, 1080, 1_500_000, 5, "medium"),
ScreenSharePresets.h1080fps15,
] as VideoPreset[],
stopMicTrackOnMute: false,
videoCodec: "vp8",
videoEncoding: VideoPresets.h360.encoding,
backupCodec: { codec: "vp8", encoding: VideoPresets.h360.encoding },
videoEncoding: VideoPresets.h720.encoding,
backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding },
} as const;
export const defaultLiveKitOptions: RoomOptions = {
@@ -35,7 +31,7 @@ export const defaultLiveKitOptions: RoomOptions = {
// capture settings
videoCaptureDefaults: {
resolution: VideoPresets.h360.resolution,
resolution: VideoPresets.h720.resolution,
},
// publish settings

View File

@@ -1,4 +1,4 @@
import { Room, RoomOptions } from "livekit-client";
import { Room, RoomOptions, setLogLevel } from "livekit-client";
import { useLiveKitRoom } from "@livekit/components-react";
import { useMemo } from "react";
@@ -15,9 +15,11 @@ export type DeviceChoices = {
enabled: boolean;
};
setLogLevel("debug");
export function useLiveKit(
userChoices: UserChoices,
sfuConfig: SFUConfig
sfuConfig?: SFUConfig
): Room | undefined {
const roomOptions = useMemo((): RoomOptions => {
const options = defaultLiveKitOptions;
@@ -33,8 +35,8 @@ export function useLiveKit(
}, [userChoices.video, userChoices.audio]);
const { room } = useLiveKitRoom({
token: sfuConfig.jwt,
serverUrl: sfuConfig.url,
token: sfuConfig?.jwt,
serverUrl: sfuConfig?.url,
audio: userChoices.audio?.enabled ?? false,
video: userChoices.video?.enabled ?? false,
options: roomOptions,

View File

@@ -1,5 +1,5 @@
import { useMediaDeviceSelect } from "@livekit/components-react";
import { Room } from "livekit-client";
import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client";
import { useEffect } from "react";
import { useDefaultDevices } from "../settings/useSetting";
@@ -17,12 +17,21 @@ export type MediaDevicesState = {
};
// if a room is passed this only affects the device selection inside a call. Without room it changes what we see in the lobby
export function useMediaDevices(room?: Room): MediaDevicesState {
export function useMediaDevicesSwitcher(
room?: Room,
tracks?: { videoTrack?: LocalVideoTrack; audioTrack?: LocalAudioTrack },
requestPermissions = true
): MediaDevicesState {
const {
devices: videoDevices,
activeDeviceId: activeVideoDevice,
setActiveMediaDevice: setActiveVideoDevice,
} = useMediaDeviceSelect({ kind: "videoinput", room });
} = useMediaDeviceSelect({
kind: "videoinput",
room,
track: tracks?.videoTrack,
requestPermissions,
});
const {
devices: audioDevices,
@@ -31,6 +40,8 @@ export function useMediaDevices(room?: Room): MediaDevicesState {
} = useMediaDeviceSelect({
kind: "audioinput",
room,
track: tracks?.audioTrack,
requestPermissions,
});
const {

View File

@@ -19,8 +19,7 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
import { createClient } from "matrix-js-sdk/src/matrix";
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
@@ -57,8 +56,8 @@ function waitForSync(client: MatrixClient) {
return new Promise<void>((resolve, reject) => {
const onSync = (
state: SyncState,
_old: SyncState,
data: ISyncStateData
_old: SyncState | null,
data?: ISyncStateData
) => {
if (state === "PREPARED") {
resolve();
@@ -87,7 +86,7 @@ export async function initClient(
): Promise<MatrixClient> {
await loadOlm();
let indexedDB: IDBFactory;
let indexedDB: IDBFactory | undefined;
try {
indexedDB = window.indexedDB;
} catch (e) {}
@@ -247,7 +246,7 @@ export function sanitiseRoomNameInput(input: string): string {
*/
export function roomNameFromRoomId(roomId: string): string {
return roomId
.match(/([^:]+):.*$/)[1]
.match(/([^:]+):.*$/)![1]
.substring(1)
.split("-")
.map((part) =>
@@ -262,7 +261,7 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
return false;
}
const parts = roomId.match(/[^:]+:(.*)$/);
const parts = roomId.match(/[^:]+:(.*)$/)!;
if (parts.length < 2) {
return false;
@@ -302,7 +301,7 @@ export async function createRoom(
"org.matrix.msc3401.call.member": 0,
},
users: {
[client.getUserId()]: 100,
[client.getUserId()!]: 100,
},
},
});
@@ -346,15 +345,11 @@ export async function createRoom(
// Returns a URL to that will load Element Call with the given room
export function getRoomUrl(roomIdOrAlias: string): string {
if (roomIdOrAlias.startsWith("#")) {
const [localPart, host] = roomIdOrAlias.replace("#", "").split(":");
if (host !== Config.defaultServerName()) {
return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`;
} else {
return `${window.location.protocol}//${window.location.host}/${localPart}`;
}
return `${window.location.protocol}//${window.location.host}/${
roomIdOrAlias.substring(1).split(":")[0]
}`;
} else {
return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`;
return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`;
}
}

View File

@@ -61,46 +61,46 @@ export class OTelCall {
}
public dispose() {
this.call.peerConn.removeEventListener(
this.call.peerConn?.removeEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged
);
this.call.peerConn.removeEventListener(
this.call.peerConn?.removeEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged
);
this.call.peerConn.removeEventListener(
this.call.peerConn?.removeEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged
);
this.call.peerConn.removeEventListener(
this.call.peerConn?.removeEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged
);
this.call.peerConn.removeEventListener(
this.call.peerConn?.removeEventListener(
"icecandidateerror",
this.onIceCandidateError
);
}
private addCallPeerConnListeners = (): void => {
this.call.peerConn.addEventListener(
this.call.peerConn?.addEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged
);
this.call.peerConn.addEventListener(
this.call.peerConn?.addEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged
);
this.call.peerConn.addEventListener(
this.call.peerConn?.addEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged
);
this.call.peerConn.addEventListener(
this.call.peerConn?.addEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged
);
this.call.peerConn.addEventListener(
this.call.peerConn?.addEventListener(
"icecandidateerror",
this.onIceCandidateError
);
@@ -108,25 +108,25 @@ export class OTelCall {
public onCallConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.callConnectionStateChange", {
callConnectionState: this.call.peerConn.connectionState,
callConnectionState: this.call.peerConn?.connectionState,
});
};
public onCallSignalingStateChanged = (): void => {
this.span.addEvent("matrix.call.callSignalingStateChange", {
callSignalingState: this.call.peerConn.signalingState,
callSignalingState: this.call.peerConn?.signalingState,
});
};
public onIceConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.iceConnectionStateChange", {
iceConnectionState: this.call.peerConn.iceConnectionState,
iceConnectionState: this.call.peerConn?.iceConnectionState,
});
};
public onIceGatheringStateChanged = (): void => {
this.span.addEvent("matrix.call.iceGatheringStateChange", {
iceGatheringState: this.call.peerConn.iceGatheringState,
iceGatheringState: this.call.peerConn?.iceGatheringState,
});
};

View File

@@ -172,7 +172,7 @@ export class OTelGroupCallMembership {
if (
!userCalls ||
!userCalls.has(callTrackingInfo.deviceId) ||
userCalls.get(callTrackingInfo.deviceId).callId !==
userCalls.get(callTrackingInfo.deviceId)?.callId !==
callTrackingInfo.call.callId
) {
callTrackingInfo.end();
@@ -420,7 +420,7 @@ export class OTelGroupCallMembership {
ctx
);
span.setAttribute("matrix.callId", callId);
span.setAttribute("matrix.callId", callId ?? "unknown");
span.setAttribute(
"matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown"

View File

@@ -23,7 +23,6 @@ import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"
import { logger } from "matrix-js-sdk/src/logger";
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
import { Anonymity } from "../analytics/PosthogAnalytics";
import { Config } from "../config/Config";
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
@@ -34,8 +33,7 @@ let sharedInstance: ElementCallOpenTelemetry;
export class ElementCallOpenTelemetry {
private _provider: WebTracerProvider;
private _tracer: Tracer;
private _anonymity: Anonymity;
private otlpExporter: OTLPTraceExporter;
private otlpExporter?: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
static globalInit(): void {
@@ -100,7 +98,7 @@ export class ElementCallOpenTelemetry {
}
public dispose(): void {
opentelemetry.trace.setGlobalTracerProvider(null);
opentelemetry.trace.disable();
this._provider?.shutdown();
}
@@ -115,8 +113,4 @@ export class ElementCallOpenTelemetry {
public get provider(): WebTracerProvider {
return this._provider;
}
public get anonymity(): Anonymity {
return this._anonymity;
}
}

View File

@@ -46,7 +46,7 @@ export const PopoverMenuTrigger = forwardRef<
buttonRef
);
const popoverRef = useRef();
const popoverRef = useRef(null);
const { overlayProps } = useOverlayPosition({
targetRef: buttonRef,

View File

@@ -21,10 +21,10 @@ import { FileType } from "matrix-js-sdk/src/http-api";
import { useState, useCallback, useEffect } from "react";
interface ProfileLoadState {
success?: boolean;
loading?: boolean;
displayName: string;
avatarUrl: string;
success: boolean;
loading: boolean;
displayName?: string;
avatarUrl?: string;
error?: Error;
}
@@ -38,23 +38,26 @@ type ProfileSaveCallback = ({
removeAvatar: boolean;
}) => Promise<void>;
export function useProfile(client: MatrixClient) {
const [{ loading, displayName, avatarUrl, error, success }, setState] =
export function useProfile(client: MatrixClient | undefined) {
const [{ success, loading, displayName, avatarUrl, error }, setState] =
useState<ProfileLoadState>(() => {
const user = client?.getUser(client.getUserId());
let user: User | undefined = undefined;
if (client) {
user = client.getUser(client.getUserId()!) ?? undefined;
}
return {
success: false,
loading: false,
displayName: user?.rawDisplayName,
avatarUrl: user?.avatarUrl,
error: null,
error: undefined,
};
});
useEffect(() => {
const onChangeUser = (
_event: MatrixEvent,
_event: MatrixEvent | undefined,
{ displayName, avatarUrl }: User
) => {
setState({
@@ -62,17 +65,16 @@ export function useProfile(client: MatrixClient) {
loading: false,
displayName,
avatarUrl,
error: null,
error: undefined,
});
};
let user: User;
let user: User | null;
if (client) {
const userId = client.getUserId();
const userId = client.getUserId()!;
user = client.getUser(userId);
user.on(UserEvent.DisplayName, onChangeUser);
user.on(UserEvent.AvatarUrl, onChangeUser);
user?.on(UserEvent.DisplayName, onChangeUser);
user?.on(UserEvent.AvatarUrl, onChangeUser);
}
return () => {
@@ -89,7 +91,7 @@ export function useProfile(client: MatrixClient) {
setState((prev) => ({
...prev,
loading: true,
error: null,
error: undefined,
success: false,
}));
@@ -110,7 +112,9 @@ export function useProfile(client: MatrixClient) {
setState((prev) => ({
...prev,
displayName,
avatarUrl: removeAvatar ? null : mxcAvatarUrl ?? prev.avatarUrl,
avatarUrl: removeAvatar
? undefined
: mxcAvatarUrl ?? prev.avatarUrl,
loading: false,
success: true,
}));

View File

@@ -38,6 +38,15 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Change layout"), [t]);
const onAction = useCallback(
(key: React.Key) => {
setLayout(key.toString() as Layout);
},
[setLayout]
);
const onClose = useCallback(() => {}, []);
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={tooltip}>
@@ -46,7 +55,12 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}>
<Menu
{...props}
label={t("Grid layout menu")}
onAction={onAction}
onClose={onClose}
>
<Item key="freedom" textValue={t("Freedom")}>
<FreedomIcon />
<span>Freedom</span>

View File

@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/*
Copyright 2022 New Vector Ltd
@@ -70,7 +73,7 @@ const defaultCollapsedFields = [
];
function shouldCollapse({ name }: CollapsedFieldProps) {
return defaultCollapsedFields.includes(name);
return name ? defaultCollapsedFields.includes(name) : false;
}
function getUserName(userId: string) {
@@ -196,7 +199,7 @@ export function SequenceDiagramViewer({
onSelectUserId,
events,
}: SequenceDiagramViewerProps) {
const mermaidElRef = useRef<HTMLDivElement>();
const mermaidElRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
@@ -217,6 +220,7 @@ export function SequenceDiagramViewer({
`;
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
if (!mermaidElRef.current) return;
mermaidElRef.current.innerHTML = svgCode;
});
}, [events, localUserId, selectedUserId]);
@@ -228,7 +232,7 @@ export function SequenceDiagramViewer({
className={styles.selectInput}
label="Remote User"
selectedKey={selectedUserId}
onSelectionChange={onSelectUserId}
onSelectionChange={(key) => onSelectUserId(key.toString())}
>
{remoteUserIds.map((userId) => (
<Item key={userId}>{userId}</Item>
@@ -498,7 +502,7 @@ export function GroupCallInspector({
return (
<Resizable
enable={{ top: true }}
defaultSize={{ height: 200, width: undefined }}
defaultSize={{ height: 200, width: 0 }}
className={styles.inspector}
>
<div className={styles.toolbar}>
@@ -507,15 +511,19 @@ export function GroupCallInspector({
</button>
<button onClick={() => setCurrentTab("inspector")}>Inspector</button>
</div>
{currentTab === "sequence-diagrams" && (
<SequenceDiagramViewer
localUserId={state.localUserId}
selectedUserId={selectedUserId}
onSelectUserId={setSelectedUserId}
remoteUserIds={state.remoteUserIds}
events={state.eventsByUserId[selectedUserId]}
/>
)}
{currentTab === "sequence-diagrams" &&
state.localUserId &&
selectedUserId &&
state.eventsByUserId &&
state.remoteUserIds && (
<SequenceDiagramViewer
localUserId={state.localUserId}
selectedUserId={selectedUserId}
onSelectUserId={setSelectedUserId}
remoteUserIds={state.remoteUserIds}
events={state.eventsByUserId[selectedUserId]}
/>
)}
{currentTab === "inspector" && (
<ReactJson
theme="monokai"

View File

@@ -21,7 +21,6 @@ import { useTranslation } from "react-i18next";
import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { usePageTitle } from "../usePageTitle";
interface Props {
client: MatrixClient;
@@ -39,26 +38,23 @@ export function GroupCallLoader({
createPtt,
}: Props): JSX.Element {
const { t } = useTranslation();
const { loading, error, groupCall } = useLoadGroupCall(
const groupCallState = useLoadGroupCall(
client,
roomIdOrAlias,
viaServers,
createPtt
);
usePageTitle(groupCall ? groupCall.room.name : t("Loading…"));
if (loading) {
return (
<FullScreenView>
<h1>{t("Loading…")}</h1>
</FullScreenView>
);
switch (groupCallState.kind) {
case "loading":
return (
<FullScreenView>
<h1>{t("Loading…")}</h1>
</FullScreenView>
);
case "loaded":
return <>{children(groupCallState.groupCall)}</>;
case "failed":
return <ErrorView error={groupCallState.error} />;
}
if (error) {
return <ErrorView error={error} />;
}
return <>{children(groupCall)}</>;
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
@@ -36,7 +36,6 @@ import { UserChoices } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
import { Config } from "../config/Config";
declare global {
interface Window {
@@ -50,7 +49,6 @@ interface Props {
isEmbedded: boolean;
preload: boolean;
hideHeader: boolean;
roomIdOrAlias: string;
groupCall: GroupCall;
}
@@ -60,7 +58,6 @@ export function GroupCallView({
isEmbedded,
preload,
hideHeader,
roomIdOrAlias,
groupCall,
}: Props) {
const {
@@ -83,13 +80,14 @@ export function GroupCallView({
}, [groupCall]);
const { displayName, avatarUrl } = useProfile(client);
const matrixInfo: MatrixInfo = {
displayName,
avatarUrl,
roomName: groupCall.room.name,
roomIdOrAlias,
};
const matrixInfo = useMemo((): MatrixInfo => {
return {
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: groupCall.room.roomId,
roomName: groupCall.room.name,
};
}, [displayName, avatarUrl, groupCall]);
useEffect(() => {
if (widget && preload) {
@@ -140,14 +138,14 @@ export function GroupCallView({
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
await Promise.all([
widget.api.setAlwaysOnScreen(true),
widget.api.transport.reply(ev.detail, {}),
widget!.api.setAlwaysOnScreen(true),
widget!.api.transport.reply(ev.detail, {}),
]);
};
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
return () => {
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
}
}, [groupCall, preload, enter]);
@@ -206,12 +204,12 @@ export function GroupCallView({
if (widget && state === GroupCallState.Entered) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leave();
await widget.api.transport.reply(ev.detail, {});
widget.api.setAlwaysOnScreen(false);
await widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false);
};
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
return () => {
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
};
}
}, [groupCall, state, leave]);
@@ -220,20 +218,14 @@ export function GroupCallView({
undefined
);
const lkServiceURL = Config.get().livekit?.livekit_service_url;
if (!lkServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
if (error) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered && userChoices) {
return (
<OpenIDLoader
client={client}
livekitServiceURL={lkServiceURL}
roomName={matrixInfo.roomName}
groupCall={groupCall}
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
>
<ActiveCall
client={client}
@@ -242,7 +234,6 @@ export function GroupCallView({
onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
matrixInfo={matrixInfo}
userChoices={userChoices}
otelGroupCallMembership={otelGroupCallMembership}
/>

View File

@@ -68,22 +68,21 @@ import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css";
import { MatrixInfo } from "./VideoPreview";
import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent } from "../video-grid/VideoTile";
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { VideoTile } from "../video-grid/VideoTile";
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { useMediaDevices } from "../livekit/useMediaDevices";
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader";
import { E2EELock } from "../E2EELock";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -99,12 +98,14 @@ export function ActiveCall(props: ActiveCallProps) {
const sfuConfig = useSFUConfig();
const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
if (!livekitRoom) {
return null;
}
return (
livekitRoom && (
<RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} />
</RoomContext.Provider>
)
<RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} />
</RoomContext.Provider>
);
}
@@ -116,8 +117,7 @@ export interface InCallViewProps {
onLeave: () => void;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean;
matrixInfo: MatrixInfo;
otelGroupCallMembership: OTelGroupCallMembership;
otelGroupCallMembership?: OTelGroupCallMembership;
}
export function InCallView({
@@ -128,7 +128,6 @@ export function InCallView({
onLeave,
unencryptedEventsFromUsers,
hideHeader,
matrixInfo,
otelGroupCallMembership,
}: InCallViewProps) {
const { t } = useTranslation();
@@ -147,7 +146,7 @@ export function InCallView({
);
// Managed media devices state coupled with an active room.
const roomMediaDevices = useMediaDevices(livekitRoom);
const roomMediaSwitcher = useMediaDevicesSwitcher(livekitRoom);
const screenSharingTracks = useTracks(
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
@@ -202,11 +201,11 @@ export function InCallView({
if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("freedom");
await widget.api.transport.reply(ev.detail, {});
await widget!.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("spotlight");
await widget.api.transport.reply(ev.detail, {});
await widget!.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
@@ -216,8 +215,8 @@ export function InCallView({
);
return () => {
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.off(
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget!.lazyActions.off(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout
);
@@ -340,7 +339,12 @@ export function InCallView({
const toggleScreensharing = useCallback(async () => {
exitFullscreen();
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled);
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
audio: true,
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
});
}, [localParticipant, isScreenShareEnabled, exitFullscreen]);
let footer: JSX.Element | null;
@@ -390,11 +394,12 @@ export function InCallView({
{!hideHeader && maximisedParticipant === null && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={matrixInfo.roomName} />
<RoomHeaderInfo roomName={groupCall.room.name} />
<VersionMismatchWarning
users={unencryptedEventsFromUsers}
room={groupCall.room}
/>
<E2EELock />
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
@@ -409,31 +414,30 @@ export function InCallView({
{renderContent()}
{footer}
</div>
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
{otelGroupCallMembership && (
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
)}
{rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomIdOrAlias={matrixInfo.roomIdOrAlias}
roomId={groupCall.room.roomId}
/>
)}
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
mediaDevices={roomMediaDevices}
mediaDevicesSwitcher={roomMediaSwitcher}
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && (
<InviteModal
roomIdOrAlias={matrixInfo.roomIdOrAlias}
{...inviteModalProps}
/>
<InviteModal roomId={groupCall.room.roomId} {...inviteModalProps} />
)}
</div>
);

View File

@@ -23,10 +23,10 @@ import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
interface Props extends Omit<ModalProps, "title" | "children"> {
roomIdOrAlias: string;
roomId: string;
}
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
const { t } = useTranslation();
return (
@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
value={getRoomUrl(roomId)}
data-testid="modal_inviteLink"
/>
</ModalContent>

View File

@@ -39,7 +39,7 @@ export function LobbyView(props: Props) {
const { t } = useTranslation();
useLocationNavigation();
const joinCallButtonRef = useRef<HTMLButtonElement>();
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (joinCallButtonRef.current) {
joinCallButtonRef.current.focus();
@@ -81,7 +81,7 @@ export function LobbyView(props: Props) {
<Body>Or</Body>
<CopyButton
variant="secondaryCopy"
value={getRoomUrl(props.matrixInfo.roomName)}
value={getRoomUrl(props.matrixInfo.roomId)}
className={styles.copyButton}
copiedMessage={t("Call link copied")}
data-testid="lobby_inviteLink"

View File

@@ -25,13 +25,13 @@ import { Body } from "../typography/Typography";
interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId: string;
roomIdOrAlias: string;
roomId: string;
onClose: () => void;
}
export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId,
roomIdOrAlias,
roomId,
...rest
}) => {
const { t } = useTranslation();
@@ -57,7 +57,7 @@ export const RageshakeRequestModal: FC<Props> = ({
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
roomId,
})
}
disabled={sending}

View File

@@ -26,15 +26,18 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Form } from "../form/Form";
import { UserMenuContainer } from "../UserMenuContainer";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { Config } from "../config/Config";
export function RoomAuthView() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } =
const { registerPasswordlessUser, recaptchaId } =
useRegisterPasswordlessUser();
const onSubmit = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(e) => {
e.preventDefault();
const data = new FormData(e.target);
@@ -83,7 +86,9 @@ export function RoomAuthView() {
<Caption>
<Trans>
By clicking "Join call now", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Link href={Config.get().eula}>
End User Licensing Agreement (EULA)
</Link>
</Trans>
</Caption>
{error && (

View File

@@ -18,7 +18,7 @@ import { FC, useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useClient } from "../ClientContext";
import { useClientLegacy } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
@@ -30,8 +30,6 @@ import { useOptInAnalytics } from "../settings/useSetting";
export const RoomPage: FC = () => {
const { t } = useTranslation();
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
const {
roomAlias,
@@ -52,39 +50,41 @@ export const RoomPage: FC = () => {
useEffect(() => {
// During the beta, opt into analytics by default
if (optInAnalytics === null) setOptInAnalytics(true);
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
}, [optInAnalytics, setOptInAnalytics]);
const { loading, authenticated, client, error, passwordlessUser } =
useClientLegacy();
useEffect(() => {
// If we've finished loading, are not already authed and we've been given a display name as
// a URL param, automatically register a passwordless user
if (!loading && !isAuthenticated && displayName) {
if (!loading && !authenticated && displayName) {
setIsRegistering(true);
registerPasswordlessUser(displayName).finally(() => {
setIsRegistering(false);
});
}
}, [
isAuthenticated,
loading,
authenticated,
displayName,
setIsRegistering,
registerPasswordlessUser,
loading,
]);
const groupCallView = useCallback(
(groupCall: GroupCall) => (
<GroupCallView
client={client}
roomIdOrAlias={roomIdOrAlias}
client={client!}
groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser}
isPasswordlessUser={passwordlessUser}
isEmbedded={isEmbedded}
preload={preload}
hideHeader={hideHeader}
/>
),
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
[client, passwordlessUser, isEmbedded, preload, hideHeader]
);
if (loading || isRegistering) {
@@ -95,7 +95,7 @@ export const RoomPage: FC = () => {
return <ErrorView error={error} />;
}
if (!isAuthenticated) {
if (!client) {
return <RoomAuthView />;
}

View File

@@ -1,44 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect } from "react";
import { useLocation, useHistory } from "react-router-dom";
import { Config } from "../config/Config";
import { LoadingView } from "../FullScreenView";
// A component that, when loaded, redirects the client to a full room URL
// based on the current URL being an abbreviated room URL
export function RoomRedirect() {
const { pathname } = useLocation();
const history = useHistory();
useEffect(() => {
let roomId = pathname;
if (pathname.startsWith("/")) {
roomId = roomId.substring(1, roomId.length);
}
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
roomId = `#${roomId}:${Config.defaultServerName()}`;
}
history.replace(`/room/${roomId.toLowerCase()}`);
}, [pathname, history]);
return <LoadingView />;
}

View File

@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useState, useEffect, useRef, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { OverlayTriggerState } from "@react-stately/overlays";
import { usePreviewDevice } from "@livekit/components-react";
import { usePreviewTracks } from "@livekit/components-react";
import { LocalAudioTrack, LocalVideoTrack, Track } from "livekit-client";
import { MicButton, SettingsButton, VideoButton } from "../button";
import { Avatar } from "../Avatar";
@@ -26,15 +27,15 @@ import styles from "./VideoPreview.module.css";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { useClient } from "../ClientContext";
import { useMediaDevices } from "../livekit/useMediaDevices";
import { DeviceChoices, UserChoices } from "../livekit/useLiveKit";
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
import { UserChoices } from "../livekit/useLiveKit";
import { useDefaultDevices } from "../settings/useSetting";
export type MatrixInfo = {
displayName: string;
avatarUrl: string;
roomId: string;
roomName: string;
roomIdOrAlias: string;
};
interface Props {
@@ -61,85 +62,111 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
settingsModalState.open();
}, [settingsModalState]);
// Fetch user media devices.
const mediaDevices = useMediaDevices();
// Create local media tracks.
const [videoEnabled, setVideoEnabled] = useState<boolean>(true);
const [audioEnabled, setAudioEnabled] = useState<boolean>(true);
const [videoId, audioId] = [
mediaDevices.videoIn.selectedId,
mediaDevices.audioIn.selectedId,
];
const [defaultDevices] = useDefaultDevices();
const video = usePreviewDevice(
videoEnabled,
videoId != "" ? videoId : defaultDevices.videoinput,
"videoinput"
// The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value.
// Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks.
const initialDefaultDevices = useRef(useDefaultDevices()[0]);
const tracks = usePreviewTracks(
{
audio: { deviceId: initialDefaultDevices.current.audioinput },
video: { deviceId: initialDefaultDevices.current.videoinput },
},
(error) => {
console.error("Error while creating preview Tracks:", error);
}
);
const audio = usePreviewDevice(
audioEnabled,
audioId != "" ? audioId : defaultDevices.audioinput,
"audioinput"
const videoTrack = React.useMemo(
() =>
tracks?.filter((t) => t.kind === Track.Kind.Video)[0] as LocalVideoTrack,
[tracks]
);
const audioTrack = React.useMemo(
() =>
tracks?.filter((t) => t.kind === Track.Kind.Audio)[0] as LocalAudioTrack,
[tracks]
);
const activeVideoId = video?.selectedDevice?.deviceId;
const activeAudioId = audio?.selectedDevice?.deviceId;
// Only let the MediaDeviceSwitcher request permissions if a video track is already available.
// Otherwise we would end up asking for permissions in usePreviewTracks and in useMediaDevicesSwitcher.
const requestPermissions = !!audioTrack && !!videoTrack;
const mediaSwitcher = useMediaDevicesSwitcher(
undefined,
{ videoTrack, audioTrack },
requestPermissions
);
const { videoIn, audioIn } = mediaSwitcher;
const videoEl = React.useRef(null);
useEffect(() => {
const createChoices = (
enabled: boolean,
deviceId?: string
): DeviceChoices | undefined => {
if (deviceId === undefined) {
return undefined;
}
return {
selectedId: deviceId,
enabled,
};
};
// Effect to update the settings
onUserChoicesChanged({
video: createChoices(videoEnabled, activeVideoId),
audio: createChoices(audioEnabled, activeAudioId),
video: {
selectedId: videoIn.selectedId,
enabled: videoEnabled,
},
audio: {
selectedId: audioIn.selectedId,
enabled: audioEnabled,
},
});
}, [
onUserChoicesChanged,
activeVideoId,
videoIn.selectedId,
videoEnabled,
activeAudioId,
audioIn.selectedId,
audioEnabled,
videoTrack,
audioTrack,
]);
const [selectVideo, selectAudio] = [
mediaDevices.videoIn.setSelected,
mediaDevices.audioIn.setSelected,
];
useEffect(() => {
if (activeVideoId && activeVideoId !== "") {
selectVideo(activeVideoId);
// Effect to update the initial device selection for the ui elements based on the current preview track.
if (!videoIn.selectedId || videoIn.selectedId == "") {
videoTrack?.getDeviceId().then((videoId) => {
videoIn.setSelected(videoId ?? "default");
});
}
if (activeAudioId && activeAudioId !== "") {
selectAudio(activeAudioId);
if (!audioIn.selectedId || audioIn.selectedId == "") {
audioTrack?.getDeviceId().then((audioId) => {
// getDeviceId() can return undefined for audio devices. This happens if
// the devices list uses "default" as the device id for the current
// device and the device set on the track also uses the deviceId
// "default". Check `normalizeDeviceId` in `getDeviceId` for more
// details.
audioIn.setSelected(audioId ?? "default");
});
}
}, [selectVideo, selectAudio, activeVideoId, activeAudioId]);
}, [videoIn, audioIn, videoTrack, audioTrack]);
const mediaElement = useRef(null);
useEffect(() => {
if (mediaElement.current) {
video?.localTrack?.attach(mediaElement.current);
// Effect to connect the videoTrack with the video element.
if (videoEl.current) {
videoTrack?.unmute();
videoTrack?.attach(videoEl.current);
}
return () => {
video?.localTrack?.detach();
videoTrack?.detach();
};
}, [video?.localTrack, mediaElement]);
}, [videoTrack]);
useEffect(() => {
// Effect to mute/unmute video track. (This has to be done, so that the hardware camera indicator does not confuse the user)
if (videoTrack && videoEnabled) {
videoTrack?.unmute();
} else if (videoTrack) {
videoTrack?.mute();
}
}, [videoEnabled, videoTrack]);
return (
<div className={styles.preview} ref={previewRef}>
<video ref={mediaElement} muted playsInline disablePictureInPicture />
<video ref={videoEl} muted playsInline disablePictureInPicture />
<>
{(video ? !videoEnabled : true) && (
{(videoTrack ? !videoEnabled : true) && (
<div className={styles.avatarContainer}>
<Avatar
size={(previewBounds.height - 66) / 2}
@@ -149,25 +176,23 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
</div>
)}
<div className={styles.previewButtons}>
{audio.localTrack && (
<MicButton
muted={!audioEnabled}
onPress={() => setAudioEnabled(!audioEnabled)}
/>
)}
{video.localTrack && (
<VideoButton
muted={!videoEnabled}
onPress={() => setVideoEnabled(!videoEnabled)}
/>
)}
<MicButton
muted={!audioEnabled}
onPress={() => setAudioEnabled(!audioEnabled)}
disabled={!audioTrack}
/>
<VideoButton
muted={!videoEnabled}
onPress={() => setVideoEnabled(!videoEnabled)}
disabled={!videoTrack}
/>
<SettingsButton onPress={openSettings} />
</div>
</>
{settingsModalState.isOpen && (
{settingsModalState.isOpen && client && (
<SettingsModal
client={client}
mediaDevices={mediaDevices}
mediaDevicesSwitcher={mediaSwitcher}
{...settingsModalProps}
/>
)}

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import { useCallback, useEffect, useReducer, useState } from "react";
import * as Sentry from "@sentry/react";
import {
GroupCallEvent,
GroupCallState,
@@ -58,12 +59,12 @@ export interface ParticipantInfo {
interface UseGroupCallReturnType {
state: GroupCallState;
localCallFeed: CallFeed;
activeSpeaker: CallFeed | null;
localCallFeed?: CallFeed;
activeSpeaker?: CallFeed;
userMediaFeeds: CallFeed[];
microphoneMuted: boolean;
localVideoMuted: boolean;
error: TranslatedError | null;
error?: TranslatedError;
initLocalCallFeed: () => void;
enter: () => Promise<void>;
leave: () => void;
@@ -74,23 +75,21 @@ interface UseGroupCallReturnType {
requestingScreenshare: boolean;
isScreensharing: boolean;
screenshareFeeds: CallFeed[];
localDesktopCapturerSourceId: string; // XXX: This looks unused?
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>;
otelGroupCallMembership: OTelGroupCallMembership;
otelGroupCallMembership?: OTelGroupCallMembership;
}
interface State {
state: GroupCallState;
localCallFeed: CallFeed;
activeSpeaker: CallFeed | null;
localCallFeed?: CallFeed;
activeSpeaker?: CallFeed;
userMediaFeeds: CallFeed[];
error: TranslatedError | null;
error?: TranslatedError;
microphoneMuted: boolean;
localVideoMuted: boolean;
screenshareFeeds: CallFeed[];
localDesktopCapturerSourceId: string;
isScreensharing: boolean;
requestingScreenshare: boolean;
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
@@ -101,7 +100,7 @@ interface State {
// level so that it doesn't pop in & out of existence as react mounts & unmounts
// components. The right solution is probably for this to live in the js-sdk and have
// the same lifetime as groupcalls themselves.
let groupCallOTelMembership: OTelGroupCallMembership;
let groupCallOTelMembership: OTelGroupCallMembership | undefined;
let groupCallOTelMembershipGroupCallId: string;
function getParticipants(
@@ -159,7 +158,6 @@ export function useGroupCall(
localVideoMuted,
isScreensharing,
screenshareFeeds,
localDesktopCapturerSourceId,
participants,
hasLocalParticipant,
requestingScreenshare,
@@ -167,15 +165,11 @@ export function useGroupCall(
setState,
] = useState<State>({
state: GroupCallState.LocalCallFeedUninitialized,
localCallFeed: null,
activeSpeaker: null,
userMediaFeeds: [],
error: null,
microphoneMuted: false,
localVideoMuted: false,
isScreensharing: false,
screenshareFeeds: [],
localDesktopCapturerSourceId: null,
requestingScreenshare: false,
participants: new Map(),
hasLocalParticipant: false,
@@ -248,12 +242,11 @@ export function useGroupCall(
updateState({
state: groupCall.state,
localCallFeed: groupCall.localCallFeed,
activeSpeaker: groupCall.activeSpeaker ?? null,
activeSpeaker: groupCall.activeSpeaker,
userMediaFeeds: [...groupCall.userMediaFeeds],
microphoneMuted: groupCall.isMicrophoneMuted(),
localVideoMuted: groupCall.isLocalVideoMuted(),
isScreensharing: groupCall.isScreensharing(),
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
screenshareFeeds: [...groupCall.screenshareFeeds],
});
}
@@ -303,7 +296,7 @@ export function useGroupCall(
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
updateState({
activeSpeaker: activeSpeaker ?? null,
activeSpeaker: activeSpeaker,
});
}
@@ -319,12 +312,11 @@ export function useGroupCall(
function onLocalScreenshareStateChanged(
isScreensharing: boolean,
_localScreenshareFeed: CallFeed,
localDesktopCapturerSourceId: string
_localScreenshareFeed?: CallFeed,
localDesktopCapturerSourceId?: string
): void {
updateState({
isScreensharing,
localDesktopCapturerSourceId,
});
}
@@ -340,6 +332,7 @@ export function useGroupCall(
}
function onError(e: GroupCallError): void {
Sentry.captureException(e);
if (e.code === GroupCallErrorCode.UnknownDevice) {
const unknownDeviceError = e as GroupCallUnknownDeviceError;
addUnencryptedEventUser(unknownDeviceError.userId);
@@ -405,15 +398,14 @@ export function useGroupCall(
);
updateState({
error: null,
error: undefined,
state: groupCall.state,
localCallFeed: groupCall.localCallFeed,
activeSpeaker: groupCall.activeSpeaker ?? null,
activeSpeaker: groupCall.activeSpeaker,
userMediaFeeds: [...groupCall.userMediaFeeds],
microphoneMuted: groupCall.isMicrophoneMuted(),
localVideoMuted: groupCall.isLocalVideoMuted(),
isScreensharing: groupCall.isScreensharing(),
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
screenshareFeeds: [...groupCall.screenshareFeeds],
participants: getParticipants(groupCall),
hasLocalParticipant: groupCall.hasLocalParticipant(),
@@ -516,7 +508,7 @@ export function useGroupCall(
}, [groupCall]);
const setMicrophoneMuted = useCallback(
(setMuted) => {
(setMuted: boolean) => {
groupCall.setMicrophoneMuted(setMuted);
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
PosthogAnalytics.instance.eventMuteMicrophone.track(
@@ -575,7 +567,7 @@ export function useGroupCall(
desktopCapturerSourceId: data.desktopCapturerSourceId as string,
audio: !data.desktopCapturerSourceId,
});
await widget.api.transport.reply(ev.detail, {});
await widget?.api.transport.reply(ev.detail, {});
},
[groupCall, updateState]
);
@@ -584,7 +576,7 @@ export function useGroupCall(
async (ev: CustomEvent<IWidgetApiRequest>) => {
updateState({ requestingScreenshare: false });
await groupCall.setScreensharingEnabled(false);
await widget.api.transport.reply(ev.detail, {});
await widget?.api.transport.reply(ev.detail, {});
},
[groupCall, updateState]
);
@@ -601,11 +593,11 @@ export function useGroupCall(
);
return () => {
widget.lazyActions.off(
widget?.lazyActions.off(
ElementWidgetActions.ScreenshareStart,
onScreenshareStart
);
widget.lazyActions.off(
widget?.lazyActions.off(
ElementWidgetActions.ScreenshareStop,
onScreenshareStop
);
@@ -644,7 +636,6 @@ export function useGroupCall(
requestingScreenshare,
isScreensharing,
screenshareFeeds,
localDesktopCapturerSourceId,
participants,
hasLocalParticipant,
unencryptedEventsFromUsers,

View File

@@ -34,8 +34,26 @@ import { widget } from "../widget";
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
export type GroupCallLoaded = {
kind: "loaded";
groupCall: GroupCall;
};
export type GroupCallLoadFailed = {
kind: "failed";
error: Error;
};
export type GroupCallLoading = {
kind: "loading";
};
export type GroupCallStatus =
| GroupCallLoaded
| GroupCallLoadFailed
| GroupCallLoading;
export interface GroupCallLoadState {
loading: boolean;
error?: Error;
groupCall?: GroupCall;
}
@@ -45,13 +63,11 @@ export const useLoadGroupCall = (
roomIdOrAlias: string,
viaServers: string[],
createPtt: boolean
): GroupCallLoadState => {
): GroupCallStatus => {
const { t } = useTranslation();
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
useEffect(() => {
setState({ loading: true });
const fetchOrCreateRoom = async (): Promise<Room> => {
try {
// We lowercase the localpart when we create the room, so we must lowercase
@@ -74,8 +90,14 @@ export const useLoadGroupCall = (
} catch (error) {
if (
isLocalRoomId(roomIdOrAlias, client) &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(error.errcode === "M_NOT_FOUND" ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(error.message &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
error.message.indexOf("Failed to fetch alias") !== -1))
) {
// The room doesn't exist, but we can create it
@@ -86,7 +108,7 @@ export const useLoadGroupCall = (
);
// likewise, wait for the room
await client.waitUntilRoomReadyForGroupCalls(roomId);
return client.getRoom(roomId);
return client.getRoom(roomId)!;
} else {
throw error;
}
@@ -170,12 +192,8 @@ export const useLoadGroupCall = (
waitForClientSyncing()
.then(fetchOrCreateGroupCall)
.then((groupCall) =>
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
)
.catch((error) =>
setState((prevState) => ({ ...prevState, loading: false, error }))
);
.then((groupCall) => setState({ kind: "loaded", groupCall }))
.catch((error) => setState({ kind: "failed", error }));
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
return state;

View File

@@ -55,14 +55,22 @@ export function usePageUnload(callback: () => void) {
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
if (isIOS()) {
window.addEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
document.addEventListener("visibilitychange", onBeforeUnload);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.addEventListener("beforeunload", onBeforeUnload);
return () => {
window.removeEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
document.removeEventListener("visibilitychange", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.removeEventListener("beforeunload", onBeforeUnload);
clearTimeout(pageVisibilityTimeout);
};

View File

@@ -35,6 +35,8 @@ export function FeedbackSettingsTab({ roomId }: Props) {
const sendRageshakeRequest = useRageshakeRequest();
const onSubmitFeedback = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(e) => {
e.preventDefault();
const data = new FormData(e.target);

View File

@@ -59,8 +59,14 @@ export function ProfileSettingsTab({ client }: Props) {
? displayNameDataEntry
: displayNameDataEntry?.name ?? null;
if (!displayName) {
return;
}
saveProfile({
displayName,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
});
@@ -71,14 +77,16 @@ export function ProfileSettingsTab({ client }: Props) {
return (
<form onChange={onFormChange} ref={formRef} className={styles.content}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
{displayName && (
<AvatarInputField
id="avatar"
name="avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
)}
</FieldRow>
<FieldRow>
<InputField

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ChangeEvent, useCallback, useState } from "react";
import { ChangeEvent, Key, useCallback, useState } from "react";
import { Item } from "@react-stately/collections";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk";
@@ -42,10 +42,14 @@ import { Body, Caption } from "../typography/Typography";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import { MediaDevices, MediaDevicesState } from "../livekit/useMediaDevices";
import {
MediaDevices,
MediaDevicesState,
} from "../livekit/useMediaDevicesSwitcher";
import { useUrlParams } from "../UrlParams";
interface Props {
mediaDevices?: MediaDevicesState;
mediaDevicesSwitcher?: MediaDevicesState;
isOpen: boolean;
client: MatrixClient;
roomId?: string;
@@ -56,6 +60,8 @@ interface Props {
export const SettingsModal = (props: Props) => {
const { t } = useTranslation();
const { isEmbedded } = useUrlParams();
const [showInspector, setShowInspector] = useShowInspector();
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const [developerSettingsTab, setDeveloperSettingsTab] =
@@ -72,7 +78,11 @@ export const SettingsModal = (props: Props) => {
return (
<SelectInput
label={caption}
selectedKey={devices.selectedId}
selectedKey={
devices.selectedId === "" || !devices.selectedId
? "default"
: devices.selectedId
}
onSelectionChange={(id) => devices.setSelected(id.toString())}
>
{devices.available.map(({ deviceId, label }, index) => (
@@ -89,8 +99,8 @@ export const SettingsModal = (props: Props) => {
const [selectedTab, setSelectedTab] = useState<string | undefined>();
const onSelectedTabChanged = useCallback(
(tab) => {
setSelectedTab(tab);
(tab: Key) => {
setSelectedTab(tab.toString());
},
[setSelectedTab]
);
@@ -106,7 +116,156 @@ export const SettingsModal = (props: Props) => {
</Caption>
);
const devices = props.mediaDevices;
const devices = props.mediaDevicesSwitcher;
const audioTab = (
<TabItem
key="audio"
title={
<>
<AudioIcon width={16} height={16} />
<span className={styles.tabLabel}>{t("Audio")}</span>
</>
}
>
{devices && generateDeviceSelection(devices.audioIn, t("Microphone"))}
{devices && generateDeviceSelection(devices.audioOut, t("Speaker"))}
</TabItem>
);
const videoTab = (
<TabItem
key="video"
title={
<>
<VideoIcon width={16} height={16} />
<span>{t("Video")}</span>
</>
}
>
{devices && generateDeviceSelection(devices.videoIn, t("Camera"))}
</TabItem>
);
const profileTab = (
<TabItem
key="profile"
title={
<>
<UserIcon width={15} height={15} />
<span>{t("Profile")}</span>
</>
}
>
<ProfileSettingsTab client={props.client} />
</TabItem>
);
const feedbackTab = (
<TabItem
key="feedback"
title={
<>
<FeedbackIcon width={16} height={16} />
<span>{t("Feedback")}</span>
</>
}
>
<FeedbackSettingsTab roomId={props.roomId} />
</TabItem>
);
const moreTab = (
<TabItem
key="more"
title={
<>
<OverflowIcon width={16} height={16} />
<span>{t("More")}</span>
</>
}
>
<h4>Developer</h4>
<p>Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}</p>
<FieldRow>
<InputField
id="developerSettingsTab"
type="checkbox"
checked={developerSettingsTab}
label={t("Developer Settings")}
description={t("Expose developer settings in the settings window.")}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setDeveloperSettingsTab(event.target.checked)
}
/>
</FieldRow>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics ?? undefined}
description={optInDescription}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setOptInAnalytics?.(event.target.checked);
}}
/>
</FieldRow>
</TabItem>
);
const developerTab = (
<TabItem
key="developer"
title={
<>
<DeveloperIcon width={16} height={16} />
<span>{t("Developer")}</span>
</>
}
>
<FieldRow>
<Body className={styles.fieldRowText}>
{t("Version: {{version}}", {
version: import.meta.env.VITE_APP_VERSION || "dev",
})}
</Body>
</FieldRow>
<FieldRow>
<InputField
id="showInspector"
name="inspector"
label={t("Show call inspector")}
type="checkbox"
checked={showInspector}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setShowInspector(e.target.checked)
}
/>
</FieldRow>
<FieldRow>
<InputField
id="showConnectionStats"
name="connection-stats"
label={t("Show connection stats")}
type="checkbox"
checked={showConnectionStats}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setShowConnectionStats(e.target.checked)
}
/>
</FieldRow>
<FieldRow>
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
</FieldRow>
</TabItem>
);
const tabs: JSX.Element[] = [];
tabs.push(audioTab, videoTab);
if (!isEmbedded) tabs.push(profileTab);
tabs.push(feedbackTab, moreTab);
if (developerSettingsTab) tabs.push(developerTab);
return (
<Modal
@@ -121,139 +280,7 @@ export const SettingsModal = (props: Props) => {
selectedKey={selectedTab ?? props.defaultTab ?? "audio"}
className={styles.tabContainer}
>
<TabItem
key="audio"
title={
<>
<AudioIcon width={16} height={16} />
<span className={styles.tabLabel}>{t("Audio")}</span>
</>
}
>
{devices && generateDeviceSelection(devices.audioIn, t("Microphone"))}
{devices && generateDeviceSelection(devices.audioOut, t("Speaker"))}
</TabItem>
<TabItem
key="video"
title={
<>
<VideoIcon width={16} height={16} />
<span>{t("Video")}</span>
</>
}
>
{devices && generateDeviceSelection(devices.videoIn, t("Camera"))}
</TabItem>
<TabItem
key="profile"
title={
<>
<UserIcon width={15} height={15} />
<span>{t("Profile")}</span>
</>
}
>
<ProfileSettingsTab client={props.client} />
</TabItem>
<TabItem
key="feedback"
title={
<>
<FeedbackIcon width={16} height={16} />
<span>{t("Feedback")}</span>
</>
}
>
<FeedbackSettingsTab roomId={props.roomId} />
</TabItem>
<TabItem
key="more"
title={
<>
<OverflowIcon width={16} height={16} />
<span>{t("More")}</span>
</>
}
>
<h4>Developer</h4>
<p>
Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}
</p>
<FieldRow>
<InputField
id="developerSettingsTab"
type="checkbox"
checked={developerSettingsTab}
label={t("Developer Settings")}
description={t(
"Expose developer settings in the settings window."
)}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setDeveloperSettingsTab(event.target.checked)
}
/>
</FieldRow>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}
/>
</FieldRow>
</TabItem>
{developerSettingsTab && (
<TabItem
key="developer"
title={
<>
<DeveloperIcon width={16} height={16} />
<span>{t("Developer")}</span>
</>
}
>
<FieldRow>
<Body className={styles.fieldRowText}>
{t("Version: {{version}}", {
version: import.meta.env.VITE_APP_VERSION || "dev",
})}
</Body>
</FieldRow>
<FieldRow>
<InputField
id="showInspector"
name="inspector"
label={t("Show call inspector")}
type="checkbox"
checked={showInspector}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setShowInspector(e.target.checked)
}
/>
</FieldRow>
<FieldRow>
<InputField
id="showConnectionStats"
name="connection-stats"
label={t("Show connection stats")}
type="checkbox"
checked={showConnectionStats}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setShowConnectionStats(e.target.checked)
}
/>
</FieldRow>
<FieldRow>
<Button onPress={downloadDebugLog}>
{t("Download debug logs")}
</Button>
</FieldRow>
</TabItem>
)}
{tabs}
</TabContainer>
</Modal>
);

View File

@@ -79,11 +79,17 @@ class ConsoleLogger extends EventEmitter {
warn: "W",
error: "E",
};
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
const level = consoleFunctionsToLevels[fnName];
const originalFn = consoleObj[fnName].bind(consoleObj);
this.originalFunctions[fnName] = originalFn;
consoleObj[fnName] = (...args) => {
Object.entries(consoleFunctionsToLevels).forEach(([name, level]) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const originalFn = consoleObj[name].bind(consoleObj);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.originalFunctions[name] = originalFn;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
consoleObj[name] = (...args) => {
this.log(level, ...args);
originalFn(...args);
};
@@ -147,9 +153,9 @@ class ConsoleLogger extends EventEmitter {
// A class which stores log lines in an IndexedDB instance.
class IndexedDBLogStore {
private index = 0;
private db: IDBDatabase = null;
private flushPromise: Promise<void> = null;
private flushAgainPromise: Promise<void> = null;
private db?: IDBDatabase;
private flushPromise?: Promise<void>;
private flushAgainPromise?: Promise<void>;
private id: string;
constructor(
@@ -175,7 +181,7 @@ class IndexedDBLogStore {
};
req.onerror = () => {
const err = "Failed to open log database: " + req.error.name;
const err = "Failed to open log database: " + req?.error?.name;
logger.error(err);
reject(new Error(err));
};
@@ -264,7 +270,7 @@ class IndexedDBLogStore {
return this.flush();
})
.then(() => {
this.flushAgainPromise = null;
this.flushAgainPromise = undefined;
});
return this.flushAgainPromise;
}
@@ -288,13 +294,13 @@ class IndexedDBLogStore {
};
txn.onerror = (event) => {
logger.error("Failed to flush logs : ", event);
reject(new Error("Failed to write logs: " + txn.error.message));
reject(new Error("Failed to write logs: " + txn?.error?.message));
};
objStore.add(this.generateLogEntry(lines));
const lastModStore = txn.objectStore("logslastmod");
lastModStore.put(this.generateLastModifiedTime());
}).then(() => {
this.flushPromise = null;
this.flushPromise = undefined;
});
return this.flushPromise;
};
@@ -311,11 +317,14 @@ class IndexedDBLogStore {
*/
public async consume(): Promise<LogEntry[]> {
const db = this.db;
if (!db) {
return Promise.reject(new Error("No connected database"));
}
// Returns: a string representing the concatenated logs for this ID.
// Stops adding log fragments when the size exceeds maxSize
function fetchLogs(id: string, maxSize: number): Promise<string> {
const objectStore = db
const objectStore = db!
.transaction("logs", "readonly")
.objectStore("logs");
@@ -325,7 +334,7 @@ class IndexedDBLogStore {
.openCursor(IDBKeyRange.only(id), "prev");
let lines = "";
query.onerror = () => {
reject(new Error("Query failed: " + query.error.message));
reject(new Error("Query failed: " + query?.error?.message));
};
query.onsuccess = () => {
const cursor = query.result;
@@ -346,7 +355,7 @@ class IndexedDBLogStore {
// Returns: A sorted array of log IDs. (newest first)
function fetchLogIds(): Promise<string[]> {
// To gather all the log IDs, query for all records in logslastmod.
const o = db
const o = db!
.transaction("logslastmod", "readonly")
.objectStore("logslastmod");
return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
@@ -366,7 +375,7 @@ class IndexedDBLogStore {
function deleteLogs(id: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
const txn = db.transaction(["logs", "logslastmod"], "readwrite");
const txn = db!.transaction(["logs", "logslastmod"], "readwrite");
const o = txn.objectStore("logs");
// only load the key path, not the data which may be huge
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
@@ -384,7 +393,7 @@ class IndexedDBLogStore {
txn.onerror = () => {
reject(
new Error(
"Failed to delete logs for " + `'${id}' : ${txn.error.message}`
"Failed to delete logs for " + `'${id}' : ${txn?.error?.message}`
)
);
};
@@ -395,7 +404,7 @@ class IndexedDBLogStore {
}
const allLogIds = await fetchLogIds();
let removeLogIds = [];
let removeLogIds: number[] = [];
const logs: LogEntry[] = [];
let size = 0;
for (let i = 0; i < allLogIds.length; i++) {
@@ -414,7 +423,7 @@ class IndexedDBLogStore {
if (size >= MAX_LOG_SIZE) {
// the remaining log IDs should be removed. If we go out of
// bounds this is just []
removeLogIds = allLogIds.slice(i + 1);
removeLogIds = allLogIds.slice(i + 1).map((id) => parseInt(id, 10));
break;
}
}
@@ -462,14 +471,14 @@ class IndexedDBLogStore {
*/
function selectQuery<T>(
store: IDBObjectStore,
keyRange: IDBKeyRange,
keyRange: IDBKeyRange | undefined,
resultMapper: (cursor: IDBCursorWithValue) => T
): Promise<T[]> {
const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => {
const results = [];
const results: T[] = [];
query.onerror = () => {
reject(new Error("Query failed: " + query.error.message));
reject(new Error("Query failed: " + query?.error?.message));
};
// collect results
query.onsuccess = () => {

Some files were not shown because too many files have changed in this diff Show More