Compare commits

..

142 Commits

Author SHA1 Message Date
Robin
cb39e760ab Merge pull request #1761 from vector-im/renovate/docker-setup-buildx-action-digest
Update docker/setup-buildx-action digest to 5d98624
2023-10-13 10:37:31 -04:00
Robin
be9591c5b5 Merge pull request #1760 from vector-im/renovate/docker-build-push-action-digest
Update docker/build-push-action digest to fdf7f43
2023-10-13 10:37:14 -04:00
David Baker
d94c41228f Merge pull request #1755 from vector-im/dbkr/remove_e2ee_setting
Remove E2EE setting
2023-10-13 15:37:01 +01:00
Robin
89e8962515 Merge pull request #1758 from vector-im/renovate/sentry-javascript-monorepo
Update sentry-javascript monorepo to v7.74.0
2023-10-13 10:36:24 -04:00
David Baker
ea1c2e9ec3 Merge remote-tracking branch 'origin/livekit' into dbkr/remove_e2ee_setting 2023-10-13 15:26:30 +01:00
David Baker
e86f9b77fc Merge pull request #1754 from vector-im/dbkr/remove_e2ee_banner
Remove E2EEBanner
2023-10-13 15:18:51 +01:00
David Baker
6ef4ce6d29 Merge pull request #1756 from vector-im/dbkr/safari_screenshare
Re-enable screen sharing on Safari
2023-10-13 15:18:27 +01:00
renovate[bot]
d12d7cf28d Update docker/setup-buildx-action digest to 5d98624 2023-10-13 14:09:00 +00:00
renovate[bot]
4f426808cf Update docker/build-push-action digest to fdf7f43 2023-10-13 14:08:55 +00:00
Robin
0993294925 Merge pull request #1757 from vector-im/renovate/react-i18next-13.x-lockfile
Update dependency react-i18next to v13.3.0
2023-10-13 10:08:37 -04:00
David Baker
777daaf209 Merge pull request #1759 from vector-im/dbkr/fix_using_non_default_device
Fix using a non-default audio device
2023-10-13 13:38:15 +01:00
David Baker
2faf9527a0 Fix using a non-default audio device
We were passing the output option when we wanted the input, so the
mic track pre-creation would just always use the system default.
2023-10-13 13:34:25 +01:00
David Baker
1b7354ff5c Merge pull request #1752 from vector-im/renovate/node-18.x-lockfile
Update dependency @types/node to v18.18.5
2023-10-13 13:13:01 +01:00
renovate[bot]
8b61cc49c9 Update sentry-javascript monorepo to v7.74.0 2023-10-13 12:12:53 +00:00
renovate[bot]
a7b74a65d9 Update dependency react-i18next to v13.3.0 2023-10-13 12:12:38 +00:00
Robin
74c381a5c3 Merge pull request #1746 from vector-im/renovate/eslint-plugin-deprecate-0.x-lockfile
Update dependency eslint-plugin-deprecate to v0.8.4
2023-10-13 08:12:12 -04:00
David Baker
42d9fe1962 Merge pull request #1720 from vector-im/dbkr/write_key_with_right_roomid
Always store room passwords with the right room ID
2023-10-13 11:35:38 +01:00
David Baker
aac92c18b3 Re-enable screen sharing on Safari
Appears to work fine now, and no reason to think it shouldn't on
Livekit.
2023-10-13 11:02:20 +01:00
David Baker
61d7adf0d4 Merge pull request #1740 from vector-im/dbkr/log_mic_and_focus
Add logging & guards for mic pre-creation & focus
2023-10-13 10:34:41 +01:00
David Baker
ac7a39d23f Merge pull request #1753 from vector-im/renovate/livekit-client-1.x-lockfile
Update dependency livekit-client to v1.14.0
2023-10-13 10:34:23 +01:00
David Baker
5ef208e789 Remove E2EE setting
Since e2ee is enabled by default now
2023-10-13 10:30:06 +01:00
David Baker
515a73ce30 i18n 2023-10-13 10:06:36 +01:00
David Baker
32657084aa Remove E2EEBanner
We have e2ee now
2023-10-13 10:04:54 +01:00
renovate[bot]
f7773c1eb9 Update dependency livekit-client to v1.14.0 2023-10-13 03:23:43 +00:00
renovate[bot]
18ce30ca0f Update dependency @types/node to v18.18.5 2023-10-12 22:56:38 +00:00
Robin
f412729696 Merge pull request #1748 from vector-im/renovate/vector-im-compound-web-0.x-lockfile
Update dependency @vector-im/compound-web to v0.5.3
2023-10-12 11:58:47 -04:00
Robin
1ba332ecbf Merge pull request #1750 from vector-im/renovate/docker-build-push-action-digest
Update docker/build-push-action digest to 8d2cf95
2023-10-12 11:57:43 -04:00
renovate[bot]
f84747e83b Update dependency @vector-im/compound-web to v0.5.3 2023-10-12 15:56:47 +00:00
Robin
e748137f32 Merge pull request #1745 from vector-im/renovate/testing-library-jest-dom-6.x-lockfile
Update dependency @testing-library/jest-dom to v6.1.4
2023-10-12 11:56:18 -04:00
Robin
b09d8ce8c2 Remove workaround for linter crash 2023-10-12 11:56:01 -04:00
renovate[bot]
ecb49ea9e6 Update dependency @testing-library/jest-dom to v6.1.4 2023-10-12 15:54:04 +00:00
Robin
fd74772e12 Merge pull request #1744 from vector-im/renovate/sass-1.x-lockfile
Update dependency sass to v1.69.3
2023-10-12 11:53:56 -04:00
Robin
deaf7e512c Merge pull request #1743 from vector-im/renovate/babel-monorepo
Update babel monorepo to v7.23.2
2023-10-12 11:53:37 -04:00
Robin
020f732671 Merge pull request #1749 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-10-12 11:51:24 -04:00
renovate[bot]
8d07d2ec48 Update docker/build-push-action digest to 8d2cf95 2023-10-12 13:27:12 +00:00
LinAGKar
61db641875 Translated using Weblate (Swedish)
Currently translated at 4.9% (6 of 121 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sv/
2023-10-12 11:45:39 +00:00
renovate[bot]
2985e06a41 Update dependency eslint-plugin-deprecate to v0.8.4 2023-10-12 08:52:06 +00:00
Timo
5262af7000 Fix sync loop by adding a 20ms break for the next mute sync (#1742)
* fix sync loop by adding a 20ms break for the next mute sync

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-12 10:51:37 +02:00
renovate[bot]
4ab4873c35 Update dependency sass to v1.69.3 2023-10-12 02:01:03 +00:00
renovate[bot]
8c048f0c08 Update babel monorepo to v7.23.2 2023-10-12 02:00:50 +00:00
David Baker
d579acd21f Even prettier 2023-10-11 16:29:08 +01:00
David Baker
11664a5bf6 Prettier 2023-10-11 16:27:17 +01:00
David Baker
d058f08c47 Prettier 2023-10-11 16:25:47 +01:00
David Baker
4c742d0ac4 Merge remote-tracking branch 'origin/livekit' into dbkr/write_key_with_right_roomid 2023-10-11 16:14:24 +01:00
David Baker
9d4ade97b0 Remove redundant check
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
2023-10-11 16:10:03 +01:00
David Baker
a9c74172a5 Add logging & guards for mic pre-creation & focus
Logs & guard for pre-recating the mic track as well as logging what
we select as the active focus (JWT URL + livekit alias).
2023-10-11 16:07:46 +01:00
Robin
94c4b4fd6a Merge pull request #1727 from vector-im/renovate/opentelemetry-instrumentation-user-interaction-0.x-lockfile
Update dependency @opentelemetry/instrumentation-user-interaction to v0.33.2
2023-10-11 11:06:28 -04:00
Robin
1a4e30a274 Merge pull request #1739 from vector-im/renovate/postcss-preset-env-9.x-lockfile
Update dependency postcss-preset-env to v9.2.0
2023-10-11 10:57:47 -04:00
Robin
fd16073c2e Merge pull request #1714 from vector-im/renovate/vite-plugin-html-template-1.x-lockfile
Update dependency vite-plugin-html-template to v1.2.1
2023-10-11 10:51:00 -04:00
Robin
5dee63d815 Merge pull request #1706 from vector-im/renovate/sass-1.x-lockfile
Update dependency sass to v1.69.2
2023-10-11 10:50:25 -04:00
Robin
ddf174c01a Merge pull request #1710 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-10-11 10:50:02 -04:00
Robin
6c2260f9da Merge pull request #1711 from vector-im/renovate/eslint-8.x-lockfile
Update dependency eslint to v8.51.0
2023-10-11 10:49:32 -04:00
renovate[bot]
227d433978 Update dependency @opentelemetry/instrumentation-user-interaction to v0.33.2 2023-10-11 14:47:59 +00:00
Robin
af13b27be5 Merge pull request #1726 from vector-im/renovate/opentelemetry-instrumentation-document-load-0.x-lockfile
Update dependency @opentelemetry/instrumentation-document-load to v0.33.2
2023-10-11 10:47:31 -04:00
Robin
f6de03585b Merge pull request #1738 from vector-im/renovate/eslint-plugin-deprecate-0.x-lockfile
Update dependency eslint-plugin-deprecate to v0.8.3
2023-10-11 10:46:47 -04:00
Robin
772c0655dc Merge pull request #1735 from vector-im/renovate/typescript-eslint-monorepo
Update typescript-eslint monorepo to v6.7.5
2023-10-11 10:46:20 -04:00
renovate[bot]
bc109a417d Update dependency postcss-preset-env to v9.2.0 2023-10-11 14:45:49 +00:00
Robin
e06ddff8bd Merge pull request #1621 from vector-im/renovate/prettier-3.x
Update dependency prettier to v3
2023-10-11 10:45:16 -04:00
Robin
614bc82402 Format code 2023-10-11 10:42:04 -04:00
renovate[bot]
b28e465122 Update dependency prettier to v3 2023-10-11 14:38:05 +00:00
renovate[bot]
e424d3698e Update dependency eslint-plugin-deprecate to v0.8.3 2023-10-11 14:33:23 +00:00
Robin
ec35f655e7 Merge pull request #1574 from robintown/eslint-upgrade
Upgrade eslint-plugin-matrix-org to 1.2.1
2023-10-11 10:32:54 -04:00
Robin
cc6f1f8631 Merge branch 'livekit' into eslint-upgrade 2023-10-11 10:30:57 -04:00
renovate[bot]
975d8a3adc Update typescript-eslint monorepo to v6.7.5 2023-10-11 13:01:40 +00:00
renovate[bot]
17be0578bc Update dependency @types/request to v2.48.10 (#1728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 15:01:30 +02:00
renovate[bot]
3964b34596 Update dependency vaul to v0.7.1 (#1729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 15:01:03 +02:00
David Baker
59cd0c87cd Merge remote-tracking branch 'origin/livekit' into dbkr/write_key_with_right_roomid 2023-10-11 12:53:54 +01:00
David Baker
6039253a32 Reafctor a bit 2023-10-11 12:53:33 +01:00
David Baker
5900b76be2 Merge pull request #1694 from vector-im/renovate/posthog-js-1.x-lockfile
Update dependency posthog-js to v1.83.0
2023-10-11 11:48:21 +01:00
raspin0
0e5005f846 Translated using Weblate (Polish)
Currently translated at 100.0% (121 of 121 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-10-11 10:47:56 +00:00
David Baker
d9ea66f091 Merge pull request #1712 from vector-im/renovate/node-18.x-lockfile
Update dependency @types/node to v18.18.4
2023-10-11 11:47:42 +01:00
David Baker
908b466b1e Merge pull request #1713 from vector-im/renovate/uuid-9.x-lockfile
Update dependency @types/uuid to v9.0.5
2023-10-11 11:47:14 +01:00
renovate[bot]
a94009043b Update dependency @opentelemetry/instrumentation-document-load to v0.33.2 2023-10-11 10:21:34 +00:00
David Baker
be36ce43e0 Merge pull request #1716 from vector-im/renovate/docker-build-push-action-digest
Update docker/build-push-action digest to 0f84726
2023-10-11 11:21:06 +01:00
renovate[bot]
2970071aa5 Update dependency sass to v1.69.2 2023-10-10 22:06:54 +00:00
David Baker
d575ea4117 Merge pull request #1722 from vector-im/dbkr/dont_use_sender
Don't use event.sender
2023-10-10 17:20:35 +01:00
David Baker
fbb2dc2afd Update to merged js-sdk commit 2023-10-10 17:17:16 +01:00
David Baker
51f87fa42a Add comment
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
2023-10-10 17:06:49 +01:00
Timo
d5edcce470 Fix mute button not being in sync with actual video/audio feed. (#1721)
* Fix mute button not being in sync with actual video/audio feed.
This happens if we toggle the button while waiting for updating the stream.
It is prohibited by checking if the stream state is in sync after the update
is done.
Signed-off-by: Timo K <toger5@hotmail.de>


---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-10 14:14:39 +02:00
David Baker
7ab69435e5 Merge pull request #1717 from vector-im/dbkr/fix_url_password_param
Use base64url encoding for the password param
2023-10-10 11:07:45 +01:00
renovate[bot]
73e11b4084 Update dependency posthog-js to v1.83.0 2023-10-10 00:59:15 +00:00
David Baker
07cde7ee4d Don't use event.sender
Pull in a js-sdk change to avoid using event.sender (see js-sdk PR
for details).

Fixes https://github.com/vector-im/element-call/issues/1697
2023-10-09 20:49:03 +01:00
David Baker
d7b33ee959 Always store room passwords with the right room ID
Take the room ID from the URL rather than just assuming it's still
the one that was in URL params before: if only the hash changes,
the app won't reload.

Fixes https://github.com/vector-im/element-call/issues/1708
2023-10-09 17:43:50 +01:00
David Baker
df93fb4a3f Add comment 2023-10-09 16:35:27 +01:00
David Baker
6faceb07cd Log if password needed url encoding 2023-10-09 16:28:48 +01:00
David Baker
0892edc432 Use base64url encoding for the password param
As base64 is fairly obviously not sensible for URLs and we were not
URL encoding it so we were ending up with spaces in the URL.

Also base 64 encode the password in case, as per comment.
2023-10-09 10:08:10 +01:00
renovate[bot]
0c4430b72c Update docker/build-push-action digest to 0f84726 2023-10-09 08:28:42 +00:00
renovate[bot]
1d7e9d1a0b Update dependency vite-plugin-html-template to v1.2.1 2023-10-07 08:09:20 +00:00
renovate[bot]
bb9c453eac Update dependency @types/uuid to v9.0.5 2023-10-07 01:59:11 +00:00
renovate[bot]
4b066269eb Update dependency @types/node to v18.18.4 2023-10-07 01:58:57 +00:00
renovate[bot]
192b6a9d9e Update dependency eslint to v8.51.0 2023-10-06 23:01:39 +00:00
David Baker
e2abeba194 Merge pull request #1705 from vector-im/dbkr/use_secure_random
Generate call passwords with secure RNG
2023-10-06 16:20:50 +01:00
David Baker
e9798441f7 Merge remote-tracking branch 'origin/livekit' into dbkr/use_secure_random 2023-10-06 16:18:53 +01:00
David Baker
bc36acafc8 Merge pull request #1704 from vector-im/dbkr/refactor_room_create
Refactor room creation code a little
2023-10-06 16:18:18 +01:00
David Baker
f2435f1c31 More consistent variable naming 2023-10-06 16:15:16 +01:00
David Baker
715c5c73ca Merge remote-tracking branch 'origin/livekit' into dbkr/refactor_room_create 2023-10-06 15:15:30 +01:00
David Baker
be4afaeb7e Merge pull request #1687 from vector-im/dbkr/update_default_device
Switch capture devices if the default device changes
2023-10-06 12:01:46 +01:00
David Baker
44e604aaa1 Merge pull request #1703 from vector-im/dbkr/keep_password_in_url
Keep the password in the URL
2023-10-06 10:55:12 +01:00
David Baker
87d5062d34 Don't use js-sdk's base64 encode function
It uses the NodeJS Buffer global which presumably is provided by
Webpack in element-web but isn't here, apparently.
2023-10-05 17:57:23 +01:00
David Baker
d373081db1 Generate call passwords with secure RNG 2023-10-05 17:32:43 +01:00
David Baker
6481b2f67e Merge branch 'dbkr/keep_password_in_url' into dbkr/refactor_room_create 2023-10-05 17:27:03 +01:00
David Baker
b646b0ae56 Remove extra function
that was now doing exactly the same thing as the one above it.
2023-10-05 17:25:06 +01:00
David Baker
e63721acea Refactor room creation code a little
We c+ped the code to create room passwords between two places, but we
already had a createRoom utility function that knew about e2ee.
2023-10-05 16:44:31 +01:00
David Baker
4984bd630e Keep the password in the URL
We changed our minds: people do copy the URL from the bar and
give that to people and expect it to work: it doesn't make sense
to prioritise shorter URLs over this. There's no security advantage
unless we think there's a risk someone might steal your key by taking
a photo of your monitor over your shoulder and decrypting the calls
they can't already hear by standing behind you.
2023-10-05 16:13:56 +01:00
renovate[bot]
847789dcda Update dependency @sentry/vite-plugin to v2.8.0 (#1701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 17:13:26 +02:00
renovate[bot]
d1cb6ee889 Update dependency vaul to ^0.7.0 (#1692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 16:25:27 +02:00
renovate[bot]
7fbd84a63c Update dependency vite to v4.4.11 (#1699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 16:24:29 +02:00
Timo
63a00eef2f await leave rtc session (#1648)
so that the widget is only getting the hangup even,
once the call has been cleaned up

Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-04 18:27:07 +02:00
Timo
c18dce3617 Make sure roomAlias = null in widget mode (#1676)
Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-04 15:56:57 +02:00
Stefan Ceriu
1eb2302060 Move apple-app-site-association to .well-known
https://developer.apple.com/videos/play/wwdc2019/717/

```
This file should be located at HTTPS://your domain name/.well-known/apple-app-site-association

Other paths are deprecated.
```
2023-10-04 16:40:49 +03:00
Stefan Ceriu
ad462f3d8e Fix apple-app-site-assoctiation no_universal_link query matching.
https://developer.apple.com/videos/play/wwdc2019/717/

```
You'll notice that I specify a question mark and an asterisk as the pattern from the query items value. A pattern consisting of a single asterisk matches any string, including the empty string. And a missing query item has a value equivalent to the empty string. So to match against the string that's at least one character long, I specify a question mark and then any additional characters are matched by the asterisk.
```
2023-10-04 16:40:49 +03:00
Robin
a3eb58f9fe Merge pull request #1688 from vector-im/renovate/vite-4.x-lockfile
Update dependency vite to v4.4.10
2023-10-03 16:29:34 -04:00
Robin
50b4d61fbd Merge pull request #1684 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-10-03 16:29:10 -04:00
Robin
d0eda79f27 Merge pull request #1691 from vector-im/renovate/vector-im-compound-web-0.x
Update dependency @vector-im/compound-web to ^0.5.0
2023-10-03 16:28:44 -04:00
Robin
a0cc7686b3 Merge pull request #1678 from vector-im/renovate/posthog-js-1.x-lockfile
Update dependency posthog-js to v1.81.3
2023-10-03 16:25:41 -04:00
renovate[bot]
20f96f17e4 Update dependency @vector-im/compound-web to ^0.5.0 2023-10-03 20:25:25 +00:00
random
1b109e1b3a Translated using Weblate (Italian)
Currently translated at 100.0% (121 of 121 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/it/
2023-10-03 19:43:00 +00:00
Jeff Huang
daa1fed0c0 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (121 of 121 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-10-03 19:43:00 +00:00
Timo
01b2367f38 allow widget related params in the fragment (#1675)
Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-03 21:35:28 +02:00
renovate[bot]
2961d588b6 Update dependency vite to v4.4.10 2023-10-03 19:29:34 +00:00
David Baker
c37b2924af Comment 2023-10-03 18:27:10 +01:00
David Baker
e0cabbc514 Switch capture devices if the default device changes
This is a bit of a hack, but is the only way I can see that we can
update to using the new default device when the OS-level default
changes. Hopefully the comments explain everything.
2023-10-03 18:22:56 +01:00
Robin
e54a1274bb Merge pull request #1679 from vector-im/renovate/livekit-components-react-1.x-lockfile
Update dependency @livekit/components-react to v1.3.0
2023-10-03 07:54:25 -04:00
Robin
e246f3f66b Merge pull request #1667 from vector-im/renovate/sentry-javascript-monorepo
Update sentry-javascript monorepo to v7.73.0
2023-10-03 07:52:31 -04:00
Robin
c769a1b86b Merge pull request #1671 from vector-im/renovate/typescript-eslint-monorepo
Update typescript-eslint monorepo to v6.7.4
2023-10-03 07:51:40 -04:00
renovate[bot]
bbc58502da Update dependency @livekit/components-react to v1.3.0 2023-10-03 11:51:28 +00:00
renovate[bot]
72ab839eff Update dependency posthog-js to v1.81.3 2023-10-03 11:49:11 +00:00
Robin
aea404588a Merge pull request #1677 from vector-im/renovate/node-18.x-lockfile
Update dependency @types/node to v18.18.3
2023-10-03 07:48:37 -04:00
renovate[bot]
b3c0a01429 Update dependency @types/node to v18.18.3 2023-10-02 21:25:51 +00:00
renovate[bot]
27fa35cbab Update typescript-eslint monorepo to v6.7.4 2023-10-02 17:32:32 +00:00
Robin
f779bc26cd Merge pull request #1666 from vector-im/renovate/i18next-parser-8.x-lockfile
Update dependency i18next-parser to v8.8.0
2023-10-02 10:36:30 -04:00
Robin
6b94e3553c Merge pull request #1665 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-10-02 10:35:33 -04:00
renovate[bot]
13579d5972 Update sentry-javascript monorepo to v7.73.0 2023-10-02 14:00:44 +00:00
renovate[bot]
47c1740504 Update dependency i18next-parser to v8.8.0 2023-10-01 17:49:06 +00:00
Ihor Hordiichuk
21789f7d22 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (121 of 121 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-10-01 04:26:55 +00:00
Robin
67ea390847 Merge pull request #1664 from vector-im/renovate/vite-plugin-svgr-4.x-lockfile
Update dependency vite-plugin-svgr to v4.1.0
2023-09-29 22:27:41 -04:00
Robin
e501c5305f Merge pull request #1662 from vector-im/renovate/node-18.x-lockfile
Update dependency @types/node to v18.18.1
2023-09-29 22:26:50 -04:00
renovate[bot]
d3704dab33 Update dependency vite-plugin-svgr to v4.1.0 2023-09-29 21:28:22 +00:00
renovate[bot]
a7a2adaf6b Update dependency @types/node to v18.18.1 2023-09-29 17:01:31 +00:00
Robin
516d365511 Merge pull request #1660 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-09-29 13:01:03 -04:00
Vri
4343ae588e Translated using Weblate (German)
Currently translated at 100.0% (121 of 121 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-09-29 14:22:14 +00:00
Robin
a7624806b2 Upgrade eslint-plugin-matrix-org to 1.2.1
This upgrade came with a number of new lints that needed to be fixed across the code base. Primarily: explicit return types on functions, and explicit visibility modifiers on class members.
2023-09-22 18:07:06 -04:00
143 changed files with 2406 additions and 1953 deletions

View File

@@ -1,13 +1,31 @@
const COPYRIGHT_HEADER = `/*
Copyright %%CURRENT_YEAR%% 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.
*/
`;
module.exports = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org"],
extends: [ extends: [
"prettier",
"plugin:matrix-org/react", "plugin:matrix-org/react",
"plugin:matrix-org/a11y", "plugin:matrix-org/a11y",
"plugin:matrix-org/typescript", "plugin:matrix-org/typescript",
"prettier",
], ],
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: "latest",
sourceType: "module", sourceType: "module",
project: ["./tsconfig.json"], project: ["./tsconfig.json"],
}, },
@@ -15,29 +33,12 @@ module.exports = {
browser: true, browser: true,
node: true, node: true,
}, },
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: { rules: {
"jsx-a11y/media-has-caption": ["off"], "matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
}, "jsx-a11y/media-has-caption": "off",
overrides: [
{
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
extends: [
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
"prettier",
],
rules: {
// We're aiming to convert this code to strict mode
"@typescript-eslint/no-non-null-assertion": "off",
// We should use the js-sdk logger, never console directly. // We should use the js-sdk logger, never console directly.
"no-console": ["error"], "no-console": ["error"],
}, },
},
],
settings: { settings: {
react: { react: {
version: "detect", version: "detect",

View File

@@ -72,10 +72,10 @@ jobs:
type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}} type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@dedd61cf5d839122591f5027c89bf3ad27691d18 uses: docker/setup-buildx-action@5d9862498505fcac67b9f455d6e94ec0339f7b90
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@4c1b68d83ad20cc1a09620ca477d5bbbb5fa14d0 uses: docker/build-push-action@fdf7f43ecf7c1a5c7afe936410233728a8c2d9c2
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -14,7 +14,7 @@ module.exports = {
Array.isArray(item) && Array.isArray(item) &&
item.length > 0 && item.length > 0 &&
item[0].name === "vite-plugin-mdx" item[0].name === "vite-plugin-mdx"
) ),
); );
config.plugins.push(svgrPlugin()); config.plugins.push(svgrPlugin());
config.resolve = config.resolve || {}; config.resolve = config.resolve || {};

View File

@@ -47,7 +47,7 @@
"@types/lodash": "^4.14.199", "@types/lodash": "^4.14.199",
"@use-gesture/react": "^10.2.11", "@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^0.0.6", "@vector-im/compound-design-tokens": "^0.0.6",
"@vector-im/compound-web": "^0.4.0", "@vector-im/compound-web": "^0.5.0",
"@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1", "@vitejs/plugin-react": "^4.0.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
@@ -58,7 +58,7 @@
"i18next-http-backend": "^2.0.0", "i18next-http-backend": "^2.0.0",
"livekit-client": "^1.12.3", "livekit-client": "^1.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6385c9c0dab8fe67bd3a8992a4777f243fdd1b68", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#c8f8fb587d29dce22d314bfc16bf25a76b04e8bb",
"matrix-widget-api": "^1.3.1", "matrix-widget-api": "^1.3.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pako": "^2.0.4", "pako": "^2.0.4",
@@ -74,7 +74,7 @@
"tinyqueue": "^2.0.3", "tinyqueue": "^2.0.3",
"unique-names-generator": "^4.6.0", "unique-names-generator": "^4.6.0",
"uuid": "9", "uuid": "9",
"vaul": "^0.6.1" "vaul": "^0.7.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.5", "@babel/core": "^7.16.5",
@@ -105,19 +105,22 @@
"eslint": "^8.14.0", "eslint": "^8.14.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-deprecate": "^0.8.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "^0.4.0", "eslint-plugin-matrix-org": "^1.2.1",
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-unicorn": "^48.0.1",
"i18next-parser": "^8.0.0", "i18next-parser": "^8.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.2.2", "jest": "^29.2.2",
"jest-environment-jsdom": "^29.3.1", "jest-environment-jsdom": "^29.3.1",
"jest-mock": "^29.5.0", "jest-mock": "^29.5.0",
"prettier": "^2.6.2", "prettier": "^3.0.0",
"sass": "^1.42.1", "sass": "^1.42.1",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"typescript-eslint-language-service": "^5.0.5",
"vite": "^4.2.0", "vite": "^4.2.0",
"vite-plugin-html-template": "^1.1.0", "vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^4.0.0" "vite-plugin-svgr": "^4.0.0"

View File

@@ -10,7 +10,7 @@
"components": [ "components": [
{ {
"?": { "?": {
"no_universal_links": "*" "no_universal_links": "?*"
}, },
"exclude": true, "exclude": true,
"comment": "Opt out of universal links" "comment": "Opt out of universal links"

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@@ -114,5 +114,10 @@
"Start new call": "Neuen Anruf beginnen", "Start new call": "Neuen Anruf beginnen",
"Call not found": "Anruf nicht gefunden", "Call not found": "Anruf nicht gefunden",
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Anrufe sind nun Ende-zu-Ende-verschlüsselt und müssen auf der Startseite erstellt werden. Damit stellen wir sicher, dass alle denselben Schlüssel verwenden.", "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Anrufe sind nun Ende-zu-Ende-verschlüsselt und müssen auf der Startseite erstellt werden. Damit stellen wir sicher, dass alle denselben Schlüssel verwenden.",
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Dein Webbrowser unterstützt keine Medien-Ende-zu-Ende-Verschlüsselung. Unterstützte Browser sind Chrome, Safari, Firefox >=117" "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Dein Webbrowser unterstützt keine Medien-Ende-zu-Ende-Verschlüsselung. Unterstützte Browser sind Chrome, Safari, Firefox >=117",
"Copy link": "Link kopieren",
"Invite": "Einladen",
"Invite to this call": "Zu diesem Anruf einladen",
"Link copied to clipboard": "Link in Zwischenablage kopiert",
"Participants": "Teilnehmende"
} }

View File

@@ -36,11 +36,8 @@
"Developer Settings": "Developer Settings", "Developer Settings": "Developer Settings",
"Display name": "Display name", "Display name": "Display name",
"Element Call Home": "Element Call Home", "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.",
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
"Encrypted": "Encrypted", "Encrypted": "Encrypted",
"End call": "End call", "End call": "End call",
"End-to-end encryption isn't supported on your browser.": "End-to-end encryption isn't supported on your browser.",
"Exit full screen": "Exit full screen", "Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback", "Feedback": "Feedback",

View File

@@ -23,7 +23,7 @@
"Debug log request": "Richiesta registro di debug", "Debug log request": "Richiesta registro di debug",
"Developer": "Sviluppatore", "Developer": "Sviluppatore",
"Developer Settings": "Impostazioni per sviluppatori", "Developer Settings": "Impostazioni per sviluppatori",
"Display name": "Nome da mostrare", "Display name": "Il tuo nome",
"Element Call Home": "Inizio di Element Call", "Element Call Home": "Inizio di Element Call",
"Enable end-to-end encryption (password protected calls)": "Attiva crittografia end-to-end (chiamate protette da password)", "Enable end-to-end encryption (password protected calls)": "Attiva crittografia end-to-end (chiamate protette da password)",
"Encrypted": "Cifrata", "Encrypted": "Cifrata",

View File

@@ -114,5 +114,10 @@
"Call not found": "Nie znaleziono połączenia", "Call not found": "Nie znaleziono połączenia",
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Połączenia są teraz szyfrowane end-to-end i muszą zostać utworzone ze strony głównej. Pomaga to upewnić się, że każdy korzysta z tego samego klucza szyfrującego.", "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Połączenia są teraz szyfrowane end-to-end i muszą zostać utworzone ze strony głównej. Pomaga to upewnić się, że każdy korzysta z tego samego klucza szyfrującego.",
"You": "Ty", "You": "Ty",
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117" "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117",
"Invite": "Zaproś",
"Link copied to clipboard": "Skopiowano link do schowka",
"Participants": "Uczestnicy",
"Copy link": "Kopiuj link",
"Invite to this call": "Zaproś do połączenia"
} }

View File

@@ -1 +1,8 @@
{} {
"{{count}} stars|one": "{{count}} stjärna",
"{{count}} stars|other": "{{count}} stjärnor",
"{{count, number}}|one": "{{count, number}}",
"{{count, number}}|other": "{{count, number}}",
"{{displayName}} is presenting": "{{displayName}} presenterar",
"{{displayName}}, your call has ended.": "{{displayName}}, ditt samtal har avslutats."
}

View File

@@ -114,5 +114,10 @@
"Back to recents": "Повернутися до недавніх", "Back to recents": "Повернутися до недавніх",
"Call not found": "Виклик не знайдено", "Call not found": "Виклик не знайдено",
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Відтепер виклики захищено наскрізним шифруванням, і їх потрібно створювати з домашньої сторінки. Це допомагає переконатися, що всі користувачі використовують один і той самий ключ шифрування.", "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Відтепер виклики захищено наскрізним шифруванням, і їх потрібно створювати з домашньої сторінки. Це допомагає переконатися, що всі користувачі використовують один і той самий ключ шифрування.",
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Ваш браузер не підтримує наскрізне шифрування мультимедійних даних. Підтримувані браузери: Chrome, Safari, Firefox >=117" "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Ваш браузер не підтримує наскрізне шифрування мультимедійних даних. Підтримувані браузери: Chrome, Safari, Firefox >=117",
"Invite": "Запросити",
"Link copied to clipboard": "Посилання скопійовано до буфера обміну",
"Participants": "Учасники",
"Copy link": "Скопіювати посилання",
"Invite to this call": "Запросити до цього виклику"
} }

View File

@@ -114,5 +114,10 @@
"Unmute microphone": "將麥克風取消靜音", "Unmute microphone": "將麥克風取消靜音",
"Call not found": "找不到通話", "Call not found": "找不到通話",
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "通話現在是端對端加密的,必須從首頁建立。這有助於確保每個人都使用相同的加密金鑰。", "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "通話現在是端對端加密的,必須從首頁建立。這有助於確保每個人都使用相同的加密金鑰。",
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "您的網路瀏覽器不支援媒體端到端加密。支援的瀏覽器包含了 Chrome、Safari、Firefox >=117" "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "您的網路瀏覽器不支援媒體端到端加密。支援的瀏覽器包含了 Chrome、Safari、Firefox >=117",
"Copy link": "複製連結",
"Invite": "邀請",
"Invite to this call": "邀請到此通話",
"Link copied to clipboard": "連結已複製到剪貼簿",
"Participants": "參與者"
} }

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Suspense, useEffect, useState } from "react"; import { FC, Suspense, useEffect, useState } from "react";
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Switch, Switch,
@@ -41,7 +41,7 @@ interface BackgroundProviderProps {
children: JSX.Element; children: JSX.Element;
} }
const BackgroundProvider = ({ children }: BackgroundProviderProps) => { const BackgroundProvider: FC<BackgroundProviderProps> = ({ children }) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
useEffect(() => { useEffect(() => {
@@ -61,7 +61,7 @@ interface AppProps {
history: History; history: History;
} }
export default function App({ history }: AppProps) { export const App: FC<AppProps> = ({ history }) => {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
@@ -109,4 +109,4 @@ export default function App({ history }: AppProps) {
</BackgroundProvider> </BackgroundProvider>
</Router> </Router>
); );
} };

View File

@@ -58,7 +58,7 @@ export const Avatar: FC<Props> = ({
Object.values(Size).includes(size as Size) Object.values(Size).includes(size as Size)
? sizes.get(size as Size) ? sizes.get(size as Size)
: (size as number), : (size as number),
[size] [size],
); );
const resolvedSrc = useMemo(() => { const resolvedSrc = useMemo(() => {

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ReactNode } from "react"; import { FC, ReactNode } from "react";
import styles from "./Banner.module.css"; import styles from "./Banner.module.css";
@@ -22,6 +22,6 @@ interface Props {
children: ReactNode; children: ReactNode;
} }
export const Banner = ({ children }: Props) => { export const Banner: FC<Props> = ({ children }) => {
return <div className={styles.banner}>{children}</div>; return <div className={styles.banner}>{children}</div>;
}; };

View File

@@ -82,7 +82,8 @@ export type SetClientParams = {
const ClientContext = createContext<ClientState | undefined>(undefined); const ClientContext = createContext<ClientState | undefined>(undefined);
export const useClientState = () => useContext(ClientContext); export const useClientState = (): ClientState | undefined =>
useContext(ClientContext);
export function useClient(): { export function useClient(): {
client?: MatrixClient; client?: MatrixClient;
@@ -189,7 +190,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
user: session.user_id, user: session.user_id,
password: session.tempPassword, password: session.tempPassword,
}, },
password password,
); );
saveSession({ ...session, passwordlessUser: false }); saveSession({ ...session, passwordlessUser: false });
@@ -199,7 +200,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
passwordlessUser: false, passwordlessUser: false,
}); });
}, },
[initClientState?.client] [initClientState?.client],
); );
const setClient = useCallback( const setClient = useCallback(
@@ -221,7 +222,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
setInitClientState(null); setInitClientState(null);
} }
}, },
[initClientState?.client] [initClientState?.client],
); );
const logout = useCallback(async () => { const logout = useCallback(async () => {
@@ -249,7 +250,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}, []); }, []);
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>( const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
undefined undefined,
); );
useEventTarget( useEventTarget(
loadChannel, loadChannel,
@@ -257,9 +258,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
useCallback(() => { useCallback(() => {
initClientState?.client.stopClient(); initClientState?.client.stopClient();
setAlreadyOpenedErr( setAlreadyOpenedErr(
translatedError("This application has been opened in another tab.", t) translatedError("This application has been opened in another tab.", t),
); );
}, [initClientState?.client, setAlreadyOpenedErr, t]) }, [initClientState?.client, setAlreadyOpenedErr, t]),
); );
const [isDisconnected, setIsDisconnected] = useState(false); const [isDisconnected, setIsDisconnected] = useState(false);
@@ -300,7 +301,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => { (state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
setIsDisconnected(clientIsDisconnected(state, data)); setIsDisconnected(clientIsDisconnected(state, data));
}, },
[] [],
); );
useEffect(() => { useEffect(() => {
@@ -386,7 +387,7 @@ async function loadClient(): Promise<InitResult | null> {
logger.warn( logger.warn(
"The previous session was lost, and we couldn't log it out, " + "The previous session was lost, and we couldn't log it out, " +
err + err +
"either" "either",
); );
} }
} }
@@ -408,8 +409,8 @@ export interface Session {
tempPassword?: string; tempPassword?: string;
} }
const clearSession = () => localStorage.removeItem("matrix-auth-store"); const clearSession = (): void => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session) => const saveSession = (s: Session): void =>
localStorage.setItem("matrix-auth-store", JSON.stringify(s)); localStorage.setItem("matrix-auth-store", JSON.stringify(s));
const loadSession = (): Session | undefined => { const loadSession = (): Session | undefined => {
const data = localStorage.getItem("matrix-auth-store"); const data = localStorage.getItem("matrix-auth-store");
@@ -422,5 +423,6 @@ const loadSession = (): Session | undefined => {
const clientIsDisconnected = ( const clientIsDisconnected = (
syncState: SyncState, syncState: SyncState,
syncData?: ISyncStateData syncData?: ISyncStateData,
) => syncState === "ERROR" && syncData?.error?.name === "ConnectionError"; ): boolean =>
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";

View File

@@ -15,22 +15,22 @@ limitations under the License.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import { HTMLAttributes, ReactNode } from "react"; import { FC, HTMLAttributes, ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css"; import styles from "./DisconnectedBanner.module.css";
import { ValidClientState, useClientState } from "./ClientContext"; import { ValidClientState, useClientState } from "./ClientContext";
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> { interface Props extends HTMLAttributes<HTMLElement> {
children?: ReactNode; children?: ReactNode;
className?: string; className?: string;
} }
export function DisconnectedBanner({ export const DisconnectedBanner: FC<Props> = ({
children, children,
className, className,
...rest ...rest
}: DisconnectedBannerProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const clientState = useClientState(); const clientState = useClientState();
let shouldShowBanner = false; let shouldShowBanner = false;
@@ -50,4 +50,4 @@ export function DisconnectedBanner({
)} )}
</> </>
); );
} };

View File

@@ -1,23 +0,0 @@
/*
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);
}

View File

@@ -1,39 +0,0 @@
/*
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 LockOffIcon from "./icons/LockOff.svg?react";
import { useEnableE2EE } from "./settings/useSetting";
export const E2EEBanner = () => {
const [e2eeEnabled] = useEnableE2EE();
if (e2eeEnabled) return null;
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>
);
};

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ReactNode, useCallback, useEffect } from "react"; import { FC, ReactNode, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
@@ -33,7 +33,10 @@ interface FullScreenViewProps {
children: ReactNode; children: ReactNode;
} }
export function FullScreenView({ className, children }: FullScreenViewProps) { export const FullScreenView: FC<FullScreenViewProps> = ({
className,
children,
}) => {
return ( return (
<div className={classNames(styles.page, className)}> <div className={classNames(styles.page, className)}>
<Header> <Header>
@@ -47,13 +50,13 @@ export function FullScreenView({ className, children }: FullScreenViewProps) {
</div> </div>
</div> </div>
); );
} };
interface ErrorViewProps { interface ErrorViewProps {
error: Error; error: Error;
} }
export function ErrorView({ error }: ErrorViewProps) { export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
const location = useLocation(); const location = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -96,9 +99,9 @@ export function ErrorView({ error }: ErrorViewProps) {
)} )}
</FullScreenView> </FullScreenView>
); );
} };
export function CrashView() { export const CrashView: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const onReload = useCallback(() => { const onReload = useCallback(() => {
@@ -127,9 +130,9 @@ export function CrashView() {
</Button> </Button>
</FullScreenView> </FullScreenView>
); );
} };
export function LoadingView() { export const LoadingView: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -137,4 +140,4 @@ export function LoadingView() {
<h1>{t("Loading…")}</h1> <h1>{t("Loading…")}</h1>
</FullScreenView> </FullScreenView>
); );
} };

View File

@@ -48,5 +48,5 @@ export const Glass = forwardRef<HTMLDivElement, Props>(
> >
{Children.only(children)} {Children.only(children)}
</div> </div>
) ),
); );

View File

@@ -32,13 +32,13 @@ interface HeaderProps extends HTMLAttributes<HTMLElement> {
className?: string; className?: string;
} }
export function Header({ children, className, ...rest }: HeaderProps) { export const Header: FC<HeaderProps> = ({ children, className, ...rest }) => {
return ( return (
<header className={classNames(styles.header, className)} {...rest}> <header className={classNames(styles.header, className)} {...rest}>
{children} {children}
</header> </header>
); );
} };
interface LeftNavProps extends HTMLAttributes<HTMLElement> { interface LeftNavProps extends HTMLAttributes<HTMLElement> {
children: ReactNode; children: ReactNode;
@@ -46,26 +46,26 @@ interface LeftNavProps extends HTMLAttributes<HTMLElement> {
hideMobile?: boolean; hideMobile?: boolean;
} }
export function LeftNav({ export const LeftNav: FC<LeftNavProps> = ({
children, children,
className, className,
hideMobile, hideMobile,
...rest ...rest
}: LeftNavProps) { }) => {
return ( return (
<div <div
className={classNames( className={classNames(
styles.nav, styles.nav,
styles.leftNav, styles.leftNav,
{ [styles.hideMobile]: hideMobile }, { [styles.hideMobile]: hideMobile },
className className,
)} )}
{...rest} {...rest}
> >
{children} {children}
</div> </div>
); );
} };
interface RightNavProps extends HTMLAttributes<HTMLElement> { interface RightNavProps extends HTMLAttributes<HTMLElement> {
children?: ReactNode; children?: ReactNode;
@@ -73,32 +73,32 @@ interface RightNavProps extends HTMLAttributes<HTMLElement> {
hideMobile?: boolean; hideMobile?: boolean;
} }
export function RightNav({ export const RightNav: FC<RightNavProps> = ({
children, children,
className, className,
hideMobile, hideMobile,
...rest ...rest
}: RightNavProps) { }) => {
return ( return (
<div <div
className={classNames( className={classNames(
styles.nav, styles.nav,
styles.rightNav, styles.rightNav,
{ [styles.hideMobile]: hideMobile }, { [styles.hideMobile]: hideMobile },
className className,
)} )}
{...rest} {...rest}
> >
{children} {children}
</div> </div>
); );
} };
interface HeaderLogoProps { interface HeaderLogoProps {
className?: string; className?: string;
} }
export function HeaderLogo({ className }: HeaderLogoProps) { export const HeaderLogo: FC<HeaderLogoProps> = ({ className }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -110,7 +110,7 @@ export function HeaderLogo({ className }: HeaderLogoProps) {
<Logo /> <Logo />
</Link> </Link>
); );
} };
interface RoomHeaderInfoProps { interface RoomHeaderInfoProps {
id: string; id: string;

View File

@@ -63,7 +63,7 @@ export class LazyEventEmitter extends EventEmitter {
public addListener( public addListener(
type: string | symbol, type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (...args: any[]) => void listener: (...args: any[]) => void,
): this { ): this {
return this.on(type, listener); return this.on(type, listener);
} }

View File

@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MutableRefObject, PointerEvent, useCallback, useRef } from "react"; import {
MutableRefObject,
PointerEvent,
ReactNode,
useCallback,
useRef,
} from "react";
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox"; import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
import { ListState } from "@react-stately/list"; import { ListState } from "@react-stately/list";
import { Node } from "@react-types/shared"; import { Node } from "@react-types/shared";
@@ -35,7 +41,7 @@ export function ListBox<T>({
className, className,
listBoxRef, listBoxRef,
...rest ...rest
}: ListBoxProps<T>) { }: ListBoxProps<T>): ReactNode {
const ref = useRef<HTMLUListElement>(null); const ref = useRef<HTMLUListElement>(null);
const listRef = listBoxRef ?? ref; const listRef = listBoxRef ?? ref;
@@ -66,12 +72,12 @@ interface OptionProps<T> {
item: Node<T>; item: Node<T>;
} }
function Option<T>({ item, state, className }: OptionProps<T>) { function Option<T>({ item, state, className }: OptionProps<T>): ReactNode {
const ref = useRef(null); const ref = useRef(null);
const { optionProps, isSelected, isFocused, isDisabled } = useOption( const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key }, { key: item.key },
state, state,
ref ref,
); );
// Hack: remove the onPointerUp event handler and re-wire it to // Hack: remove the onPointerUp event handler and re-wire it to
@@ -91,7 +97,7 @@ function Option<T>({ item, state, className }: OptionProps<T>) {
// @ts-ignore // @ts-ignore
origPointerUp(e as unknown as PointerEvent<HTMLElement>); origPointerUp(e as unknown as PointerEvent<HTMLElement>);
}, },
[origPointerUp] [origPointerUp],
); );
return ( return (

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Key, useRef, useState } from "react"; import { Key, ReactNode, useRef, useState } from "react";
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu"; import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
import { TreeState, useTreeState } from "@react-stately/tree"; import { TreeState, useTreeState } from "@react-stately/tree";
import { mergeProps } from "@react-aria/utils"; import { mergeProps } from "@react-aria/utils";
@@ -37,7 +37,7 @@ export function Menu<T extends object>({
onClose, onClose,
label, label,
...rest ...rest
}: MenuProps<T>) { }: MenuProps<T>): ReactNode {
const state = useTreeState<T>({ ...rest, selectionMode: "none" }); const state = useTreeState<T>({ ...rest, selectionMode: "none" });
const menuRef = useRef(null); const menuRef = useRef(null);
const { menuProps } = useMenu<T>(rest, state, menuRef); const { menuProps } = useMenu<T>(rest, state, menuRef);
@@ -68,7 +68,12 @@ interface MenuItemProps<T> {
onClose: () => void; onClose: () => void;
} }
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) { function MenuItem<T>({
item,
state,
onAction,
onClose,
}: MenuItemProps<T>): ReactNode {
const ref = useRef(null); const ref = useRef(null);
const { menuItemProps } = useMenuItem( const { menuItemProps } = useMenuItem(
{ {
@@ -77,7 +82,7 @@ function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
onClose, onClose,
}, },
state, state,
ref ref,
); );
const [isFocused, setFocused] = useState(false); const [isFocused, setFocused] = useState(false);

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ReactNode, useCallback } from "react"; import { FC, ReactNode, useCallback } from "react";
import { AriaDialogProps } from "@react-types/dialog"; import { AriaDialogProps } from "@react-types/dialog";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -37,7 +37,7 @@ import { useMediaQuery } from "./useMediaQuery";
import { Glass } from "./Glass"; import { Glass } from "./Glass";
// TODO: Support tabs // TODO: Support tabs
export interface ModalProps extends AriaDialogProps { export interface Props extends AriaDialogProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
className?: string; className?: string;
@@ -59,14 +59,14 @@ export interface ModalProps extends AriaDialogProps {
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices, * A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
* and a dialog box on desktop. * and a dialog box on desktop.
*/ */
export function Modal({ export const Modal: FC<Props> = ({
title, title,
children, children,
className, className,
open, open,
onDismiss, onDismiss,
...rest ...rest
}: ModalProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Empirically, Chrome on Android can end up not matching (hover: none), but // Empirically, Chrome on Android can end up not matching (hover: none), but
// still matching (pointer: coarse) :/ // still matching (pointer: coarse) :/
@@ -75,7 +75,7 @@ export function Modal({
(open: boolean) => { (open: boolean) => {
if (!open) onDismiss?.(); if (!open) onDismiss?.();
}, },
[onDismiss] [onDismiss],
); );
if (touchscreen) { if (touchscreen) {
@@ -92,7 +92,7 @@ export function Modal({
className, className,
overlayStyles.overlay, overlayStyles.overlay,
styles.modal, styles.modal,
styles.drawer styles.drawer,
)} )}
{...rest} {...rest}
> >
@@ -124,7 +124,7 @@ export function Modal({
overlayStyles.overlay, overlayStyles.overlay,
overlayStyles.animate, overlayStyles.animate,
styles.modal, styles.modal,
styles.dialog styles.dialog,
)} )}
> >
<div className={styles.content}> <div className={styles.content}>
@@ -152,4 +152,4 @@ export function Modal({
</DialogRoot> </DialogRoot>
); );
} }
} };

View File

@@ -70,7 +70,7 @@ export const Toast: FC<Props> = ({
(open: boolean) => { (open: boolean) => {
if (!open) onDismiss(); if (!open) onDismiss();
}, },
[onDismiss] [onDismiss],
); );
useEffect(() => { useEffect(() => {
@@ -91,7 +91,7 @@ export const Toast: FC<Props> = ({
className={classNames( className={classNames(
overlayStyles.overlay, overlayStyles.overlay,
overlayStyles.animate, overlayStyles.animate,
styles.toast styles.toast,
)} )}
> >
<DialogTitle asChild> <DialogTitle asChild>

View File

@@ -43,7 +43,7 @@ interface TooltipProps {
const Tooltip = forwardRef<HTMLDivElement, TooltipProps>( const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
( (
{ state, className, children, ...rest }: TooltipProps, { state, className, children, ...rest }: TooltipProps,
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>,
) => { ) => {
const { tooltipProps } = useTooltip(rest, state); const { tooltipProps } = useTooltip(rest, state);
@@ -56,7 +56,7 @@ const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
{children} {children}
</div> </div>
); );
} },
); );
interface TooltipTriggerProps { interface TooltipTriggerProps {
@@ -69,7 +69,7 @@ interface TooltipTriggerProps {
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>( export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
( (
{ children, placement, tooltip, ...rest }: TooltipTriggerProps, { children, placement, tooltip, ...rest }: TooltipTriggerProps,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>,
) => { ) => {
const tooltipTriggerProps = { delay: 250, ...rest }; const tooltipTriggerProps = { delay: 250, ...rest };
const tooltipState = useTooltipTriggerState(tooltipTriggerProps); const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
@@ -78,7 +78,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
const { triggerProps, tooltipProps } = useTooltipTrigger( const { triggerProps, tooltipProps } = useTooltipTrigger(
tooltipTriggerProps, tooltipTriggerProps,
tooltipState, tooltipState,
triggerRef triggerRef,
); );
const { overlayProps } = useOverlayPosition({ const { overlayProps } = useOverlayPosition({
@@ -94,7 +94,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
<children.type <children.type
{...mergeProps<typeof children.props | typeof rest>( {...mergeProps<typeof children.props | typeof rest>(
children.props, children.props,
rest rest,
)} )}
/> />
{tooltipState.isOpen && ( {tooltipState.isOpen && (
@@ -110,5 +110,5 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
)} )}
</FocusableProvider> </FocusableProvider>
); );
} },
); );

View File

@@ -37,5 +37,7 @@ class TranslatedErrorImpl extends TranslatedError {}
// i18next-parser can't detect calls to a constructor, so we expose a bare // i18next-parser can't detect calls to a constructor, so we expose a bare
// function instead // function instead
export const translatedError = (messageKey: string, t: typeof i18n.t) => export const translatedError = (
new TranslatedErrorImpl(messageKey, t); messageKey: string,
t: typeof i18n.t,
): TranslatedError => new TranslatedErrorImpl(messageKey, t);

View File

@@ -33,6 +33,9 @@ interface RoomIdentifier {
// clearer what each flag means, and helps us avoid coupling Element Call's // clearer what each flag means, and helps us avoid coupling Element Call's
// behavior to the needs of specific consumers. // behavior to the needs of specific consumers.
interface UrlParams { interface UrlParams {
// Widget api related params
widgetId: string | null;
parentUrl: string | null;
/** /**
* Anything about what room we're pointed to should be from useRoomIdentifier which * Anything about what room we're pointed to should be from useRoomIdentifier which
* parses the path and resolves alias with respect to the default server name, however * parses the path and resolves alias with respect to the default server name, however
@@ -116,17 +119,17 @@ interface UrlParams {
// file. // file.
export function editFragmentQuery( export function editFragmentQuery(
hash: string, hash: string,
edit: (params: URLSearchParams) => URLSearchParams edit: (params: URLSearchParams) => URLSearchParams,
): string { ): string {
const fragmentQueryStart = hash.indexOf("?"); const fragmentQueryStart = hash.indexOf("?");
const fragmentParams = edit( const fragmentParams = edit(
new URLSearchParams( new URLSearchParams(
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart) fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
) ),
); );
return `${hash.substring( return `${hash.substring(
0, 0,
fragmentQueryStart fragmentQueryStart,
)}?${fragmentParams.toString()}`; )}?${fragmentParams.toString()}`;
} }
@@ -134,30 +137,30 @@ class ParamParser {
private fragmentParams: URLSearchParams; private fragmentParams: URLSearchParams;
private queryParams: URLSearchParams; private queryParams: URLSearchParams;
constructor(search: string, hash: string) { public constructor(search: string, hash: string) {
this.queryParams = new URLSearchParams(search); this.queryParams = new URLSearchParams(search);
const fragmentQueryStart = hash.indexOf("?"); const fragmentQueryStart = hash.indexOf("?");
this.fragmentParams = new URLSearchParams( this.fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart) fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
); );
} }
// Normally, URL params should be encoded in the fragment so as to avoid // 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 // leaking them to the server. However, we also check the normal query
// string for backwards compatibility with versions that only used that. // string for backwards compatibility with versions that only used that.
getParam(name: string): string | null { public getParam(name: string): string | null {
return this.fragmentParams.get(name) ?? this.queryParams.get(name); return this.fragmentParams.get(name) ?? this.queryParams.get(name);
} }
getAllParams(name: string): string[] { public getAllParams(name: string): string[] {
return [ return [
...this.fragmentParams.getAll(name), ...this.fragmentParams.getAll(name),
...this.queryParams.getAll(name), ...this.queryParams.getAll(name),
]; ];
} }
getFlagParam(name: string, defaultValue = false): boolean { public getFlagParam(name: string, defaultValue = false): boolean {
const param = this.getParam(name); const param = this.getParam(name);
return param === null ? defaultValue : param !== "false"; return param === null ? defaultValue : param !== "false";
} }
@@ -171,13 +174,16 @@ class ParamParser {
*/ */
export const getUrlParams = ( export const getUrlParams = (
search = window.location.search, search = window.location.search,
hash = window.location.hash hash = window.location.hash,
): UrlParams => { ): UrlParams => {
const parser = new ParamParser(search, hash); const parser = new ParamParser(search, hash);
const fontScale = parseFloat(parser.getParam("fontScale") ?? ""); const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
return { return {
widgetId: parser.getParam("widgetId"),
parentUrl: parser.getParam("parentUrl"),
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl: // NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
// what would we do if it were invalid? If the widget API says that's what // what would we do if it were invalid? If the widget API says that's what
// the room ID is, then that's what it is. // the room ID is, then that's what it is.
@@ -215,38 +221,44 @@ export const useUrlParams = (): UrlParams => {
export function getRoomIdentifierFromUrl( export function getRoomIdentifierFromUrl(
pathname: string, pathname: string,
search: string, search: string,
hash: string hash: string,
): RoomIdentifier { ): RoomIdentifier {
let roomAlias: string | null = null; let roomAlias: string | null = null;
pathname = pathname.substring(1); // Strip the "/"
const pathComponents = pathname.split("/");
const pathHasRoom = pathComponents[0] == "room";
const hasRoomAlias = pathComponents.length > 1;
// Here we handle the beginning of the alias and make sure it starts with a "#" // What type is our url: roomAlias in hash, room alias as the search path, roomAlias after /room/
if (hash === "" || hash.startsWith("#?")) { if (hash === "" || hash.startsWith("#?")) {
roomAlias = pathname.substring(1); // Strip the "/" if (hasRoomAlias && pathHasRoom) {
roomAlias = pathComponents[1];
// Delete "/room/", if present
if (roomAlias.startsWith("room/")) {
roomAlias = roomAlias.substring("room/".length);
} }
// Add "#", if not present if (!pathHasRoom) {
if (!roomAlias.startsWith("#")) { roomAlias = pathComponents[0];
roomAlias = `#${roomAlias}`;
} }
} else { } else {
roomAlias = hash; roomAlias = hash;
} }
// Delete "?" and what comes afterwards // Delete "?" and what comes afterwards
roomAlias = roomAlias.split("?")[0]; roomAlias = roomAlias?.split("?")[0] ?? null;
if (roomAlias.length <= 1) { if (roomAlias) {
// Make roomAlias is null, if it only is a "#" // Make roomAlias is null, if it only is a "#"
if (roomAlias.length <= 1) {
roomAlias = null; roomAlias = null;
} else { } else {
// Add "#", if not present
if (!roomAlias.startsWith("#")) {
roomAlias = `#${roomAlias}`;
}
// Add server part, if not present // Add server part, if not present
if (!roomAlias.includes(":")) { if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`; roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
} }
} }
}
const parser = new ParamParser(search, hash); const parser = new ParamParser(search, hash);
@@ -269,6 +281,6 @@ export const useRoomIdentifier = (): RoomIdentifier => {
const { pathname, search, hash } = useLocation(); const { pathname, search, hash } = useLocation();
return useMemo( return useMemo(
() => getRoomIdentifierFromUrl(pathname, search, hash), () => getRoomIdentifierFromUrl(pathname, search, hash),
[pathname, search, hash] [pathname, search, hash],
); );
}; };

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useMemo } from "react"; import { FC, ReactNode, useCallback, useMemo } from "react";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -31,7 +31,7 @@ import LogoutIcon from "./icons/Logout.svg?react";
import { Body } from "./typography/Typography"; import { Body } from "./typography/Typography";
import styles from "./UserMenu.module.css"; import styles from "./UserMenu.module.css";
interface UserMenuProps { interface Props {
preventNavigation: boolean; preventNavigation: boolean;
isAuthenticated: boolean; isAuthenticated: boolean;
isPasswordlessUser: boolean; isPasswordlessUser: boolean;
@@ -41,7 +41,7 @@ interface UserMenuProps {
onAction: (value: string) => void; onAction: (value: string) => void;
} }
export function UserMenu({ export const UserMenu: FC<Props> = ({
preventNavigation, preventNavigation,
isAuthenticated, isAuthenticated,
isPasswordlessUser, isPasswordlessUser,
@@ -49,7 +49,7 @@ export function UserMenu({
displayName, displayName,
avatarUrl, avatarUrl,
onAction, onAction,
}: UserMenuProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
@@ -123,7 +123,7 @@ export function UserMenu({
</TooltipTrigger> </TooltipTrigger>
{ {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(props: any) => ( (props: any): ReactNode => (
<Menu {...props} label={t("User menu")} onAction={onAction}> <Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label, dataTestid }) => ( {items.map(({ key, icon: Icon, label, dataTestid }) => (
<Item key={key} textValue={label}> <Item key={key} textValue={label}>
@@ -141,4 +141,4 @@ export function UserMenu({
} }
</PopoverMenuTrigger> </PopoverMenuTrigger>
); );
} };

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useState } from "react"; import { FC, useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { useClientLegacy } from "./ClientContext"; import { useClientLegacy } from "./ClientContext";
@@ -26,7 +26,7 @@ interface Props {
preventNavigation?: boolean; preventNavigation?: boolean;
} }
export function UserMenuContainer({ preventNavigation = false }: Props) { export const UserMenuContainer: FC<Props> = ({ preventNavigation = false }) => {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const { client, logout, authenticated, passwordlessUser } = useClientLegacy(); const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
@@ -34,7 +34,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const onDismissSettingsModal = useCallback( const onDismissSettingsModal = useCallback(
() => setSettingsModalOpen(false), () => setSettingsModalOpen(false),
[setSettingsModalOpen] [setSettingsModalOpen],
); );
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>(); const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
@@ -58,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
break; break;
} }
}, },
[history, location, logout, setSettingsModalOpen] [history, location, logout, setSettingsModalOpen],
); );
const userName = client?.getUserIdLocalpart() ?? ""; const userName = client?.getUserIdLocalpart() ?? "";
@@ -83,4 +83,4 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
)} )}
</> </>
); );
} };

View File

@@ -1,3 +1,19 @@
/*
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 { FC } from "react"; import { FC } from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";

View File

@@ -117,7 +117,7 @@ export class PosthogAnalytics {
return this.internalInstance; return this.internalInstance;
} }
constructor(private readonly posthog: PostHog) { private constructor(private readonly posthog: PostHog) {
const posthogConfig: PosthogSettings = { const posthogConfig: PosthogSettings = {
project_api_key: Config.get().posthog?.api_key, project_api_key: Config.get().posthog?.api_key,
api_host: Config.get().posthog?.api_host, api_host: Config.get().posthog?.api_host,
@@ -146,7 +146,7 @@ export class PosthogAnalytics {
this.enabled = true; this.enabled = true;
} else { } else {
logger.info( logger.info(
"Posthog is not enabled because there is no api key or no host given in the config" "Posthog is not enabled because there is no api key or no host given in the config",
); );
this.enabled = false; this.enabled = false;
} }
@@ -157,7 +157,7 @@ export class PosthogAnalytics {
private sanitizeProperties = ( private sanitizeProperties = (
properties: Properties, properties: Properties,
_eventName: string _eventName: string,
): Properties => { ): Properties => {
// Callback from posthog to sanitize properties before sending them to the server. // Callback from posthog to sanitize properties before sending them to the server.
// Here we sanitize posthog's built in properties which leak PII e.g. url reporting. // Here we sanitize posthog's built in properties which leak PII e.g. url reporting.
@@ -183,7 +183,7 @@ export class PosthogAnalytics {
return properties; return properties;
}; };
private registerSuperProperties(properties: Properties) { private registerSuperProperties(properties: Properties): void {
if (this.enabled) { if (this.enabled) {
this.posthog.register(properties); this.posthog.register(properties);
} }
@@ -201,8 +201,8 @@ export class PosthogAnalytics {
private capture( private capture(
eventName: string, eventName: string,
properties: Properties, properties: Properties,
options?: CaptureOptions options?: CaptureOptions,
) { ): void {
if (!this.enabled) { if (!this.enabled) {
return; return;
} }
@@ -213,7 +213,7 @@ export class PosthogAnalytics {
return this.enabled; return this.enabled;
} }
setAnonymity(anonymity: Anonymity): void { private setAnonymity(anonymity: Anonymity): void {
// Update this.anonymity. // Update this.anonymity.
// To update the anonymity typically you want to call updateAnonymityFromSettings // To update the anonymity typically you want to call updateAnonymityFromSettings
// to ensure this value is in step with the user's settings. // to ensure this value is in step with the user's settings.
@@ -236,7 +236,9 @@ export class PosthogAnalytics {
.join(""); .join("");
} }
private async identifyUser(analyticsIdGenerator: () => string) { private async identifyUser(
analyticsIdGenerator: () => string,
): Promise<void> {
if (this.anonymity == Anonymity.Pseudonymous && this.enabled) { if (this.anonymity == Anonymity.Pseudonymous && this.enabled) {
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows // Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
// different devices to send the same ID. // different devices to send the same ID.
@@ -258,27 +260,27 @@ export class PosthogAnalytics {
// The above could fail due to network requests, but not essential to starting the application, // The above could fail due to network requests, but not essential to starting the application,
// so swallow it. // so swallow it.
logger.log( logger.log(
"Unable to identify user for tracking" + (e as Error)?.toString() "Unable to identify user for tracking" + (e as Error)?.toString(),
); );
} }
if (analyticsID) { if (analyticsID) {
this.posthog.identify(analyticsID); this.posthog.identify(analyticsID);
} else { } else {
logger.info( logger.info(
"No analyticsID is availble. Should not try to setup posthog" "No analyticsID is availble. Should not try to setup posthog",
); );
} }
} }
} }
async getAnalyticsId() { private async getAnalyticsId(): Promise<string | null> {
const client: MatrixClient = window.matrixclient; const client: MatrixClient = window.matrixclient;
let accountAnalyticsId; let accountAnalyticsId;
if (widget) { if (widget) {
accountAnalyticsId = getUrlParams().analyticsID; accountAnalyticsId = getUrlParams().analyticsID;
} else { } else {
const accountData = await client.getAccountDataFromServer( const accountData = await client.getAccountDataFromServer(
PosthogAnalytics.ANALYTICS_EVENT_TYPE PosthogAnalytics.ANALYTICS_EVENT_TYPE,
); );
accountAnalyticsId = accountData?.id; accountAnalyticsId = accountData?.id;
} }
@@ -291,12 +293,14 @@ export class PosthogAnalytics {
return null; return null;
} }
async hashedEcAnalyticsId(accountAnalyticsId: string): Promise<string> { private async hashedEcAnalyticsId(
accountAnalyticsId: string,
): Promise<string> {
const client: MatrixClient = window.matrixclient; const client: MatrixClient = window.matrixclient;
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId(); const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
const bufferForPosthogId = await crypto.subtle.digest( const bufferForPosthogId = await crypto.subtle.digest(
"sha-256", "sha-256",
Buffer.from(posthogIdMaterial, "utf-8") Buffer.from(posthogIdMaterial, "utf-8"),
); );
const view = new Int32Array(bufferForPosthogId); const view = new Int32Array(bufferForPosthogId);
return Array.from(view) return Array.from(view)
@@ -304,17 +308,17 @@ export class PosthogAnalytics {
.join(""); .join("");
} }
async setAccountAnalyticsId(analyticsID: string) { private async setAccountAnalyticsId(analyticsID: string): Promise<void> {
if (!widget) { if (!widget) {
const client = window.matrixclient; const client = window.matrixclient;
// the analytics ID only needs to be set in the standalone version. // the analytics ID only needs to be set in the standalone version.
const accountData = await client.getAccountDataFromServer( const accountData = await client.getAccountDataFromServer(
PosthogAnalytics.ANALYTICS_EVENT_TYPE PosthogAnalytics.ANALYTICS_EVENT_TYPE,
); );
await client.setAccountData( await client.setAccountData(
PosthogAnalytics.ANALYTICS_EVENT_TYPE, PosthogAnalytics.ANALYTICS_EVENT_TYPE,
Object.assign({ id: analyticsID }, accountData) Object.assign({ id: analyticsID }, accountData),
); );
} }
} }
@@ -335,7 +339,7 @@ export class PosthogAnalytics {
this.updateAnonymityAndIdentifyUser(optInAnalytics); this.updateAnonymityAndIdentifyUser(optInAnalytics);
} }
private updateSuperProperties() { private updateSuperProperties(): void {
// Update super properties in posthog with our platform (app version, platform). // Update super properties in posthog with our platform (app version, platform).
// These properties will be subsequently passed in every event. // These properties will be subsequently passed in every event.
// //
@@ -356,7 +360,7 @@ export class PosthogAnalytics {
} }
private async updateAnonymityAndIdentifyUser( private async updateAnonymityAndIdentifyUser(
pseudonymousOptIn: boolean pseudonymousOptIn: boolean,
): Promise<void> { ): Promise<void> {
// Update this.anonymity based on the user's analytics opt-in settings // Update this.anonymity based on the user's analytics opt-in settings
const anonymity = pseudonymousOptIn const anonymity = pseudonymousOptIn
@@ -372,11 +376,11 @@ export class PosthogAnalytics {
this.setRegistrationType( this.setRegistrationType(
window.matrixclient.isGuest() || window.passwordlessUser window.matrixclient.isGuest() || window.passwordlessUser
? RegistrationType.Guest ? RegistrationType.Guest
: RegistrationType.Registered : RegistrationType.Registered,
); );
// store the promise to await posthog-tracking-events until the identification is done. // store the promise to await posthog-tracking-events until the identification is done.
this.identificationPromise = this.identifyUser( this.identificationPromise = this.identifyUser(
PosthogAnalytics.getRandomAnalyticsId PosthogAnalytics.getRandomAnalyticsId,
); );
await this.identificationPromise; await this.identificationPromise;
if (this.userRegisteredInThisSession()) { if (this.userRegisteredInThisSession()) {
@@ -391,7 +395,7 @@ export class PosthogAnalytics {
public async trackEvent<E extends IPosthogEvent>( public async trackEvent<E extends IPosthogEvent>(
{ eventName, ...properties }: E, { eventName, ...properties }: E,
options?: CaptureOptions options?: CaptureOptions,
): Promise<void> { ): Promise<void> {
if (this.identificationPromise) { if (this.identificationPromise) {
// only make calls to posthog after the identificaion is done // only make calls to posthog after the identificaion is done

View File

@@ -36,18 +36,22 @@ export class CallEndedTracker {
maxParticipantsCount: 0, maxParticipantsCount: 0,
}; };
cacheStartCall(time: Date) { public cacheStartCall(time: Date): void {
this.cache.startTime = time; this.cache.startTime = time;
} }
cacheParticipantCountChanged(count: number) { public cacheParticipantCountChanged(count: number): void {
this.cache.maxParticipantsCount = Math.max( this.cache.maxParticipantsCount = Math.max(
count, count,
this.cache.maxParticipantsCount this.cache.maxParticipantsCount,
); );
} }
track(callId: string, callParticipantsNow: number, sendInstantly: boolean) { public track(
callId: string,
callParticipantsNow: number,
sendInstantly: boolean,
): void {
PosthogAnalytics.instance.trackEvent<CallEnded>( PosthogAnalytics.instance.trackEvent<CallEnded>(
{ {
eventName: "CallEnded", eventName: "CallEnded",
@@ -56,7 +60,7 @@ export class CallEndedTracker {
callParticipantsOnLeave: callParticipantsNow, callParticipantsOnLeave: callParticipantsNow,
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000, callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
}, },
{ send_instantly: sendInstantly } { send_instantly: sendInstantly },
); );
} }
} }
@@ -67,7 +71,7 @@ interface CallStarted extends IPosthogEvent {
} }
export class CallStartedTracker { export class CallStartedTracker {
track(callId: string) { public track(callId: string): void {
PosthogAnalytics.instance.trackEvent<CallStarted>({ PosthogAnalytics.instance.trackEvent<CallStarted>({
eventName: "CallStarted", eventName: "CallStarted",
callId: callId, callId: callId,
@@ -86,19 +90,19 @@ export class SignupTracker {
signupEnd: new Date(0), signupEnd: new Date(0),
}; };
cacheSignupStart(time: Date) { public cacheSignupStart(time: Date): void {
this.cache.signupStart = time; this.cache.signupStart = time;
} }
getSignupEndTime() { public getSignupEndTime(): Date {
return this.cache.signupEnd; return this.cache.signupEnd;
} }
cacheSignupEnd(time: Date) { public cacheSignupEnd(time: Date): void {
this.cache.signupEnd = time; this.cache.signupEnd = time;
} }
track() { public track(): void {
PosthogAnalytics.instance.trackEvent<Signup>({ PosthogAnalytics.instance.trackEvent<Signup>({
eventName: "Signup", eventName: "Signup",
signupDuration: Date.now() - this.cache.signupStart.getTime(), signupDuration: Date.now() - this.cache.signupStart.getTime(),
@@ -112,7 +116,7 @@ interface Login extends IPosthogEvent {
} }
export class LoginTracker { export class LoginTracker {
track() { public track(): void {
PosthogAnalytics.instance.trackEvent<Login>({ PosthogAnalytics.instance.trackEvent<Login>({
eventName: "Login", eventName: "Login",
}); });
@@ -127,7 +131,7 @@ interface MuteMicrophone {
} }
export class MuteMicrophoneTracker { export class MuteMicrophoneTracker {
track(targetIsMute: boolean, callId: string) { public track(targetIsMute: boolean, callId: string): void {
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({ PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
eventName: "MuteMicrophone", eventName: "MuteMicrophone",
targetMuteState: targetIsMute ? "mute" : "unmute", targetMuteState: targetIsMute ? "mute" : "unmute",
@@ -143,7 +147,7 @@ interface MuteCamera {
} }
export class MuteCameraTracker { export class MuteCameraTracker {
track(targetIsMute: boolean, callId: string) { public track(targetIsMute: boolean, callId: string): void {
PosthogAnalytics.instance.trackEvent<MuteCamera>({ PosthogAnalytics.instance.trackEvent<MuteCamera>({
eventName: "MuteCamera", eventName: "MuteCamera",
targetMuteState: targetIsMute ? "mute" : "unmute", targetMuteState: targetIsMute ? "mute" : "unmute",
@@ -158,7 +162,7 @@ interface UndecryptableToDeviceEvent {
} }
export class UndecryptableToDeviceEventTracker { export class UndecryptableToDeviceEventTracker {
track(callId: string) { public track(callId: string): void {
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({ PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
eventName: "UndecryptableToDeviceEvent", eventName: "UndecryptableToDeviceEvent",
callId, callId,
@@ -174,7 +178,7 @@ interface QualitySurveyEvent {
} }
export class QualitySurveyEventTracker { export class QualitySurveyEventTracker {
track(callId: string, feedbackText: string, stars: number) { public track(callId: string, feedbackText: string, stars: number): void {
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({ PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
eventName: "QualitySurvey", eventName: "QualitySurvey",
callId, callId,
@@ -190,7 +194,7 @@ interface CallDisconnectedEvent {
} }
export class CallDisconnectedEventTracker { export class CallDisconnectedEventTracker {
track(reason?: DisconnectReason) { public track(reason?: DisconnectReason): void {
PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({ PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({
eventName: "CallDisconnected", eventName: "CallDisconnected",
reason, reason,

View File

@@ -39,9 +39,9 @@ const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
* Span processor that extracts certain metrics from spans to send to PostHog * Span processor that extracts certain metrics from spans to send to PostHog
*/ */
export class PosthogSpanProcessor implements SpanProcessor { export class PosthogSpanProcessor implements SpanProcessor {
async forceFlush(): Promise<void> {} public async forceFlush(): Promise<void> {}
onStart(span: Span): void { public onStart(span: Span): void {
// Hack: Yield to allow attributes to be set before processing // Hack: Yield to allow attributes to be set before processing
Promise.resolve().then(() => { Promise.resolve().then(() => {
switch (span.name) { switch (span.name) {
@@ -55,7 +55,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
}); });
} }
onEnd(span: ReadableSpan): void { public onEnd(span: ReadableSpan): void {
switch (span.name) { switch (span.name) {
case "matrix.groupCallMembership": case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span); this.onGroupCallMembershipEnd(span);
@@ -148,7 +148,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices, ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
}, },
// Send instantly because the window might be closing // Send instantly because the window might be closing
{ send_instantly: true } { send_instantly: true },
); );
} }
} }
@@ -157,7 +157,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
/** /**
* Shutdown the processor. * Shutdown the processor.
*/ */
shutdown(): Promise<void> { public shutdown(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
} }

View File

@@ -1,4 +1,20 @@
import { Attributes } from "@opentelemetry/api"; /*
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 { AttributeValue, Attributes } from "@opentelemetry/api";
import { hrTimeToMicroseconds } from "@opentelemetry/core"; import { hrTimeToMicroseconds } from "@opentelemetry/core";
import { import {
SpanProcessor, SpanProcessor,
@@ -6,7 +22,21 @@ import {
Span, Span,
} from "@opentelemetry/sdk-trace-base"; } from "@opentelemetry/sdk-trace-base";
const dumpAttributes = (attr: Attributes) => const dumpAttributes = (
attr: Attributes,
): {
key: string;
type:
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
value: AttributeValue | undefined;
}[] =>
Object.entries(attr).map(([key, value]) => ({ Object.entries(attr).map(([key, value]) => ({
key, key,
type: typeof value, type: typeof value,
@@ -20,13 +50,13 @@ const dumpAttributes = (attr: Attributes) =>
export class RageshakeSpanProcessor implements SpanProcessor { export class RageshakeSpanProcessor implements SpanProcessor {
private readonly spans: ReadableSpan[] = []; private readonly spans: ReadableSpan[] = [];
async forceFlush(): Promise<void> {} public async forceFlush(): Promise<void> {}
onStart(span: Span): void { public onStart(span: Span): void {
this.spans.push(span); this.spans.push(span);
} }
onEnd(): void {} public onEnd(): void {}
/** /**
* Dumps the spans collected so far as Jaeger-compatible JSON. * Dumps the spans collected so far as Jaeger-compatible JSON.
@@ -110,5 +140,5 @@ export class RageshakeSpanProcessor implements SpanProcessor {
}); });
} }
async shutdown(): Promise<void> {} public async shutdown(): Promise<void> {}
} }

View File

@@ -22,7 +22,7 @@ limitations under the License.
// Array.prototype.findLastIndex // Array.prototype.findLastIndex
export function findLastIndex<T>( export function findLastIndex<T>(
array: T[], array: T[],
predicate: (item: T, index: number) => boolean predicate: (item: T, index: number) => boolean,
): number | null { ): number | null {
for (let i = array.length - 1; i >= 0; i--) { for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i], i)) return i; if (predicate(array[i], i)) return i;
@@ -36,9 +36,9 @@ export function findLastIndex<T>(
*/ */
export const count = <T>( export const count = <T>(
array: T[], array: T[],
predicate: (item: T, index: number) => boolean predicate: (item: T, index: number) => boolean,
): number => ): number =>
array.reduce( array.reduce(
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc), (acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
0 0,
); );

View File

@@ -80,7 +80,7 @@ export const LoginPage: FC = () => {
setLoading(false); setLoading(false);
}); });
}, },
[login, location, history, homeserver, setClient] [login, location, history, homeserver, setClient],
); );
return ( return (

View File

@@ -69,7 +69,7 @@ export const RegisterPage: FC = () => {
if (password !== passwordConfirmation) return; if (password !== passwordConfirmation) return;
const submit = async () => { const submit = async (): Promise<void> => {
setRegistering(true); setRegistering(true);
const recaptchaResponse = await execute(); const recaptchaResponse = await execute();
@@ -78,7 +78,7 @@ export const RegisterPage: FC = () => {
password, password,
userName, userName,
recaptchaResponse, recaptchaResponse,
passwordlessUser passwordlessUser,
); );
if (client && client?.groupCallEventHandler && passwordlessUser) { if (client && client?.groupCallEventHandler && passwordlessUser) {
@@ -135,7 +135,7 @@ export const RegisterPage: FC = () => {
execute, execute,
client, client,
setClient, setClient,
] ],
); );
useEffect(() => { useEffect(() => {
@@ -184,7 +184,7 @@ export const RegisterPage: FC = () => {
required required
name="password" name="password"
type="password" type="password"
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setPassword(e.target.value) setPassword(e.target.value)
} }
value={password} value={password}
@@ -198,7 +198,7 @@ export const RegisterPage: FC = () => {
required required
type="password" type="password"
name="passwordConfirmation" name="passwordConfirmation"
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setPasswordConfirmation(e.target.value) setPasswordConfirmation(e.target.value)
} }
value={passwordConfirmation} value={passwordConfirmation}

View File

@@ -21,12 +21,16 @@ import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
import { initClient } from "../matrix-utils"; import { initClient } from "../matrix-utils";
import { Session } from "../ClientContext"; import { Session } from "../ClientContext";
export const useInteractiveLogin = () => export function useInteractiveLogin(): (
useCallback< homeserver: string,
username: string,
password: string,
) => Promise<[MatrixClient, Session]> {
return useCallback<
( (
homeserver: string, homeserver: string,
username: string, username: string,
password: string password: string,
) => Promise<[MatrixClient, Session]> ) => Promise<[MatrixClient, Session]>
>(async (homeserver: string, username: string, password: string) => { >(async (homeserver: string, username: string, password: string) => {
const authClient = createClient({ baseUrl: homeserver }); const authClient = createClient({ baseUrl: homeserver });
@@ -41,8 +45,8 @@ export const useInteractiveLogin = () =>
}, },
password, password,
}), }),
stateUpdated: (...args) => {}, stateUpdated: (): void => {},
requestEmailToken: (...args): Promise<{ sid: string }> => { requestEmailToken: (): Promise<{ sid: string }> => {
return Promise.resolve({ sid: "" }); return Promise.resolve({ sid: "" });
}, },
}); });
@@ -66,9 +70,9 @@ export const useInteractiveLogin = () =>
userId: user_id, userId: user_id,
deviceId: device_id, deviceId: device_id,
}, },
false false,
); );
/* eslint-enable camelcase */ /* eslint-enable camelcase */
return [client, session]; return [client, session];
}, []); }, []);
}

View File

@@ -30,14 +30,14 @@ export const useInteractiveRegistration = (): {
password: string, password: string,
displayName: string, displayName: string,
recaptchaResponse: string, recaptchaResponse: string,
passwordlessUser: boolean passwordlessUser: boolean,
) => Promise<[MatrixClient, Session]>; ) => Promise<[MatrixClient, Session]>;
} => { } => {
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>( const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
undefined undefined,
); );
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>( const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
undefined undefined,
); );
const authClient = useRef<MatrixClient>(); const authClient = useRef<MatrixClient>();
@@ -50,7 +50,7 @@ export const useInteractiveRegistration = (): {
useEffect(() => { useEffect(() => {
authClient.current!.registerRequest({}).catch((error) => { authClient.current!.registerRequest({}).catch((error) => {
setPrivacyPolicyUrl( setPrivacyPolicyUrl(
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url,
); );
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key); setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
}); });
@@ -62,7 +62,7 @@ export const useInteractiveRegistration = (): {
password: string, password: string,
displayName: string, displayName: string,
recaptchaResponse: string, recaptchaResponse: string,
passwordlessUser: boolean passwordlessUser: boolean,
): Promise<[MatrixClient, Session]> => { ): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({ const interactiveAuth = new InteractiveAuth({
matrixClient: authClient.current!, matrixClient: authClient.current!,
@@ -72,7 +72,7 @@ export const useInteractiveRegistration = (): {
password, password,
auth: auth || undefined, auth: auth || undefined,
}), }),
stateUpdated: (nextStage, status) => { stateUpdated: (nextStage, status): void => {
if (status.error) { if (status.error) {
throw new Error(status.error); throw new Error(status.error);
} }
@@ -88,7 +88,7 @@ export const useInteractiveRegistration = (): {
}); });
} }
}, },
requestEmailToken: (...args) => { requestEmailToken: (): Promise<{ sid: string }> => {
return Promise.resolve({ sid: "dummy" }); return Promise.resolve({ sid: "dummy" });
}, },
}); });
@@ -106,7 +106,7 @@ export const useInteractiveRegistration = (): {
userId: user_id, userId: user_id,
deviceId: device_id, deviceId: device_id,
}, },
false false,
); );
await client.setDisplayName(displayName); await client.setDisplayName(displayName);
@@ -129,7 +129,7 @@ export const useInteractiveRegistration = (): {
return [client, session]; return [client, session];
}, },
[] [],
); );
return { privacyPolicyUrl, recaptchaKey, register }; return { privacyPolicyUrl, recaptchaKey, register };

View File

@@ -35,7 +35,11 @@ interface RecaptchaPromiseRef {
reject: (error: Error) => void; reject: (error: Error) => void;
} }
export const useRecaptcha = (sitekey?: string) => { export function useRecaptcha(sitekey?: string): {
execute: () => Promise<string>;
reset: () => void;
recaptchaId: string;
} {
const { t } = useTranslation(); const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16)); const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>(); const promiseRef = useRef<RecaptchaPromiseRef>();
@@ -43,7 +47,7 @@ export const useRecaptcha = (sitekey?: string) => {
useEffect(() => { useEffect(() => {
if (!sitekey) return; if (!sitekey) return;
const onRecaptchaLoaded = () => { const onRecaptchaLoaded = (): void => {
if (!document.getElementById(recaptchaId)) return; if (!document.getElementById(recaptchaId)) return;
window.grecaptcha.render(recaptchaId, { window.grecaptcha.render(recaptchaId, {
@@ -91,11 +95,11 @@ export const useRecaptcha = (sitekey?: string) => {
}); });
promiseRef.current = { promiseRef.current = {
resolve: (value) => { resolve: (value): void => {
resolve(value); resolve(value);
observer.disconnect(); observer.disconnect();
}, },
reject: (error) => { reject: (error): void => {
reject(error); reject(error);
observer.disconnect(); observer.disconnect();
}, },
@@ -104,7 +108,7 @@ export const useRecaptcha = (sitekey?: string) => {
window.grecaptcha.execute(); window.grecaptcha.execute();
const iframe = document.querySelector<HTMLIFrameElement>( const iframe = document.querySelector<HTMLIFrameElement>(
'iframe[src*="recaptcha/api2/bframe"]' 'iframe[src*="recaptcha/api2/bframe"]',
); );
if (iframe?.parentNode?.parentNode) { if (iframe?.parentNode?.parentNode) {
@@ -120,4 +124,4 @@ export const useRecaptcha = (sitekey?: string) => {
}, []); }, []);
return { execute, reset, recaptchaId }; return { execute, reset, recaptchaId };
}; }

View File

@@ -48,7 +48,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
randomString(16), randomString(16),
displayName, displayName,
recaptchaResponse, recaptchaResponse,
true true,
); );
setClient({ client, session }); setClient({ client, session });
} catch (e) { } catch (e) {
@@ -56,7 +56,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
throw e; throw e;
} }
}, },
[execute, reset, register, setClient] [execute, reset, register, setClient],
); );
return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId }; return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId };

View File

@@ -146,7 +146,9 @@ limitations under the License.
.copyButton { .copyButton {
width: 100%; width: 100%;
height: 40px; height: 40px;
transition: border-color 250ms, background-color 250ms; transition:
border-color 250ms,
background-color 250ms;
} }
.copyButton span { .copyButton span {

View File

@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { forwardRef } from "react"; import { FC, forwardRef } from "react";
import { PressEvent } from "@react-types/shared"; import { PressEvent } from "@react-types/shared";
import classNames from "classnames"; import classNames from "classnames";
import { useButton } from "@react-aria/button"; import { useButton } from "@react-aria/button";
@@ -94,12 +94,12 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
onPressStart, onPressStart,
...rest ...rest
}, },
ref ref,
) => { ) => {
const buttonRef = useObjectRef<HTMLButtonElement>(ref); const buttonRef = useObjectRef<HTMLButtonElement>(ref);
const { buttonProps } = useButton( const { buttonProps } = useButton(
{ onPress, onPressStart, ...rest }, { onPress, onPressStart, ...rest },
buttonRef buttonRef,
); );
// TODO: react-aria's useButton hook prevents form submission via keyboard // TODO: react-aria's useButton hook prevents form submission via keyboard
@@ -121,7 +121,7 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
{ {
[styles.on]: on, [styles.on]: on,
[styles.off]: off, [styles.off]: off,
} },
)} )}
{...mergeProps(rest, filteredButtonProps)} {...mergeProps(rest, filteredButtonProps)}
ref={buttonRef} ref={buttonRef}
@@ -132,17 +132,14 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
</> </>
</button> </button>
); );
} },
); );
export function MicButton({ export const MicButton: FC<{
muted,
...rest
}: {
muted: boolean; muted: boolean;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ muted, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon; const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
const label = muted ? t("Unmute microphone") : t("Mute microphone"); const label = muted ? t("Unmute microphone") : t("Mute microphone");
@@ -154,16 +151,13 @@ export function MicButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function VideoButton({ export const VideoButton: FC<{
muted,
...rest
}: {
muted: boolean; muted: boolean;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ muted, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Icon = muted ? VideoCallOffIcon : VideoCallIcon; const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
const label = muted ? t("Start video") : t("Stop video"); const label = muted ? t("Start video") : t("Stop video");
@@ -175,18 +169,14 @@ export function VideoButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function ScreenshareButton({ export const ScreenshareButton: FC<{
enabled,
className,
...rest
}: {
enabled: boolean; enabled: boolean;
className?: string; className?: string;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ enabled, className, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const label = enabled ? t("Sharing screen") : t("Share screen"); const label = enabled ? t("Sharing screen") : t("Share screen");
@@ -197,16 +187,13 @@ export function ScreenshareButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function HangupButton({ export const HangupButton: FC<{
className,
...rest
}: {
className?: string; className?: string;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ className, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -220,16 +207,13 @@ export function HangupButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function SettingsButton({ export const SettingsButton: FC<{
className,
...rest
}: {
className?: string; className?: string;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ className, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -239,7 +223,7 @@ export function SettingsButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
interface AudioButtonProps extends Omit<Props, "variant"> { interface AudioButtonProps extends Omit<Props, "variant"> {
/** /**
@@ -248,7 +232,7 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
volume: number; volume: number;
} }
export function AudioButton({ volume, ...rest }: AudioButtonProps) { export const AudioButton: FC<AudioButtonProps> = ({ volume, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -258,16 +242,16 @@ export function AudioButton({ volume, ...rest }: AudioButtonProps) {
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
interface FullscreenButtonProps extends Omit<Props, "variant"> { interface FullscreenButtonProps extends Omit<Props, "variant"> {
fullscreen?: boolean; fullscreen?: boolean;
} }
export function FullscreenButton({ export const FullscreenButton: FC<FullscreenButtonProps> = ({
fullscreen, fullscreen,
...rest ...rest
}: FullscreenButtonProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Icon = fullscreen ? FullscreenExit : Fullscreen; const Icon = fullscreen ? FullscreenExit : Fullscreen;
const label = fullscreen ? t("Exit full screen") : t("Full screen"); const label = fullscreen ? t("Exit full screen") : t("Full screen");
@@ -279,4 +263,4 @@ export function FullscreenButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };

View File

@@ -16,6 +16,7 @@ limitations under the License.
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useClipboard from "react-use-clipboard"; import useClipboard from "react-use-clipboard";
import { FC } from "react";
import CheckIcon from "../icons/Check.svg?react"; import CheckIcon from "../icons/Check.svg?react";
import CopyIcon from "../icons/Copy.svg?react"; import CopyIcon from "../icons/Copy.svg?react";
@@ -28,14 +29,15 @@ interface Props {
variant?: ButtonVariant; variant?: ButtonVariant;
copiedMessage?: string; copiedMessage?: string;
} }
export function CopyButton({
export const CopyButton: FC<Props> = ({
value, value,
children, children,
className, className,
variant, variant,
copiedMessage, copiedMessage,
...rest ...rest
}: Props) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 }); const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
@@ -62,4 +64,4 @@ export function CopyButton({
)} )}
</Button> </Button>
); );
} };

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { HTMLAttributes } from "react"; import { FC, HTMLAttributes } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import * as H from "history"; import * as H from "history";
@@ -34,20 +34,20 @@ interface Props extends HTMLAttributes<HTMLAnchorElement> {
className?: string; className?: string;
} }
export function LinkButton({ export const LinkButton: FC<Props> = ({
children, children,
to, to,
size, size,
variant, variant,
className, className,
...rest ...rest
}: Props) { }) => {
return ( return (
<Link <Link
className={classNames( className={classNames(
variantToClassName[variant || "secondary"], variantToClassName[variant || "secondary"],
size ? sizeToClassName[size] : [], size ? sizeToClassName[size] : [],
className className,
)} )}
to={to} to={to}
{...rest} {...rest}
@@ -55,4 +55,4 @@ export function LinkButton({
{children} {children}
</Link> </Link>
); );
} };

View File

@@ -57,7 +57,7 @@ export class Config {
} }
async function downloadConfig( async function downloadConfig(
configJsonFilename: string configJsonFilename: string,
): Promise<ConfigOptions> { ): Promise<ConfigOptions> {
const url = new URL(configJsonFilename, window.location.href); const url = new URL(configJsonFilename, window.location.href);
url.searchParams.set("cachebuster", Date.now().toString()); url.searchParams.set("cachebuster", Date.now().toString());

View File

@@ -16,71 +16,60 @@ limitations under the License.
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useEnableE2EE } from "../settings/useSetting"; import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
import { useLocalStorage } from "../useLocalStorage";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { PASSWORD_STRING, useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
import { widget } from "../widget"; import { widget } from "../widget";
export const getRoomSharedKeyLocalStorageKey = (roomId: string): string => export const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
`room-shared-key-${roomId}`; `room-shared-key-${roomId}`;
const useInternalRoomSharedKey = ( const useInternalRoomSharedKey = (roomId: string): string | null => {
roomId: string const key = getRoomSharedKeyLocalStorageKey(roomId);
): [string | null, (value: string) => void] => { const roomSharedKey = useLocalStorage(key)[0];
const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]);
const [e2eeEnabled] = useEnableE2EE();
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key);
return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey]; return roomSharedKey;
}; };
const useKeyFromUrl = (roomId: string): string | null => { /**
* Extracts the room password from the URL if one is present, saving it in localstorage
* and returning it in a tuple with the corresponding room ID from the URL.
* @returns A tuple of the roomId and password from the URL if the URL has both,
* otherwise [undefined, undefined]
*/
const useKeyFromUrl = (): [string, string] | [undefined, undefined] => {
const urlParams = useUrlParams(); const urlParams = useUrlParams();
const [e2eeSharedKey, setE2EESharedKey] = useInternalRoomSharedKey(roomId);
useEffect(() => { useEffect(() => {
if (!urlParams.password) return; if (!urlParams.password || !urlParams.roomId) return;
if (urlParams.password === "") return; if (!urlParams.roomId) return;
if (urlParams.password === e2eeSharedKey) return;
setE2EESharedKey(urlParams.password); setLocalStorageItem(
}, [urlParams, e2eeSharedKey, setE2EESharedKey]); // We set the Item by only using data from the url. This way we
// make sure, we always have matching pairs in the LocalStorage,
// as they occur in the call links.
getRoomSharedKeyLocalStorageKey(urlParams.roomId),
urlParams.password,
);
}, [urlParams]);
return urlParams.password ?? null; return urlParams.roomId && urlParams.password
? [urlParams.roomId, urlParams.password]
: [undefined, undefined];
}; };
export const useRoomSharedKey = (roomId: string): string | null => { export const useRoomSharedKey = (roomId: string): string | undefined => {
// make sure we've extracted the key from the URL first // make sure we've extracted the key from the URL first
// (and we still need to take the value it returns because // (and we still need to take the value it returns because
// the effect won't run in time for it to save to localstorage in // the effect won't run in time for it to save to localstorage in
// time for us to read it out again). // time for us to read it out again).
const passwordFormUrl = useKeyFromUrl(roomId); const [urlRoomId, passwordFormUrl] = useKeyFromUrl();
return useInternalRoomSharedKey(roomId)[0] ?? passwordFormUrl; const storedPassword = useInternalRoomSharedKey(roomId);
};
export const useManageRoomSharedKey = (roomId: string): string | null => { if (storedPassword) return storedPassword;
const urlParams = useUrlParams(); if (urlRoomId === roomId) return passwordFormUrl;
return undefined;
const urlPassword = useKeyFromUrl(roomId);
const [e2eeSharedKey] = useInternalRoomSharedKey(roomId);
useEffect(() => {
const hash = location.hash;
if (!hash.includes("?")) return;
if (!hash.includes(PASSWORD_STRING)) return;
if (urlParams.password !== e2eeSharedKey) return;
const [hashStart, passwordStart] = hash.split(PASSWORD_STRING);
const hashEnd = passwordStart.split("&").slice(1).join("&");
location.replace((hashStart ?? "") + (hashEnd ?? ""));
}, [urlParams, e2eeSharedKey]);
return e2eeSharedKey ?? urlPassword;
}; };
export const useIsRoomE2EE = (roomId: string): boolean | null => { export const useIsRoomE2EE = (roomId: string): boolean | null => {
@@ -91,6 +80,6 @@ export const useIsRoomE2EE = (roomId: string): boolean | null => {
// should inspect the e2eEnabled URL parameter here? // should inspect the e2eEnabled URL parameter here?
return useMemo( return useMemo(
() => widget === null && (room === null || !room.getCanonicalAlias()), () => widget === null && (room === null || !room.getCanonicalAlias()),
[room] [room],
); );
}; };

View File

@@ -36,5 +36,5 @@ export const Form = forwardRef<HTMLFormElement, FormProps>(
{children} {children}
</form> </form>
); );
} },
); );

View File

@@ -18,6 +18,7 @@ import { Link } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { FC } from "react";
import { CopyButton } from "../button"; import { CopyButton } from "../button";
import { Avatar, Size } from "../Avatar"; import { Avatar, Size } from "../Avatar";
@@ -31,7 +32,8 @@ interface CallListProps {
rooms: GroupCallRoom[]; rooms: GroupCallRoom[];
client: MatrixClient; client: MatrixClient;
} }
export function CallList({ rooms, client }: CallListProps) {
export const CallList: FC<CallListProps> = ({ rooms, client }) => {
return ( return (
<> <>
<div className={styles.callList}> <div className={styles.callList}>
@@ -54,7 +56,7 @@ export function CallList({ rooms, client }: CallListProps) {
</div> </div>
</> </>
); );
} };
interface CallTileProps { interface CallTileProps {
name: string; name: string;
avatarUrl: string; avatarUrl: string;
@@ -62,15 +64,18 @@ interface CallTileProps {
participants: RoomMember[]; participants: RoomMember[];
client: MatrixClient; client: MatrixClient;
} }
function CallTile({ name, avatarUrl, room }: CallTileProps) {
const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
const roomSharedKey = useRoomSharedKey(room.roomId); const roomSharedKey = useRoomSharedKey(room.roomId);
return ( return (
<div className={styles.callTile}> <div className={styles.callTile}>
<Link <Link
// note we explicitly omit the password here as we don't want it on this link because to={getRelativeRoomUrl(
// it's just for the user to navigate around and not for sharing room.roomId,
to={getRelativeRoomUrl(room.roomId, room.name)} room.name,
roomSharedKey ?? undefined,
)}
className={styles.callTileLink} className={styles.callTileLink}
> >
<Avatar id={room.roomId} name={name} size={Size.LG} src={avatarUrl} /> <Avatar id={room.roomId} name={name} size={Size.LG} src={avatarUrl} />
@@ -87,9 +92,9 @@ function CallTile({ name, avatarUrl, room }: CallTileProps) {
value={getAbsoluteRoomUrl( value={getAbsoluteRoomUrl(
room.roomId, room.roomId,
room.name, room.name,
roomSharedKey ?? undefined roomSharedKey ?? undefined,
)} )}
/> />
</div> </div>
); );
} };

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/ */
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC } from "react";
import { useClientState } from "../ClientContext"; import { useClientState } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
@@ -22,7 +23,7 @@ import { UnauthenticatedView } from "./UnauthenticatedView";
import { RegisteredView } from "./RegisteredView"; import { RegisteredView } from "./RegisteredView";
import { usePageTitle } from "../usePageTitle"; import { usePageTitle } from "../usePageTitle";
export function HomePage() { export const HomePage: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
usePageTitle(t("Home")); usePageTitle(t("Home"));
@@ -39,4 +40,4 @@ export function HomePage() {
<UnauthenticatedView /> <UnauthenticatedView />
); );
} }
} };

View File

@@ -16,6 +16,7 @@ limitations under the License.
import { PressEvent } from "@react-types/shared"; import { PressEvent } from "@react-types/shared";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC } from "react";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
@@ -28,7 +29,11 @@ interface Props {
onJoin: (e: PressEvent) => void; onJoin: (e: PressEvent) => void;
} }
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) { export const JoinExistingCallModal: FC<Props> = ({
onJoin,
open,
onDismiss,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -42,4 +47,4 @@ export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
</FieldRow> </FieldRow>
</Modal> </Modal>
); );
} };

View File

@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useState, useCallback, FormEvent, FormEventHandler } from "react"; import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Heading } from "@vector-im/compound-web"; import { Heading } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -39,17 +38,14 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption } from "../typography/Typography"; import { Caption } from "../typography/Typography";
import { Form } from "../form/Form"; import { Form } from "../form/Form";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting"; import { useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
import { setLocalStorageItem } from "../useLocalStorage";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
} }
export function RegisteredView({ client }: Props) { export const RegisteredView: FC<Props> = ({ client }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics(); const [optInAnalytics] = useOptInAnalytics();
@@ -59,9 +55,8 @@ export function RegisteredView({ client }: Props) {
useState(false); useState(false);
const onDismissJoinExistingCallModal = useCallback( const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false), () => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen] [setJoinExistingCallModalOpen],
); );
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback( const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => { (e: FormEvent) => {
@@ -73,24 +68,21 @@ export function RegisteredView({ client }: Props) {
? sanitiseRoomNameInput(roomNameData) ? sanitiseRoomNameInput(roomNameData)
: ""; : "";
async function submit() { async function submit(): Promise<void> {
setError(undefined); setError(undefined);
setLoading(true); setLoading(true);
const roomId = ( const createRoomResult = await createRoom(client, roomName, true);
await createRoom(client, roomName, e2eeEnabled ?? false)
)[1];
if (e2eeEnabled) { history.push(
setLocalStorageItem( getRelativeRoomUrl(
getRoomSharedKeyLocalStorageKey(roomId), createRoomResult.roomId,
randomString(32) roomName,
createRoomResult.password,
),
); );
} }
history.push(getRelativeRoomUrl(roomId, roomName));
}
submit().catch((error) => { submit().catch((error) => {
if (error.errcode === "M_ROOM_IN_USE") { if (error.errcode === "M_ROOM_IN_USE") {
setExistingAlias(roomAliasLocalpartFromRoomName(roomName)); setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
@@ -104,7 +96,7 @@ export function RegisteredView({ client }: Props) {
} }
}); });
}, },
[client, history, setJoinExistingCallModalOpen, e2eeEnabled] [client, history, setJoinExistingCallModalOpen],
); );
const recentRooms = useGroupCallRooms(client); const recentRooms = useGroupCallRooms(client);
@@ -158,7 +150,6 @@ export function RegisteredView({ client }: Props) {
<AnalyticsNotice /> <AnalyticsNotice />
</Caption> </Caption>
)} )}
<E2EEBanner />
{error && ( {error && (
<FieldRow className={styles.fieldRow}> <FieldRow className={styles.fieldRow}>
<ErrorMessage error={error} /> <ErrorMessage error={error} />
@@ -177,4 +168,4 @@ export function RegisteredView({ client }: Props) {
/> />
</> </>
); );
} };

View File

@@ -41,11 +41,8 @@ import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css"; import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName"; import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting"; import { useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
import { setLocalStorageItem } from "../useLocalStorage";
export const UnauthenticatedView: FC = () => { export const UnauthenticatedView: FC = () => {
const { setClient } = useClient(); const { setClient } = useClient();
@@ -59,14 +56,12 @@ export const UnauthenticatedView: FC = () => {
useState(false); useState(false);
const onDismissJoinExistingCallModal = useCallback( const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false), () => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen] [setJoinExistingCallModalOpen],
); );
const [onFinished, setOnFinished] = useState<() => void>(); const [onFinished, setOnFinished] = useState<() => void>();
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback( const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
@@ -74,7 +69,7 @@ export const UnauthenticatedView: FC = () => {
const roomName = sanitiseRoomNameInput(data.get("callName") as string); const roomName = sanitiseRoomNameInput(data.get("callName") as string);
const displayName = data.get("displayName") as string; const displayName = data.get("displayName") as string;
async function submit() { async function submit(): Promise<void> {
setError(undefined); setError(undefined);
setLoading(true); setLoading(true);
const recaptchaResponse = await execute(); const recaptchaResponse = await execute();
@@ -84,21 +79,12 @@ export const UnauthenticatedView: FC = () => {
randomString(16), randomString(16),
displayName, displayName,
recaptchaResponse, recaptchaResponse,
true true,
); );
let roomId: string; let createRoomResult;
try { try {
roomId = ( createRoomResult = await createRoom(client, roomName, true);
await createRoom(client, roomName, e2eeEnabled ?? false)
)[1];
if (e2eeEnabled) {
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(roomId),
randomString(32)
);
}
} catch (error) { } catch (error) {
if (!setClient) { if (!setClient) {
throw error; throw error;
@@ -127,7 +113,13 @@ export const UnauthenticatedView: FC = () => {
} }
setClient({ client, session }); setClient({ client, session });
history.push(getRelativeRoomUrl(roomId, roomName)); history.push(
getRelativeRoomUrl(
createRoomResult.roomId,
roomName,
createRoomResult.password,
),
);
} }
submit().catch((error) => { submit().catch((error) => {
@@ -144,8 +136,7 @@ export const UnauthenticatedView: FC = () => {
history, history,
setJoinExistingCallModalOpen, setJoinExistingCallModalOpen,
setClient, setClient,
e2eeEnabled, ],
]
); );
return ( return (
@@ -202,7 +193,6 @@ export const UnauthenticatedView: FC = () => {
</Link> </Link>
</Trans> </Trans>
</Caption> </Caption>
<E2EEBanner />
{error && ( {error && (
<FieldRow> <FieldRow>
<ErrorMessage error={error} /> <ErrorMessage error={error} />

View File

@@ -31,7 +31,7 @@ export interface GroupCallRoom {
} }
const tsCache: { [index: string]: number } = {}; const tsCache: { [index: string]: number } = {};
function getLastTs(client: MatrixClient, r: Room) { function getLastTs(client: MatrixClient, r: Room): number {
if (tsCache[r.roomId]) { if (tsCache[r.roomId]) {
return tsCache[r.roomId]; return tsCache[r.roomId];
} }
@@ -47,7 +47,7 @@ function getLastTs(client: MatrixClient, r: Room) {
if (r.getMyMembership() !== "join") { if (r.getMyMembership() !== "join") {
const membershipEvent = r.currentState.getStateEvents( const membershipEvent = r.currentState.getStateEvents(
"m.room.member", "m.room.member",
myUserId myUserId,
); );
if (membershipEvent && !Array.isArray(membershipEvent)) { if (membershipEvent && !Array.isArray(membershipEvent)) {
@@ -82,7 +82,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const [rooms, setRooms] = useState<GroupCallRoom[]>([]); const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
useEffect(() => { useEffect(() => {
function updateRooms() { function updateRooms(): void {
if (!client.groupCallEventHandler) { if (!client.groupCallEventHandler) {
return; return;
} }
@@ -115,7 +115,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms); client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
client.removeListener( client.removeListener(
GroupCallEventHandlerEvent.Participants, GroupCallEventHandlerEvent.Participants,
updateRooms updateRooms,
); );
}; };
}, [client]); }, [client]);

View File

@@ -68,7 +68,8 @@ limitations under the License.
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-Regular.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-Regular.woff2") format("woff2"),
url("/fonts/Inter/Inter-Regular.woff") format("woff"); url("/fonts/Inter/Inter-Regular.woff") format("woff");
} }
@@ -78,7 +79,8 @@ limitations under the License.
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-Italic.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-Italic.woff2") format("woff2"),
url("/fonts/Inter/Inter-Italic.woff") format("woff"); url("/fonts/Inter/Inter-Italic.woff") format("woff");
} }
@@ -88,7 +90,8 @@ limitations under the License.
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-Medium.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-Medium.woff2") format("woff2"),
url("/fonts/Inter/Inter-Medium.woff") format("woff"); url("/fonts/Inter/Inter-Medium.woff") format("woff");
} }
@@ -98,7 +101,8 @@ limitations under the License.
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"),
url("/fonts/Inter/Inter-MediumItalic.woff") format("woff"); url("/fonts/Inter/Inter-MediumItalic.woff") format("woff");
} }
@@ -108,7 +112,8 @@ limitations under the License.
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"),
url("/fonts/Inter/Inter-SemiBold.woff") format("woff"); url("/fonts/Inter/Inter-SemiBold.woff") format("woff");
} }
@@ -118,7 +123,8 @@ limitations under the License.
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"),
url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff"); url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff");
} }
@@ -128,7 +134,8 @@ limitations under the License.
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-Bold.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-Bold.woff2") format("woff2"),
url("/fonts/Inter/Inter-Bold.woff") format("woff"); url("/fonts/Inter/Inter-Bold.woff") format("woff");
} }
@@ -138,7 +145,8 @@ limitations under the License.
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
unicode-range: var(--inter-unicode-range); unicode-range: var(--inter-unicode-range);
src: url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"), src:
url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"),
url("/fonts/Inter/Inter-BoldItalic.woff") format("woff"); url("/fonts/Inter/Inter-BoldItalic.woff") format("woff");
} }

View File

@@ -35,11 +35,11 @@ enum LoadState {
class DependencyLoadStates { class DependencyLoadStates {
// TODO: decide where olm should be initialized (see TODO comment below) // TODO: decide where olm should be initialized (see TODO comment below)
// olm: LoadState = LoadState.None; // olm: LoadState = LoadState.None;
config: LoadState = LoadState.None; public config: LoadState = LoadState.None;
sentry: LoadState = LoadState.None; public sentry: LoadState = LoadState.None;
openTelemetry: LoadState = LoadState.None; public openTelemetry: LoadState = LoadState.None;
allDepsAreLoaded() { public allDepsAreLoaded(): boolean {
return !Object.values(this).some((s) => s !== LoadState.Loaded); return !Object.values(this).some((s) => s !== LoadState.Loaded);
} }
} }
@@ -52,7 +52,7 @@ export class Initializer {
return Initializer.internalInstance?.isInitialized; return Initializer.internalInstance?.isInitialized;
} }
public static initBeforeReact() { public static initBeforeReact(): void {
// this maybe also needs to return a promise in the future, // this maybe also needs to return a promise in the future,
// if we have to do async inits before showing the loading screen // if we have to do async inits before showing the loading screen
// but this should be avioded if possible // but this should be avioded if possible
@@ -99,13 +99,13 @@ export class Initializer {
if (fontScale !== null) { if (fontScale !== null) {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
"--font-scale", "--font-scale",
fontScale.toString() fontScale.toString(),
); );
} }
if (fonts.length > 0) { if (fonts.length > 0) {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
"--font-family", "--font-family",
fonts.map((f) => `"${f}"`).join(", ") fonts.map((f) => `"${f}"`).join(", "),
); );
} }
@@ -126,9 +126,9 @@ export class Initializer {
return Initializer.internalInstance.initPromise; return Initializer.internalInstance.initPromise;
} }
loadStates = new DependencyLoadStates(); private loadStates = new DependencyLoadStates();
initStep(resolve: (value: void | PromiseLike<void>) => void) { private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
// TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`) // TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`)
// we need to decide if we want to init it here or keep it in initClient // we need to decide if we want to init it here or keep it in initClient
// if (this.loadStates.olm === LoadState.None) { // if (this.loadStates.olm === LoadState.None) {

View File

@@ -52,7 +52,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
onRemoveAvatar, onRemoveAvatar,
...rest ...rest
}, },
ref ref,
) => { ) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -64,7 +64,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
useEffect(() => { useEffect(() => {
const currentInput = fileInputRef.current; const currentInput = fileInputRef.current;
const onChange = (e: Event) => { const onChange = (e: Event): void => {
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>; const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
if (inputEvent.target.files && inputEvent.target.files.length > 0) { if (inputEvent.target.files && inputEvent.target.files.length > 0) {
setObjUrl(URL.createObjectURL(inputEvent.target.files[0])); setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
@@ -76,7 +76,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
currentInput.addEventListener("change", onChange); currentInput.addEventListener("change", onChange);
return () => { return (): void => {
currentInput?.removeEventListener("change", onChange); currentInput?.removeEventListener("change", onChange);
}; };
}); });
@@ -120,5 +120,5 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
)} )}
</div> </div>
); );
} },
); );

View File

@@ -85,8 +85,11 @@ limitations under the License.
} }
.inputField label { .inputField label {
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s, transition:
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s; font-size 0.25s ease-out 0.1s,
color 0.25s ease-out 0.1s,
top 0.25s ease-out 0.1s,
background-color 0.25s ease-out 0.1s;
color: var(--cpd-color-text-secondary); color: var(--cpd-color-text-secondary);
background-color: transparent; background-color: transparent;
font-size: var(--font-size-body); font-size: var(--font-size-body);
@@ -118,8 +121,11 @@ limitations under the License.
.inputField textarea:not(:placeholder-shown) + label, .inputField textarea:not(:placeholder-shown) + label,
.inputField.prefix textarea + label { .inputField.prefix textarea + label {
background-color: var(--cpd-color-bg-canvas-default); background-color: var(--cpd-color-bg-canvas-default);
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s, transition:
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s; font-size 0.25s ease-out 0s,
color 0.25s ease-out 0s,
top 0.25s ease-out 0s,
background-color 0.25s ease-out 0s;
font-size: var(--font-size-micro); font-size: var(--font-size-micro);
top: -13px; top: -13px;
padding: 0 2px; padding: 0 2px;

View File

@@ -44,7 +44,7 @@ export function FieldRow({
className={classNames( className={classNames(
styles.fieldRow, styles.fieldRow,
{ [styles.rightAlign]: rightAlign }, { [styles.rightAlign]: rightAlign },
className className,
)} )}
> >
{children} {children}
@@ -102,7 +102,7 @@ export const InputField = forwardRef<
disabled, disabled,
...rest ...rest
}, },
ref ref,
) => { ) => {
const descriptionId = useId(); const descriptionId = useId();
@@ -114,7 +114,7 @@ export const InputField = forwardRef<
[styles.prefix]: !!prefix, [styles.prefix]: !!prefix,
[styles.disabled]: disabled, [styles.disabled]: disabled,
}, },
className className,
)} )}
> >
{prefix && <span>{prefix}</span>} {prefix && <span>{prefix}</span>}
@@ -163,7 +163,7 @@ export const InputField = forwardRef<
)} )}
</Field> </Field>
); );
} },
); );
interface ErrorMessageProps { interface ErrorMessageProps {

View File

@@ -38,7 +38,7 @@ export function SelectInput(props: Props): JSX.Element {
const { labelProps, triggerProps, valueProps, menuProps } = useSelect( const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
props, props,
state, state,
ref ref,
); );
const { buttonProps } = useButton(triggerProps, ref); const { buttonProps } = useButton(triggerProps, ref);

View File

@@ -41,8 +41,8 @@ export function StarRatingInput({
return ( return (
<div <div
className={styles.inputContainer} className={styles.inputContainer}
onMouseEnter={() => setHover(index)} onMouseEnter={(): void => setHover(index)}
onMouseLeave={() => setHover(rating)} onMouseLeave={(): void => setHover(rating)}
key={index} key={index}
> >
<input <input
@@ -51,7 +51,7 @@ export function StarRatingInput({
id={"starInput" + String(index)} id={"starInput" + String(index)}
value={String(index) + "Star"} value={String(index) + "Star"}
name="star rating" name="star rating"
onChange={(_ev) => { onChange={(_ev): void => {
setRating(index); setRating(index);
onChange(index); onChange(index);
}} }}

View File

@@ -51,8 +51,8 @@ export interface MediaDevices {
// Cargo-culted from @livekit/components-react // Cargo-culted from @livekit/components-react
function useObservableState<T>( function useObservableState<T>(
observable: Observable<T> | undefined, observable: Observable<T> | undefined,
startWith: T startWith: T,
) { ): T {
const [state, setState] = useState<T>(startWith); const [state, setState] = useState<T>(startWith);
useEffect(() => { useEffect(() => {
// observable state doesn't run in SSR // observable state doesn't run in SSR
@@ -67,7 +67,7 @@ function useMediaDevice(
kind: MediaDeviceKind, kind: MediaDeviceKind,
fallbackDevice: string | undefined, fallbackDevice: string | undefined,
usingNames: boolean, usingNames: boolean,
alwaysDefault: boolean = false alwaysDefault: boolean = false,
): MediaDevice { ): MediaDevice {
// Make sure we don't needlessly reset to a device observer without names, // Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given // once permissions are already given
@@ -83,7 +83,7 @@ function useMediaDevice(
// kind, which then results in multiple permissions requests. // kind, which then results in multiple permissions requests.
const deviceObserver = useMemo( const deviceObserver = useMemo(
() => createMediaDeviceObserver(kind, requestPermissions), () => createMediaDeviceObserver(kind, requestPermissions),
[kind, requestPermissions] [kind, requestPermissions],
); );
const available = useObservableState(deviceObserver, []); const available = useObservableState(deviceObserver, []);
const [selectedId, select] = useState(fallbackDevice); const [selectedId, select] = useState(fallbackDevice);
@@ -143,18 +143,18 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
const audioInput = useMediaDevice( const audioInput = useMediaDevice(
"audioinput", "audioinput",
audioInputSetting, audioInputSetting,
usingNames usingNames,
); );
const audioOutput = useMediaDevice( const audioOutput = useMediaDevice(
"audiooutput", "audiooutput",
audioOutputSetting, audioOutputSetting,
useOutputNames, useOutputNames,
alwaysUseDefaultAudio alwaysUseDefaultAudio,
); );
const videoInput = useMediaDevice( const videoInput = useMediaDevice(
"videoinput", "videoinput",
videoInputSetting, videoInputSetting,
usingNames usingNames,
); );
useEffect(() => { useEffect(() => {
@@ -176,11 +176,11 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
const startUsingDeviceNames = useCallback( const startUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n + 1), () => setNumCallersUsingNames((n) => n + 1),
[setNumCallersUsingNames] [setNumCallersUsingNames],
); );
const stopUsingDeviceNames = useCallback( const stopUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n - 1), () => setNumCallersUsingNames((n) => n - 1),
[setNumCallersUsingNames] [setNumCallersUsingNames],
); );
const context: MediaDevices = useMemo( const context: MediaDevices = useMemo(
@@ -197,7 +197,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
videoInput, videoInput,
startUsingDeviceNames, startUsingDeviceNames,
stopUsingDeviceNames, stopUsingDeviceNames,
] ],
); );
return ( return (
@@ -207,7 +207,8 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
); );
}; };
export const useMediaDevices = () => useContext(MediaDevicesContext); export const useMediaDevices = (): MediaDevices =>
useContext(MediaDevicesContext);
/** /**
* React hook that requests for the media devices context to be populated with * React hook that requests for the media devices context to be populated with
@@ -215,7 +216,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext);
* default because it may involve requesting additional permissions from the * default because it may involve requesting additional permissions from the
* user. * user.
*/ */
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) => export const useMediaDeviceNames = (
context: MediaDevices,
enabled = true,
): void =>
useEffect(() => { useEffect(() => {
if (enabled) { if (enabled) {
context.startUsingDeviceNames(); context.startUsingDeviceNames();

View File

@@ -42,14 +42,14 @@ export type OpenIDClientParts = Pick<
export function useOpenIDSFU( export function useOpenIDSFU(
client: OpenIDClientParts, client: OpenIDClientParts,
rtcSession: MatrixRTCSession rtcSession: MatrixRTCSession,
) { ): SFUConfig | undefined {
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined); const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
const activeFocus = useActiveFocus(rtcSession); const activeFocus = useActiveFocus(rtcSession);
useEffect(() => { useEffect(() => {
(async () => { (async (): Promise<void> => {
const sfuConfig = activeFocus const sfuConfig = activeFocus
? await getSFUConfigWithOpenID(client, activeFocus) ? await getSFUConfigWithOpenID(client, activeFocus)
: undefined; : undefined;
@@ -62,20 +62,20 @@ export function useOpenIDSFU(
export async function getSFUConfigWithOpenID( export async function getSFUConfigWithOpenID(
client: OpenIDClientParts, client: OpenIDClientParts,
activeFocus: LivekitFocus activeFocus: LivekitFocus,
): Promise<SFUConfig | undefined> { ): Promise<SFUConfig | undefined> {
const openIdToken = await client.getOpenIdToken(); const openIdToken = await client.getOpenIdToken();
logger.debug("Got openID token", openIdToken); logger.debug("Got openID token", openIdToken);
try { try {
logger.info( logger.info(
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...` `Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`,
); );
const sfuConfig = await getLiveKitJWT( const sfuConfig = await getLiveKitJWT(
client, client,
activeFocus.livekit_service_url, activeFocus.livekit_service_url,
activeFocus.livekit_alias, activeFocus.livekit_alias,
openIdToken openIdToken,
); );
logger.info(`Got JWT from call's active focus URL.`); logger.info(`Got JWT from call's active focus URL.`);
@@ -83,7 +83,7 @@ export async function getSFUConfigWithOpenID(
} catch (e) { } catch (e) {
logger.warn( logger.warn(
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`, `Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
e e,
); );
return undefined; return undefined;
} }
@@ -93,7 +93,7 @@ async function getLiveKitJWT(
client: OpenIDClientParts, client: OpenIDClientParts,
livekitServiceURL: string, livekitServiceURL: string,
roomName: string, roomName: string,
openIDToken: IOpenIDToken openIDToken: IOpenIDToken,
): Promise<SFUConfig> { ): Promise<SFUConfig> {
try { try {
const res = await fetch(livekitServiceURL + "/sfu/get", { const res = await fetch(livekitServiceURL + "/sfu/get", {

View File

@@ -1,3 +1,19 @@
/*
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 { import {
AudioPresets, AudioPresets,
DefaultReconnectPolicy, DefaultReconnectPolicy,

View File

@@ -19,6 +19,7 @@ import {
ConnectionState, ConnectionState,
Room, Room,
RoomEvent, RoomEvent,
Track,
} from "livekit-client"; } from "livekit-client";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -51,7 +52,7 @@ async function doConnect(
livekitRoom: Room, livekitRoom: Room,
sfuConfig: SFUConfig, sfuConfig: SFUConfig,
audioEnabled: boolean, audioEnabled: boolean,
audioOptions: AudioCaptureOptions audioOptions: AudioCaptureOptions,
): Promise<void> { ): Promise<void> {
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt); await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
@@ -60,6 +61,14 @@ async function doConnect(
// doesn't publish it until you unmute. We want to publish it from the start so we're // doesn't publish it until you unmute. We want to publish it from the start so we're
// always capturing audio: it helps keep bluetooth headsets in the right mode and // always capturing audio: it helps keep bluetooth headsets in the right mode and
// mobile browsers to know we're doing a call. // mobile browsers to know we're doing a call.
if (livekitRoom!.localParticipant.getTrack(Track.Source.Microphone)) {
logger.warn(
"Pre-creating audio track but participant already appears to have an microphone track: this shouldn't happen!",
);
return;
}
logger.info("Pre-creating microphone track");
const audioTracks = await livekitRoom!.localParticipant.createTracks({ const audioTracks = await livekitRoom!.localParticipant.createTracks({
audio: audioOptions, audio: audioOptions,
}); });
@@ -69,6 +78,14 @@ async function doConnect(
} }
if (!audioEnabled) await audioTracks[0].mute(); if (!audioEnabled) await audioTracks[0].mute();
// check again having awaited for the track to create
if (livekitRoom!.localParticipant.getTrack(Track.Source.Microphone)) {
logger.warn(
"Publishing pre-created audio track but participant already appears to have an microphone track: this shouldn't happen!",
);
return;
}
logger.info("Publishing pre-created mic track");
await livekitRoom?.localParticipant.publishTrack(audioTracks[0]); await livekitRoom?.localParticipant.publishTrack(audioTracks[0]);
} }
@@ -76,12 +93,12 @@ export function useECConnectionState(
initialAudioOptions: AudioCaptureOptions, initialAudioOptions: AudioCaptureOptions,
initialAudioEnabled: boolean, initialAudioEnabled: boolean,
livekitRoom?: Room, livekitRoom?: Room,
sfuConfig?: SFUConfig sfuConfig?: SFUConfig,
): ECConnectionState { ): ECConnectionState {
const [connState, setConnState] = useState( const [connState, setConnState] = useState(
sfuConfig && livekitRoom sfuConfig && livekitRoom
? livekitRoom.state ? livekitRoom.state
: ECAddonConnectionState.ECWaiting : ECAddonConnectionState.ECWaiting,
); );
const [isSwitchingFocus, setSwitchingFocus] = useState(false); const [isSwitchingFocus, setSwitchingFocus] = useState(false);
@@ -116,10 +133,10 @@ export function useECConnectionState(
!sfuConfigEquals(currentSFUConfig.current, sfuConfig) !sfuConfigEquals(currentSFUConfig.current, sfuConfig)
) { ) {
logger.info( logger.info(
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}` `SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`,
); );
(async () => { (async (): Promise<void> => {
setSwitchingFocus(true); setSwitchingFocus(true);
await livekitRoom?.disconnect(); await livekitRoom?.disconnect();
setIsInDoConnect(true); setIsInDoConnect(true);
@@ -128,7 +145,7 @@ export function useECConnectionState(
livekitRoom!, livekitRoom!,
sfuConfig!, sfuConfig!,
initialAudioEnabled, initialAudioEnabled,
initialAudioOptions initialAudioOptions,
); );
} finally { } finally {
setIsInDoConnect(false); setIsInDoConnect(false);
@@ -149,7 +166,7 @@ export function useECConnectionState(
livekitRoom!, livekitRoom!,
sfuConfig!, sfuConfig!,
initialAudioEnabled, initialAudioEnabled,
initialAudioOptions initialAudioOptions,
).finally(() => setIsInDoConnect(false)); ).finally(() => setIsInDoConnect(false));
} }

View File

@@ -20,6 +20,7 @@ import {
ExternalE2EEKeyProvider, ExternalE2EEKeyProvider,
Room, Room,
RoomOptions, RoomOptions,
Track,
} from "livekit-client"; } from "livekit-client";
import { useLiveKitRoom } from "@livekit/components-react"; import { useLiveKitRoom } from "@livekit/components-react";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
@@ -51,7 +52,7 @@ interface UseLivekitResult {
export function useLiveKit( export function useLiveKit(
muteStates: MuteStates, muteStates: MuteStates,
sfuConfig?: SFUConfig, sfuConfig?: SFUConfig,
e2eeConfig?: E2EEConfig e2eeConfig?: E2EEConfig,
): UseLivekitResult { ): UseLivekitResult {
const e2eeOptions = useMemo(() => { const e2eeOptions = useMemo(() => {
if (!e2eeConfig?.sharedKey) return undefined; if (!e2eeConfig?.sharedKey) return undefined;
@@ -66,7 +67,7 @@ export function useLiveKit(
if (!e2eeConfig?.sharedKey || !e2eeOptions) return; if (!e2eeConfig?.sharedKey || !e2eeOptions) return;
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
e2eeConfig?.sharedKey e2eeConfig?.sharedKey,
); );
}, [e2eeOptions, e2eeConfig?.sharedKey]); }, [e2eeOptions, e2eeConfig?.sharedKey]);
@@ -92,7 +93,7 @@ export function useLiveKit(
}, },
e2ee: e2eeOptions, e2ee: e2eeOptions,
}), }),
[e2eeOptions] [e2eeOptions],
); );
// useECConnectionState creates and publishes an audio track by hand. To keep // useECConnectionState creates and publishes an audio track by hand. To keep
@@ -100,6 +101,17 @@ export function useLiveKit(
// block audio from being enabled until the connection is finished. // block audio from being enabled until the connection is finished.
const [blockAudio, setBlockAudio] = useState(true); const [blockAudio, setBlockAudio] = useState(true);
// Store if audio/video are currently updating. If to prohibit unnecessary calls
// to setMicrophoneEnabled/setCameraEnabled
const audioMuteUpdating = useRef(false);
const videoMuteUpdating = useRef(false);
// Store the current button mute state that gets passed to this hook via props.
// We need to store it for awaited code that relies on the current value.
const buttonEnabled = useRef({
audio: initialMuteStates.current.audio.enabled,
video: initialMuteStates.current.video.enabled,
});
// We have to create the room manually here due to a bug inside // We have to create the room manually here due to a bug inside
// @livekit/components-react. JSON.stringify() is used in deps of a // @livekit/components-react. JSON.stringify() is used in deps of a
// useEffect() with an argument that references itself, if E2EE is enabled // useEffect() with an argument that references itself, if E2EE is enabled
@@ -115,11 +127,11 @@ export function useLiveKit(
const connectionState = useECConnectionState( const connectionState = useECConnectionState(
{ {
deviceId: initialDevices.current.audioOutput.selectedId, deviceId: initialDevices.current.audioInput.selectedId,
}, },
initialMuteStates.current.audio.enabled, initialMuteStates.current.audio.enabled,
room, room,
sfuConfig sfuConfig,
); );
// Unblock audio once the connection is finished // Unblock audio once the connection is finished
@@ -136,35 +148,115 @@ export function useLiveKit(
// and setting tracks to be enabled during this time causes errors. // and setting tracks to be enabled during this time causes errors.
if (room !== undefined && connectionState === ConnectionState.Connected) { if (room !== undefined && connectionState === ConnectionState.Connected) {
const participant = room.localParticipant; const participant = room.localParticipant;
if (participant.isMicrophoneEnabled !== muteStates.audio.enabled) { // Always update the muteButtonState Ref so that we can read the current
participant // state in awaited blocks.
.setMicrophoneEnabled(muteStates.audio.enabled) buttonEnabled.current = {
.catch((e) => audio: muteStates.audio.enabled,
logger.error("Failed to sync audio mute state with LiveKit", e) video: muteStates.video.enabled,
); };
const syncMuteStateAudio = async (): Promise<void> => {
if (
participant.isMicrophoneEnabled !== buttonEnabled.current.audio &&
!audioMuteUpdating.current
) {
audioMuteUpdating.current = true;
try {
await participant.setMicrophoneEnabled(buttonEnabled.current.audio);
} catch (e) {
logger.error("Failed to sync audio mute state with LiveKit", e);
} }
if (participant.isCameraEnabled !== muteStates.video.enabled) { audioMuteUpdating.current = false;
participant // await participant.setMicrophoneEnabled can return immediately in some instances,
.setCameraEnabled(muteStates.video.enabled) // so that participant.isMicrophoneEnabled !== buttonEnabled.current.audio still holds true.
.catch((e) => // This happens if the device is still in a pending state
logger.error("Failed to sync video mute state with LiveKit", e) // "sleeping" here makes sure we let react do its thing so that participant.isMicrophoneEnabled is updated,
); // so we do not end up in a recursion loop.
await new Promise((r) => setTimeout(r, 20));
// Run the check again after the change is done. Because the user
// can update the state (presses mute button) while the device is enabling
// itself we need might need to update the mute state right away.
// This async recursion makes sure that setCamera/MicrophoneEnabled is
// called as little times as possible.
syncMuteStateAudio();
} }
};
const syncMuteStateVideo = async (): Promise<void> => {
if (
participant.isCameraEnabled !== buttonEnabled.current.video &&
!videoMuteUpdating.current
) {
videoMuteUpdating.current = true;
try {
await participant.setCameraEnabled(buttonEnabled.current.video);
} catch (e) {
logger.error("Failed to sync audio mute state with LiveKit", e);
}
videoMuteUpdating.current = false;
// see above
await new Promise((r) => setTimeout(r, 20));
// see above
syncMuteStateVideo();
}
};
syncMuteStateAudio();
syncMuteStateVideo();
} }
}, [room, muteStates, connectionState]); }, [room, muteStates, connectionState]);
useEffect(() => { useEffect(() => {
// Sync the requested devices with LiveKit's devices // Sync the requested devices with LiveKit's devices
if (room !== undefined && connectionState === ConnectionState.Connected) { if (room !== undefined && connectionState === ConnectionState.Connected) {
const syncDevice = (kind: MediaDeviceKind, device: MediaDevice) => { const syncDevice = (kind: MediaDeviceKind, device: MediaDevice): void => {
const id = device.selectedId; const id = device.selectedId;
// Detect if we're trying to use chrome's default device, in which case
// we need to to see if the default device has changed to a different device
// by comparing the group ID of the device we're using against the group ID
// of what the default device is *now*.
// This is special-cased for only audio inputs because we need to dig around
// in the LocalParticipant object for the track object and there's not a nice
// way to do that generically. There is usually no OS-level default video capture
// device anyway, and audio outputs work differently.
if (
id === "default" &&
kind === "audioinput" &&
room.options.audioCaptureDefaults?.deviceId === "default"
) {
const activeMicTrack = Array.from(
room.localParticipant.audioTracks.values(),
).find((d) => d.source === Track.Source.Microphone)?.track;
const defaultDevice = device.available.find(
(d) => d.deviceId === "default",
);
if (
defaultDevice &&
activeMicTrack &&
// only restart if the stream is still running: LiveKit will detect
// when a track stops & restart appropriately, so this is not our job.
// Plus, we need to avoid restarting again if the track is already in
// the process of being restarted.
activeMicTrack.mediaStreamTrack.readyState !== "ended" &&
defaultDevice.groupId !==
activeMicTrack.mediaStreamTrack.getSettings().groupId
) {
// It's different, so restart the track, ie. cause Livekit to do another
// getUserMedia() call with deviceId: default to get the *new* default device.
// Note that room.switchActiveDevice() won't work: Livekit will ignore it because
// the deviceId hasn't changed (was & still is default).
room.localParticipant
.getTrack(Track.Source.Microphone)
?.audioTrack?.restartTrack();
}
} else {
if (id !== undefined && room.getActiveDevice(kind) !== id) { if (id !== undefined && room.getActiveDevice(kind) !== id) {
room room
.switchActiveDevice(kind, id) .switchActiveDevice(kind, id)
.catch((e) => .catch((e) =>
logger.error(`Failed to sync ${kind} device with LiveKit`, e) logger.error(`Failed to sync ${kind} device with LiveKit`, e),
); );
} }
}
}; };
syncDevice("audioinput", devices.audioInput); syncDevice("audioinput", devices.audioInput);

View File

@@ -30,7 +30,7 @@ import {
setLogLevel, setLogLevel,
} from "livekit-client"; } from "livekit-client";
import App from "./App"; import { App } from "./App";
import { init as initRageshake } from "./settings/rageshake"; import { init as initRageshake } from "./settings/rageshake";
import { Initializer } from "./initializer"; import { Initializer } from "./initializer";
@@ -48,7 +48,7 @@ if (!window.isSecureContext) {
fatalError = new Error( fatalError = new Error(
"This app cannot run in an insecure context. To fix this, access the app " + "This app cannot run in an insecure context. To fix this, access the app " +
"via a local loopback address, or serve it over HTTPS.\n" + "via a local loopback address, or serve it over HTTPS.\n" +
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
); );
} else if (!navigator.mediaDevices) { } else if (!navigator.mediaDevices) {
fatalError = new Error("Your browser does not support WebRTC."); fatalError = new Error("Your browser does not support WebRTC.");
@@ -66,5 +66,5 @@ const history = createBrowserHistory();
root.render( root.render(
<StrictMode> <StrictMode>
<App history={history} /> <App history={history} />
</StrictMode> </StrictMode>,
); );

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -35,12 +35,14 @@ import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getUrlParams, PASSWORD_STRING } from "./UrlParams"; import { getUrlParams, PASSWORD_STRING } from "./UrlParams";
import { loadOlm } from "./olm"; import { loadOlm } from "./olm";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { setLocalStorageItem } from "./useLocalStorage";
import { getRoomSharedKeyLocalStorageKey } from "./e2ee/sharedKeyManagement";
export const fallbackICEServerAllowed = export const fallbackICEServerAllowed =
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
export class CryptoStoreIntegrityError extends Error { export class CryptoStoreIntegrityError extends Error {
constructor() { public constructor() {
super("Crypto store data was expected, but none was found"); super("Crypto store data was expected, but none was found");
} }
} }
@@ -52,13 +54,13 @@ const SYNC_STORE_NAME = "element-call-sync";
// (It's a good opportunity to make the database names consistent.) // (It's a good opportunity to make the database names consistent.)
const CRYPTO_STORE_NAME = "element-call-crypto"; const CRYPTO_STORE_NAME = "element-call-crypto";
function waitForSync(client: MatrixClient) { function waitForSync(client: MatrixClient): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const onSync = ( const onSync = (
state: SyncState, state: SyncState,
_old: SyncState | null, _old: SyncState | null,
data?: ISyncStateData data?: ISyncStateData,
) => { ): void => {
if (state === "PREPARED") { if (state === "PREPARED") {
client.removeListener(ClientEvent.Sync, onSync); client.removeListener(ClientEvent.Sync, onSync);
resolve(); resolve();
@@ -71,6 +73,23 @@ function waitForSync(client: MatrixClient) {
}); });
} }
function secureRandomString(entropyBytes: number): string {
const key = new Uint8Array(entropyBytes);
crypto.getRandomValues(key);
// encode to base64url as this value goes into URLs
// base64url is just base64 with thw two non-alphanum characters swapped out for
// ones that can be put in a URL without encoding. Browser JS has a native impl
// for base64 encoding but only a string (there isn't one that takes a UInt8Array
// yet) so just use the built-in one and convert, replace the chars and strip the
// padding from the end (otherwise we'd need to pull in another dependency).
return btoa(
key.reduce((acc, current) => acc + String.fromCharCode(current), ""),
)
.replace("+", "-")
.replace("/", "_")
.replace(/=*$/, "");
}
/** /**
* Initialises and returns a new standalone Matrix Client. * Initialises and returns a new standalone Matrix Client.
* If true is passed for the 'restore' parameter, a check will be made * If true is passed for the 'restore' parameter, a check will be made
@@ -82,7 +101,7 @@ function waitForSync(client: MatrixClient) {
*/ */
export async function initClient( export async function initClient(
clientOptions: ICreateClientOpts, clientOptions: ICreateClientOpts,
restore: boolean restore: boolean,
): Promise<MatrixClient> { ): Promise<MatrixClient> {
await loadOlm(); await loadOlm();
@@ -108,7 +127,7 @@ export async function initClient(
// Chrome supports it. (It bundles them fine in production mode.) // Chrome supports it. (It bundles them fine in production mode.)
workerFactory: import.meta.env.DEV workerFactory: import.meta.env.DEV
? undefined ? undefined
: () => new IndexedDBWorker(), : (): Worker => new IndexedDBWorker(),
}); });
} else if (localStorage) { } else if (localStorage) {
baseOpts.store = new MemoryStore({ localStorage }); baseOpts.store = new MemoryStore({ localStorage });
@@ -129,7 +148,7 @@ export async function initClient(
if (indexedDB) { if (indexedDB) {
const cryptoStoreExists = await IndexedDBCryptoStore.exists( const cryptoStoreExists = await IndexedDBCryptoStore.exists(
indexedDB, indexedDB,
CRYPTO_STORE_NAME CRYPTO_STORE_NAME,
); );
if (!cryptoStoreExists) throw new CryptoStoreIntegrityError(); if (!cryptoStoreExists) throw new CryptoStoreIntegrityError();
} else if (localStorage) { } else if (localStorage) {
@@ -145,7 +164,7 @@ export async function initClient(
if (indexedDB) { if (indexedDB) {
baseOpts.cryptoStore = new IndexedDBCryptoStore( baseOpts.cryptoStore = new IndexedDBCryptoStore(
indexedDB, indexedDB,
CRYPTO_STORE_NAME CRYPTO_STORE_NAME,
); );
} else if (localStorage) { } else if (localStorage) {
baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
@@ -179,7 +198,7 @@ export async function initClient(
} catch (error) { } catch (error) {
logger.error( logger.error(
"Error starting matrix client store. Falling back to memory store.", "Error starting matrix client store. Falling back to memory store.",
error error,
); );
client.store = new MemoryStore({ localStorage }); client.store = new MemoryStore({ localStorage });
await client.store.startup(); await client.store.startup();
@@ -249,7 +268,7 @@ export function roomNameFromRoomId(roomId: string): string {
.substring(1) .substring(1)
.split("-") .split("-")
.map((part) => .map((part) =>
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part,
) )
.join(" ") .join(" ")
.toLowerCase(); .toLowerCase();
@@ -269,11 +288,17 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
return parts[1] === client.getDomain(); return parts[1] === client.getDomain();
} }
interface CreateRoomResult {
roomId: string;
alias?: string;
password?: string;
}
export async function createRoom( export async function createRoom(
client: MatrixClient, client: MatrixClient,
name: string, name: string,
e2ee: boolean e2ee: boolean,
): Promise<[string, string]> { ): Promise<CreateRoomResult> {
logger.log(`Creating room for group call`); logger.log(`Creating room for group call`);
const createPromise = client.createRoom({ const createPromise = client.createRoom({
visibility: Visibility.Private, visibility: Visibility.Private,
@@ -307,7 +332,7 @@ export async function createRoom(
// Wait for the room to arrive // Wait for the room to arrive
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const onRoom = async (room: Room) => { const onRoom = async (room: Room): Promise<void> => {
if (room.roomId === (await createPromise).room_id) { if (room.roomId === (await createPromise).room_id) {
resolve(); resolve();
cleanUp(); cleanUp();
@@ -318,7 +343,7 @@ export async function createRoom(
cleanUp(); cleanUp();
}); });
const cleanUp = () => { const cleanUp = (): void => {
client.off(ClientEvent.Room, onRoom); client.off(ClientEvent.Room, onRoom);
}; };
client.on(ClientEvent.Room, onRoom); client.on(ClientEvent.Room, onRoom);
@@ -333,10 +358,23 @@ export async function createRoom(
GroupCallType.Video, GroupCallType.Video,
false, false,
GroupCallIntent.Room, GroupCallIntent.Room,
true true,
); );
return [fullAliasFromRoomName(name, client), result.room_id]; let password;
if (e2ee) {
password = secureRandomString(16);
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(result.room_id),
password,
);
}
return {
roomId: result.room_id,
alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
password,
};
} }
/** /**
@@ -348,7 +386,7 @@ export async function createRoom(
export function getAbsoluteRoomUrl( export function getAbsoluteRoomUrl(
roomId: string, roomId: string,
roomName?: string, roomName?: string,
password?: string password?: string,
): string { ): string {
return `${window.location.protocol}//${ return `${window.location.protocol}//${
window.location.host window.location.host
@@ -364,17 +402,24 @@ export function getAbsoluteRoomUrl(
export function getRelativeRoomUrl( export function getRelativeRoomUrl(
roomId: string, roomId: string,
roomName?: string, roomName?: string,
password?: string password?: string,
): string { ): string {
// The password shouldn't need URL encoding here (we generate URL-safe ones) but encode
// it in case it came from another client that generated a non url-safe one
const encodedPassword = password ? encodeURIComponent(password) : undefined;
if (password && encodedPassword !== password) {
logger.info("Encoded call password used non URL-safe chars: buggy client?");
}
return `/room/#${ return `/room/#${
roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : "" roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : ""
}?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`; }?roomId=${roomId}${password ? "&" + PASSWORD_STRING + encodedPassword : ""}`;
} }
export function getAvatarUrl( export function getAvatarUrl(
client: MatrixClient, client: MatrixClient,
mxcUrl: string, mxcUrl: string,
avatarSize = 96 avatarSize = 96,
): string { ): string {
const width = Math.floor(avatarSize * window.devicePixelRatio); const width = Math.floor(avatarSize * window.devicePixelRatio);
const height = Math.floor(avatarSize * window.devicePixelRatio); const height = Math.floor(avatarSize * window.devicePixelRatio);

View File

@@ -23,10 +23,10 @@ limitations under the License.
export async function findDeviceByName( export async function findDeviceByName(
deviceName: string, deviceName: string,
kind: MediaDeviceKind, kind: MediaDeviceKind,
devices: MediaDeviceInfo[] devices: MediaDeviceInfo[],
): Promise<string | undefined> { ): Promise<string | undefined> {
const deviceInfo = devices.find( const deviceInfo = devices.find(
(d) => d.kind === kind && d.label === deviceName (d) => d.kind === kind && d.label === deviceName,
); );
return deviceInfo?.deviceId; return deviceInfo?.deviceId;
} }

View File

@@ -44,65 +44,65 @@ export class OTelCall {
OTelCallAbstractMediaStreamSpan OTelCallAbstractMediaStreamSpan
>(); >();
constructor( public constructor(
public userId: string, public userId: string,
public deviceId: string, public deviceId: string,
public call: MatrixCall, public call: MatrixCall,
public span: Span public span: Span,
) { ) {
if (call.peerConn) { if (call.peerConn) {
this.addCallPeerConnListeners(); this.addCallPeerConnListeners();
} else { } else {
this.call.once( this.call.once(
CallEvent.PeerConnectionCreated, CallEvent.PeerConnectionCreated,
this.addCallPeerConnListeners this.addCallPeerConnListeners,
); );
} }
} }
public dispose() { public dispose(): void {
this.call.peerConn?.removeEventListener( this.call.peerConn?.removeEventListener(
"connectionstatechange", "connectionstatechange",
this.onCallConnectionStateChanged this.onCallConnectionStateChanged,
); );
this.call.peerConn?.removeEventListener( this.call.peerConn?.removeEventListener(
"signalingstatechange", "signalingstatechange",
this.onCallSignalingStateChanged this.onCallSignalingStateChanged,
); );
this.call.peerConn?.removeEventListener( this.call.peerConn?.removeEventListener(
"iceconnectionstatechange", "iceconnectionstatechange",
this.onIceConnectionStateChanged this.onIceConnectionStateChanged,
); );
this.call.peerConn?.removeEventListener( this.call.peerConn?.removeEventListener(
"icegatheringstatechange", "icegatheringstatechange",
this.onIceGatheringStateChanged this.onIceGatheringStateChanged,
); );
this.call.peerConn?.removeEventListener( this.call.peerConn?.removeEventListener(
"icecandidateerror", "icecandidateerror",
this.onIceCandidateError this.onIceCandidateError,
); );
} }
private addCallPeerConnListeners = (): void => { private addCallPeerConnListeners = (): void => {
this.call.peerConn?.addEventListener( this.call.peerConn?.addEventListener(
"connectionstatechange", "connectionstatechange",
this.onCallConnectionStateChanged this.onCallConnectionStateChanged,
); );
this.call.peerConn?.addEventListener( this.call.peerConn?.addEventListener(
"signalingstatechange", "signalingstatechange",
this.onCallSignalingStateChanged this.onCallSignalingStateChanged,
); );
this.call.peerConn?.addEventListener( this.call.peerConn?.addEventListener(
"iceconnectionstatechange", "iceconnectionstatechange",
this.onIceConnectionStateChanged this.onIceConnectionStateChanged,
); );
this.call.peerConn?.addEventListener( this.call.peerConn?.addEventListener(
"icegatheringstatechange", "icegatheringstatechange",
this.onIceGatheringStateChanged this.onIceGatheringStateChanged,
); );
this.call.peerConn?.addEventListener( this.call.peerConn?.addEventListener(
"icecandidateerror", "icecandidateerror",
this.onIceCandidateError this.onIceCandidateError,
); );
}; };
@@ -147,8 +147,8 @@ export class OTelCall {
new OTelCallFeedMediaStreamSpan( new OTelCallFeedMediaStreamSpan(
ElementCallOpenTelemetry.instance, ElementCallOpenTelemetry.instance,
this.span, this.span,
feed feed,
) ),
); );
} }
this.trackFeedSpan.get(feed.stream)?.update(feed); this.trackFeedSpan.get(feed.stream)?.update(feed);
@@ -171,13 +171,13 @@ export class OTelCall {
new OTelCallTransceiverMediaStreamSpan( new OTelCallTransceiverMediaStreamSpan(
ElementCallOpenTelemetry.instance, ElementCallOpenTelemetry.instance,
this.span, this.span,
transStats transStats,
) ),
); );
} }
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats); this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
prvTransSpan = prvTransSpan.filter( prvTransSpan = prvTransSpan.filter(
(prvStreamId) => prvStreamId !== transStats.mid (prvStreamId) => prvStreamId !== transStats.mid,
); );
}); });
@@ -190,7 +190,7 @@ export class OTelCall {
public end(): void { public end(): void {
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end()); this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
this.trackTransceiverSpan.forEach((transceiverSpan) => this.trackTransceiverSpan.forEach((transceiverSpan) =>
transceiverSpan.end() transceiverSpan.end(),
); );
this.span.end(); this.span.end();
} }

View File

@@ -1,3 +1,19 @@
/*
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 opentelemetry, { Span } from "@opentelemetry/api"; import opentelemetry, { Span } from "@opentelemetry/api";
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
@@ -14,13 +30,13 @@ export abstract class OTelCallAbstractMediaStreamSpan {
public readonly span; public readonly span;
public constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span, protected readonly callSpan: Span,
protected readonly type: string protected readonly type: string,
) { ) {
const ctx = opentelemetry.trace.setSpan( const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(), opentelemetry.context.active(),
callSpan callSpan,
); );
const options = { const options = {
links: [ links: [
@@ -32,13 +48,13 @@ export abstract class OTelCallAbstractMediaStreamSpan {
this.span = oTel.tracer.startSpan(this.type, options, ctx); this.span = oTel.tracer.startSpan(this.type, options, ctx);
} }
protected upsertTrackSpans(tracks: TrackStats[]) { protected upsertTrackSpans(tracks: TrackStats[]): void {
let prvTracks: TrackId[] = [...this.trackSpans.keys()]; let prvTracks: TrackId[] = [...this.trackSpans.keys()];
tracks.forEach((t) => { tracks.forEach((t) => {
if (!this.trackSpans.has(t.id)) { if (!this.trackSpans.has(t.id)) {
this.trackSpans.set( this.trackSpans.set(
t.id, t.id,
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t) new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t),
); );
} }
this.trackSpans.get(t.id)?.update(t); this.trackSpans.get(t.id)?.update(t);

View File

@@ -1,3 +1,19 @@
/*
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 { Span } from "@opentelemetry/api"; import { Span } from "@opentelemetry/api";
import { import {
CallFeedStats, CallFeedStats,
@@ -10,10 +26,10 @@ import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSp
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean }; private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span, protected readonly callSpan: Span,
callFeed: CallFeedStats callFeed: CallFeedStats,
) { ) {
const postFix = const postFix =
callFeed.type === "local" && callFeed.prefix === "from-call-feed" callFeed.type === "local" && callFeed.prefix === "from-call-feed"

View File

@@ -1,3 +1,19 @@
/*
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 { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
import opentelemetry, { Span } from "@opentelemetry/api"; import opentelemetry, { Span } from "@opentelemetry/api";
@@ -8,13 +24,13 @@ export class OTelCallMediaStreamTrackSpan {
private prev: TrackStats; private prev: TrackStats;
public constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly streamSpan: Span, protected readonly streamSpan: Span,
data: TrackStats data: TrackStats,
) { ) {
const ctx = opentelemetry.trace.setSpan( const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(), opentelemetry.context.active(),
streamSpan streamSpan,
); );
const options = { const options = {
links: [ links: [

View File

@@ -1,3 +1,19 @@
/*
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 { Span } from "@opentelemetry/api"; import { Span } from "@opentelemetry/api";
import { import {
TrackStats, TrackStats,
@@ -13,10 +29,10 @@ export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStr
currentDirection: string; currentDirection: string;
}; };
constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span, protected readonly callSpan: Span,
stats: TransceiverStats stats: TransceiverStats,
) { ) {
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`); super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);
this.span.setAttribute("transceiver.mid", stats.mid); this.span.setAttribute("transceiver.mid", stats.mid);

View File

@@ -62,7 +62,10 @@ export class OTelGroupCallMembership {
}; };
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>(); private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
constructor(private groupCall: GroupCall, client: MatrixClient) { public constructor(
private groupCall: GroupCall,
client: MatrixClient,
) {
const clientId = client.getUserId(); const clientId = client.getUserId();
if (clientId) { if (clientId) {
this.myUserId = clientId; this.myUserId = clientId;
@@ -76,14 +79,14 @@ export class OTelGroupCallMembership {
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
} }
dispose() { public dispose(): void {
this.groupCall.removeListener( this.groupCall.removeListener(
GroupCallEvent.CallsChanged, GroupCallEvent.CallsChanged,
this.onCallsChanged this.onCallsChanged,
); );
} }
public onJoinCall() { public onJoinCall(): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
if (this.callMembershipSpan !== undefined) { if (this.callMembershipSpan !== undefined) {
logger.warn("Call membership span is already started"); logger.warn("Call membership span is already started");
@@ -93,28 +96,28 @@ export class OTelGroupCallMembership {
// Create the main span that tracks the time we intend to be in the call // Create the main span that tracks the time we intend to be in the call
this.callMembershipSpan = this.callMembershipSpan =
ElementCallOpenTelemetry.instance.tracer.startSpan( ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.groupCallMembership" "matrix.groupCallMembership",
); );
this.callMembershipSpan.setAttribute( this.callMembershipSpan.setAttribute(
"matrix.confId", "matrix.confId",
this.groupCall.groupCallId this.groupCall.groupCallId,
); );
this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId);
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
this.callMembershipSpan.setAttribute( this.callMembershipSpan.setAttribute(
"matrix.displayName", "matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name" this.myMember ? this.myMember.name : "unknown-name",
); );
this.groupCallContext = opentelemetry.trace.setSpan( this.groupCallContext = opentelemetry.trace.setSpan(
opentelemetry.context.active(), opentelemetry.context.active(),
this.callMembershipSpan this.callMembershipSpan,
); );
this.callMembershipSpan?.addEvent("matrix.joinCall"); this.callMembershipSpan?.addEvent("matrix.joinCall");
} }
public onLeaveCall() { public onLeaveCall(): void {
if (this.callMembershipSpan === undefined) { if (this.callMembershipSpan === undefined) {
logger.warn("Call membership span is already ended"); logger.warn("Call membership span is already ended");
return; return;
@@ -127,7 +130,7 @@ export class OTelGroupCallMembership {
this.groupCallContext = undefined; this.groupCallContext = undefined;
} }
public onUpdateRoomState(event: MatrixEvent) { public onUpdateRoomState(event: MatrixEvent): void {
if ( if (
!event || !event ||
(!event.getType().startsWith("m.call") && (!event.getType().startsWith("m.call") &&
@@ -138,11 +141,11 @@ export class OTelGroupCallMembership {
this.callMembershipSpan?.addEvent( this.callMembershipSpan?.addEvent(
`matrix.roomStateEvent_${event.getType()}`, `matrix.roomStateEvent_${event.getType()}`,
ObjectFlattener.flattenVoipEvent(event.getContent()) ObjectFlattener.flattenVoipEvent(event.getContent()),
); );
} }
public onCallsChanged = (calls: CallsByUserAndDevice) => { public onCallsChanged(calls: CallsByUserAndDevice): void {
for (const [userId, userCalls] of calls.entries()) { for (const [userId, userCalls] of calls.entries()) {
for (const [deviceId, call] of userCalls.entries()) { for (const [deviceId, call] of userCalls.entries()) {
if (!this.callsByCallId.has(call.callId)) { if (!this.callsByCallId.has(call.callId)) {
@@ -150,7 +153,7 @@ export class OTelGroupCallMembership {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan( const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
`matrix.call`, `matrix.call`,
undefined, undefined,
this.groupCallContext this.groupCallContext,
); );
// XXX: anonymity // XXX: anonymity
span.setAttribute("matrix.call.target.userId", userId); span.setAttribute("matrix.call.target.userId", userId);
@@ -160,7 +163,7 @@ export class OTelGroupCallMembership {
span.setAttribute("matrix.call.target.displayName", displayName); span.setAttribute("matrix.call.target.displayName", displayName);
this.callsByCallId.set( this.callsByCallId.set(
call.callId, call.callId,
new OTelCall(userId, deviceId, call, span) new OTelCall(userId, deviceId, call, span),
); );
} }
} }
@@ -179,9 +182,9 @@ export class OTelGroupCallMembership {
this.callsByCallId.delete(callTrackingInfo.call.callId); this.callsByCallId.delete(callTrackingInfo.call.callId);
} }
} }
}; }
public onCallStateChange(call: MatrixCall, newState: CallState) { public onCallStateChange(call: MatrixCall, newState: CallState): void {
const callTrackingInfo = this.callsByCallId.get(call.callId); const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) { if (!callTrackingInfo) {
logger.error(`Got call state change for unknown call ID ${call.callId}`); logger.error(`Got call state change for unknown call ID ${call.callId}`);
@@ -193,7 +196,7 @@ export class OTelGroupCallMembership {
}); });
} }
public onSendEvent(call: MatrixCall, event: VoipEvent) { public onSendEvent(call: MatrixCall, event: VoipEvent): void {
const eventType = event.eventType as string; const eventType = event.eventType as string;
if ( if (
!eventType.startsWith("m.call") && !eventType.startsWith("m.call") &&
@@ -210,17 +213,17 @@ export class OTelGroupCallMembership {
if (event.type === "toDevice") { if (event.type === "toDevice") {
callTrackingInfo.span.addEvent( callTrackingInfo.span.addEvent(
`matrix.sendToDeviceEvent_${event.eventType}`, `matrix.sendToDeviceEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event) ObjectFlattener.flattenVoipEvent(event),
); );
} else if (event.type === "sendEvent") { } else if (event.type === "sendEvent") {
callTrackingInfo.span.addEvent( callTrackingInfo.span.addEvent(
`matrix.sendToRoomEvent_${event.eventType}`, `matrix.sendToRoomEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event) ObjectFlattener.flattenVoipEvent(event),
); );
} }
} }
public onReceivedVoipEvent(event: MatrixEvent) { public onReceivedVoipEvent(event: MatrixEvent): void {
// These come straight from CallEventHandler so don't have // These come straight from CallEventHandler so don't have
// a call already associated (in principle we could receive // a call already associated (in principle we could receive
// events for calls we don't know about). // events for calls we don't know about).
@@ -239,7 +242,7 @@ export class OTelGroupCallMembership {
"matrix.receive_voip_event_unknown_callid", "matrix.receive_voip_event_unknown_callid",
{ {
"sender.userId": event.getSender(), "sender.userId": event.getSender(),
} },
); );
logger.error("Received call event for unknown call ID " + callId); logger.error("Received call event for unknown call ID " + callId);
return; return;
@@ -251,37 +254,41 @@ export class OTelGroupCallMembership {
}); });
} }
public onToggleMicrophoneMuted(newValue: boolean) { public onToggleMicrophoneMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
"matrix.microphone.muted": newValue, "matrix.microphone.muted": newValue,
}); });
} }
public onSetMicrophoneMuted(setMuted: boolean) { public onSetMicrophoneMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setMicMuted", { this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
"matrix.microphone.muted": setMuted, "matrix.microphone.muted": setMuted,
}); });
} }
public onToggleLocalVideoMuted(newValue: boolean) { public onToggleLocalVideoMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
"matrix.video.muted": newValue, "matrix.video.muted": newValue,
}); });
} }
public onSetLocalVideoMuted(setMuted: boolean) { public onSetLocalVideoMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", { this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.video.muted": setMuted, "matrix.video.muted": setMuted,
}); });
} }
public onToggleScreensharing(newValue: boolean) { public onToggleScreensharing(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", { this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.screensharing.enabled": newValue, "matrix.screensharing.enabled": newValue,
}); });
} }
public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) { public onSpeaking(
member: RoomMember,
deviceId: string,
speaking: boolean,
): void {
if (speaking) { if (speaking) {
// Ensure that there's an audio activity span for this speaker // Ensure that there's an audio activity span for this speaker
let deviceMap = this.speakingSpans.get(member); let deviceMap = this.speakingSpans.get(member);
@@ -294,7 +301,7 @@ export class OTelGroupCallMembership {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan( const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.audioActivity", "matrix.audioActivity",
undefined, undefined,
this.groupCallContext this.groupCallContext,
); );
span.setAttribute("matrix.userId", member.userId); span.setAttribute("matrix.userId", member.userId);
span.setAttribute("matrix.displayName", member.rawDisplayName); span.setAttribute("matrix.displayName", member.rawDisplayName);
@@ -311,7 +318,7 @@ export class OTelGroupCallMembership {
} }
} }
public onCallError(error: CallError, call: MatrixCall) { public onCallError(error: CallError, call: MatrixCall): void {
const callTrackingInfo = this.callsByCallId.get(call.callId); const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) { if (!callTrackingInfo) {
logger.error(`Got error for unknown call ID ${call.callId}`); logger.error(`Got error for unknown call ID ${call.callId}`);
@@ -321,17 +328,19 @@ export class OTelGroupCallMembership {
callTrackingInfo.span.recordException(error); callTrackingInfo.span.recordException(error);
} }
public onGroupCallError(error: GroupCallError) { public onGroupCallError(error: GroupCallError): void {
this.callMembershipSpan?.recordException(error); this.callMembershipSpan?.recordException(error);
} }
public onUndecryptableToDevice(event: MatrixEvent) { public onUndecryptableToDevice(event: MatrixEvent): void {
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
"sender.userId": event.getSender(), "sender.userId": event.getSender(),
}); });
} }
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) { public onCallFeedStatsReport(
report: GroupCallStatsReport<CallFeedReport>,
): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined; let call: OTelCall | undefined;
const callId = report.report?.callId; const callId = report.report?.callId;
@@ -348,10 +357,10 @@ export class OTelGroupCallMembership {
"call.opponentMemberId": report.report?.opponentMemberId "call.opponentMemberId": report.report?.opponentMemberId
? report.report?.opponentMemberId ? report.report?.opponentMemberId
: "unknown", : "unknown",
} },
); );
logger.error( logger.error(
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}` `Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`,
); );
return; return;
} else { } else {
@@ -361,26 +370,26 @@ export class OTelGroupCallMembership {
} }
public onConnectionStatsReport( public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport> statsReport: GroupCallStatsReport<ConnectionStatsReport>,
) { ): void {
this.buildCallStatsSpan( this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport, OTelStatsReportType.ConnectionReport,
statsReport.report statsReport.report,
); );
} }
public onByteSentStatsReport( public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport> statsReport: GroupCallStatsReport<ByteSentStatsReport>,
) { ): void {
this.buildCallStatsSpan( this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport, OTelStatsReportType.ByteSentReport,
statsReport.report statsReport.report,
); );
} }
public buildCallStatsSpan( public buildCallStatsSpan(
type: OTelStatsReportType, type: OTelStatsReportType,
report: ByteSentStatsReport | ConnectionStatsReport report: ByteSentStatsReport | ConnectionStatsReport,
): void { ): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined; let call: OTelCall | undefined;
@@ -403,7 +412,7 @@ export class OTelGroupCallMembership {
const data = ObjectFlattener.flattenReportObject(type, report); const data = ObjectFlattener.flattenReportObject(type, report);
const ctx = opentelemetry.trace.setSpan( const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(), opentelemetry.context.active(),
call.span call.span,
); );
const options = { const options = {
@@ -417,21 +426,21 @@ export class OTelGroupCallMembership {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan( const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
type, type,
options, options,
ctx ctx,
); );
span.setAttribute("matrix.callId", callId ?? "unknown"); span.setAttribute("matrix.callId", callId ?? "unknown");
span.setAttribute( span.setAttribute(
"matrix.opponentMemberId", "matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown" report.opponentMemberId ? report.opponentMemberId : "unknown",
); );
span.addEvent("matrix.call.connection_stats_event", data); span.addEvent("matrix.call.connection_stats_event", data);
span.end(); span.end();
} }
public onSummaryStatsReport( public onSummaryStatsReport(
statsReport: GroupCallStatsReport<SummaryStatsReport> statsReport: GroupCallStatsReport<SummaryStatsReport>,
) { ): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.SummaryReport; const type = OTelStatsReportType.SummaryReport;
@@ -439,12 +448,12 @@ export class OTelGroupCallMembership {
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
const ctx = setSpan( const ctx = setSpan(
opentelemetry.context.active(), opentelemetry.context.active(),
this.callMembershipSpan this.callMembershipSpan,
); );
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan( const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
"matrix.groupCallMembership.summaryReport", "matrix.groupCallMembership.summaryReport",
undefined, undefined,
ctx ctx,
); );
if (span === undefined) { if (span === undefined) {
return; return;
@@ -453,7 +462,7 @@ export class OTelGroupCallMembership {
span.setAttribute("matrix.userId", this.myUserId); span.setAttribute("matrix.userId", this.myUserId);
span.setAttribute( span.setAttribute(
"matrix.displayName", "matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name" this.myMember ? this.myMember.name : "unknown-name",
); );
span.addEvent(type, data); span.addEvent(type, data);
span.end(); span.end();

View File

@@ -25,7 +25,7 @@ import {
export class ObjectFlattener { export class ObjectFlattener {
public static flattenReportObject( public static flattenReportObject(
prefix: string, prefix: string,
report: ConnectionStatsReport | ByteSentStatsReport report: ConnectionStatsReport | ByteSentStatsReport,
): Attributes { ): Attributes {
const flatObject = {}; const flatObject = {};
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0); ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
@@ -33,27 +33,27 @@ export class ObjectFlattener {
} }
public static flattenByteSentStatsReportObject( public static flattenByteSentStatsReportObject(
statsReport: GroupCallStatsReport<ByteSentStatsReport> statsReport: GroupCallStatsReport<ByteSentStatsReport>,
): Attributes { ): Attributes {
const flatObject = {}; const flatObject = {};
ObjectFlattener.flattenObjectRecursive( ObjectFlattener.flattenObjectRecursive(
statsReport.report, statsReport.report,
flatObject, flatObject,
"matrix.stats.bytesSent.", "matrix.stats.bytesSent.",
0 0,
); );
return flatObject; return flatObject;
} }
static flattenSummaryStatsReportObject( public static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport> statsReport: GroupCallStatsReport<SummaryStatsReport>,
) { ): Attributes {
const flatObject = {}; const flatObject = {};
ObjectFlattener.flattenObjectRecursive( ObjectFlattener.flattenObjectRecursive(
statsReport.report, statsReport.report,
flatObject, flatObject,
"matrix.stats.summary.", "matrix.stats.summary.",
0 0,
); );
return flatObject; return flatObject;
} }
@@ -67,7 +67,7 @@ export class ObjectFlattener {
event as unknown as Record<string, unknown>, // XXX Types event as unknown as Record<string, unknown>, // XXX Types
flatObject, flatObject,
"matrix.event.", "matrix.event.",
0 0,
); );
return flatObject; return flatObject;
@@ -77,12 +77,12 @@ export class ObjectFlattener {
obj: Object, obj: Object,
flatObject: Attributes, flatObject: Attributes,
prefix: string, prefix: string,
depth: number depth: number,
): void { ): void {
if (depth > 10) if (depth > 10)
throw new Error( throw new Error(
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
prefix prefix,
); );
let entries; let entries;
if (obj instanceof Map) { if (obj instanceof Map) {
@@ -101,7 +101,7 @@ export class ObjectFlattener {
v, v,
flatObject, flatObject,
prefix + k + ".", prefix + k + ".",
depth + 1 depth + 1,
); );
} }
} }

View File

@@ -36,7 +36,7 @@ export class ElementCallOpenTelemetry {
private otlpExporter?: OTLPTraceExporter; private otlpExporter?: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor; public readonly rageshakeProcessor?: RageshakeSpanProcessor;
static globalInit(): void { public static globalInit(): void {
const config = Config.get(); const config = Config.get();
// we always enable opentelemetry in general. We only enable the OTLP // we always enable opentelemetry in general. We only enable the OTLP
// collector if a URL is defined (and in future if another setting is defined) // collector if a URL is defined (and in future if another setting is defined)
@@ -50,18 +50,18 @@ export class ElementCallOpenTelemetry {
sharedInstance = new ElementCallOpenTelemetry( sharedInstance = new ElementCallOpenTelemetry(
config.opentelemetry?.collector_url, config.opentelemetry?.collector_url,
config.rageshake?.submit_url config.rageshake?.submit_url,
); );
} }
} }
static get instance(): ElementCallOpenTelemetry { public static get instance(): ElementCallOpenTelemetry {
return sharedInstance; return sharedInstance;
} }
constructor( private constructor(
collectorUrl: string | undefined, collectorUrl: string | undefined,
rageshakeUrl: string | undefined rageshakeUrl: string | undefined,
) { ) {
// This is how we can make Jaeger show a reasonable service in the dropdown on the left. // This is how we can make Jaeger show a reasonable service in the dropdown on the left.
const providerConfig = { const providerConfig = {
@@ -77,7 +77,7 @@ export class ElementCallOpenTelemetry {
url: collectorUrl, url: collectorUrl,
}); });
this._provider.addSpanProcessor( this._provider.addSpanProcessor(
new SimpleSpanProcessor(this.otlpExporter) new SimpleSpanProcessor(this.otlpExporter),
); );
} else { } else {
logger.info("OTLP collector disabled"); logger.info("OTLP collector disabled");
@@ -93,7 +93,7 @@ export class ElementCallOpenTelemetry {
this._tracer = opentelemetry.trace.getTracer( this._tracer = opentelemetry.trace.getTracer(
// This is not the serviceName shown in jaeger // This is not the serviceName shown in jaeger
"my-element-call-otl-tracer" "my-element-call-otl-tracer",
); );
} }

View File

@@ -40,7 +40,7 @@ export const Popover = forwardRef<HTMLDivElement, Props>(
shouldCloseOnBlur: true, shouldCloseOnBlur: true,
isDismissable: true, isDismissable: true,
}, },
popoverRef popoverRef,
); );
return ( return (
@@ -56,5 +56,5 @@ export const Popover = forwardRef<HTMLDivElement, Props>(
</div> </div>
</FocusScope> </FocusScope>
); );
} },
); );

View File

@@ -43,7 +43,7 @@ export const PopoverMenuTrigger = forwardRef<
const { menuTriggerProps, menuProps } = useMenuTrigger( const { menuTriggerProps, menuProps } = useMenuTrigger(
{}, {},
popoverMenuState, popoverMenuState,
buttonRef buttonRef,
); );
const popoverRef = useRef(null); const popoverRef = useRef(null);
@@ -62,7 +62,7 @@ export const PopoverMenuTrigger = forwardRef<
typeof children[1] !== "function" typeof children[1] !== "function"
) { ) {
throw new Error( throw new Error(
"PopoverMenu must have two props. The first being a button and the second being a render prop." "PopoverMenu must have two props. The first being a button and the second being a render prop.",
); );
} }

View File

@@ -39,7 +39,11 @@ type ProfileSaveCallback = ({
removeAvatar: boolean; removeAvatar: boolean;
}) => Promise<void>; }) => Promise<void>;
export function useProfile(client: MatrixClient | undefined) { interface UseProfile extends ProfileLoadState {
saveProfile: ProfileSaveCallback;
}
export function useProfile(client: MatrixClient | undefined): UseProfile {
const [{ success, loading, displayName, avatarUrl, error }, setState] = const [{ success, loading, displayName, avatarUrl, error }, setState] =
useState<ProfileLoadState>(() => { useState<ProfileLoadState>(() => {
let user: User | undefined = undefined; let user: User | undefined = undefined;
@@ -59,8 +63,8 @@ export function useProfile(client: MatrixClient | undefined) {
useEffect(() => { useEffect(() => {
const onChangeUser = ( const onChangeUser = (
_event: MatrixEvent | undefined, _event: MatrixEvent | undefined,
{ displayName, avatarUrl }: User { displayName, avatarUrl }: User,
) => { ): void => {
setState({ setState({
success: false, success: false,
loading: false, loading: false,
@@ -104,9 +108,8 @@ export function useProfile(client: MatrixClient | undefined) {
if (removeAvatar) { if (removeAvatar) {
await client.setAvatarUrl(""); await client.setAvatarUrl("");
} else if (avatar) { } else if (avatar) {
({ content_uri: mxcAvatarUrl } = await client.uploadContent( ({ content_uri: mxcAvatarUrl } =
avatar await client.uploadContent(avatar));
));
await client.setAvatarUrl(mxcAvatarUrl); await client.setAvatarUrl(mxcAvatarUrl);
} }
@@ -131,7 +134,7 @@ export function useProfile(client: MatrixClient | undefined) {
logger.error("Client not initialized before calling saveProfile"); logger.error("Client not initialized before calling saveProfile");
} }
}, },
[client] [client],
); );
return { return {

View File

@@ -40,14 +40,14 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
e.stopPropagation(); e.stopPropagation();
setOpen(false); setOpen(false);
}, },
[setOpen] [setOpen],
); );
const roomSharedKey = useRoomSharedKey(roomId ?? ""); const roomSharedKey = useRoomSharedKey(roomId ?? "");
const roomIsEncrypted = useIsRoomE2EE(roomId ?? ""); const roomIsEncrypted = useIsRoomE2EE(roomId ?? "");
if (roomIsEncrypted && roomSharedKey === undefined) { if (roomIsEncrypted && roomSharedKey === undefined) {
logger.error( logger.error(
"Generating app redirect URL for encrypted room but don't have key available!" "Generating app redirect URL for encrypted room but don't have key available!",
); );
} }
@@ -60,7 +60,7 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
const url = new URL( const url = new URL(
roomId === null roomId === null
? window.location.href ? window.location.href
: getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined) : getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined),
); );
// Edit the URL to prevent the app selection prompt from appearing a second // Edit the URL to prevent the app selection prompt from appearing a second
// time within the app, and to keep the user confined to the current room // time within the app, and to keep the user confined to the current room

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { FC, FormEventHandler, useCallback, useState } from "react"; import { FC, FormEventHandler, ReactNode, useCallback, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@@ -64,7 +64,7 @@ export const CallEndedView: FC<Props> = ({
PosthogAnalytics.instance.eventQualitySurvey.track( PosthogAnalytics.instance.eventQualitySurvey.track(
endedCallId, endedCallId,
feedbackText, feedbackText,
starRating starRating,
); );
setSubmitting(true); setSubmitting(true);
@@ -83,7 +83,7 @@ export const CallEndedView: FC<Props> = ({
}, 1000); }, 1000);
}, 1000); }, 1000);
}, },
[endedCallId, history, isPasswordlessUser, confineToRoom, starRating] [endedCallId, history, isPasswordlessUser, confineToRoom, starRating],
); );
const createAccountDialog = isPasswordlessUser && ( const createAccountDialog = isPasswordlessUser && (
@@ -148,7 +148,7 @@ export const CallEndedView: FC<Props> = ({
</div> </div>
); );
const renderBody = () => { const renderBody = (): ReactNode => {
if (leaveError) { if (leaveError) {
return ( return (
<> <>

View File

@@ -47,7 +47,7 @@ export function GroupCallLoader({
ev.preventDefault(); ev.preventDefault();
history.push("/"); history.push("/");
}, },
[history] [history],
); );
switch (groupCallState.kind) { switch (groupCallState.kind) {
@@ -66,7 +66,7 @@ export function GroupCallLoader({
<Heading>{t("Call not found")}</Heading> <Heading>{t("Call not found")}</Heading>
<Text> <Text>
{t( {t(
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key." "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.",
)} )}
</Text> </Text>
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have {/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room, isE2EESupported } from "livekit-client"; import { Room, isE2EESupported } from "livekit-client";
@@ -39,11 +39,7 @@ import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers"; import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState"; import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
import { import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
useManageRoomSharedKey,
useIsRoomE2EE,
} from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar"; import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName"; import { useRoomName } from "./useRoomName";
import { useJoinRule } from "./useJoinRule"; import { useJoinRule } from "./useJoinRule";
@@ -64,18 +60,18 @@ interface Props {
rtcSession: MatrixRTCSession; rtcSession: MatrixRTCSession;
} }
export function GroupCallView({ export const GroupCallView: FC<Props> = ({
client, client,
isPasswordlessUser, isPasswordlessUser,
confineToRoom, confineToRoom,
preload, preload,
hideHeader, hideHeader,
rtcSession, rtcSession,
}: Props) { }) => {
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession); const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const e2eeSharedKey = useManageRoomSharedKey(rtcSession.room.roomId); const e2eeSharedKey = useRoomSharedKey(rtcSession.room.roomId);
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId); const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
useEffect(() => { useEffect(() => {
@@ -113,8 +109,8 @@ export function GroupCallView({
// Count each member only once, regardless of how many devices they use // Count each member only once, regardless of how many devices they use
const participantCount = useMemo( const participantCount = useMemo(
() => new Set<string>(memberships.map((m) => m.member.userId)).size, () => new Set<string>(memberships.map((m) => m.sender!)).size,
[memberships] [memberships],
); );
const deviceContext = useMediaDevices(); const deviceContext = useMediaDevices();
@@ -128,7 +124,9 @@ export function GroupCallView({
useEffect(() => { useEffect(() => {
if (widget && preload) { if (widget && preload) {
// In preload mode, wait for a join action before entering // In preload mode, wait for a join action before entering
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => { const onJoin = async (
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
// XXX: I think this is broken currently - LiveKit *won't* request // XXX: I think this is broken currently - LiveKit *won't* request
// permissions and give you device names unless you specify a kind, but // permissions and give you device names unless you specify a kind, but
// here we want all kinds of devices. This needs a fix in livekit-client // here we want all kinds of devices. This needs a fix in livekit-client
@@ -144,14 +142,14 @@ export function GroupCallView({
const deviceId = await findDeviceByName( const deviceId = await findDeviceByName(
audioInput, audioInput,
"audioinput", "audioinput",
devices devices,
); );
if (!deviceId) { if (!deviceId) {
logger.warn("Unknown audio input: " + audioInput); logger.warn("Unknown audio input: " + audioInput);
latestMuteStates.current!.audio.setEnabled?.(false); latestMuteStates.current!.audio.setEnabled?.(false);
} else { } else {
logger.debug( logger.debug(
`Found audio input ID ${deviceId} for name ${audioInput}` `Found audio input ID ${deviceId} for name ${audioInput}`,
); );
latestDevices.current!.audioInput.select(deviceId); latestDevices.current!.audioInput.select(deviceId);
latestMuteStates.current!.audio.setEnabled?.(true); latestMuteStates.current!.audio.setEnabled?.(true);
@@ -164,14 +162,14 @@ export function GroupCallView({
const deviceId = await findDeviceByName( const deviceId = await findDeviceByName(
videoInput, videoInput,
"videoinput", "videoinput",
devices devices,
); );
if (!deviceId) { if (!deviceId) {
logger.warn("Unknown video input: " + videoInput); logger.warn("Unknown video input: " + videoInput);
latestMuteStates.current!.video.setEnabled?.(false); latestMuteStates.current!.video.setEnabled?.(false);
} else { } else {
logger.debug( logger.debug(
`Found video input ID ${deviceId} for name ${videoInput}` `Found video input ID ${deviceId} for name ${videoInput}`,
); );
latestDevices.current!.videoInput.select(deviceId); latestDevices.current!.videoInput.select(deviceId);
latestMuteStates.current!.video.setEnabled?.(true); latestMuteStates.current!.video.setEnabled?.(true);
@@ -183,7 +181,7 @@ export function GroupCallView({
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
// we only have room sessions right now, so call ID is the emprty string - we use the room ID // we only have room sessions right now, so call ID is the emprty string - we use the room ID
PosthogAnalytics.instance.eventCallStarted.track( PosthogAnalytics.instance.eventCallStarted.track(
rtcSession.room.roomId rtcSession.room.roomId,
); );
await Promise.all([ await Promise.all([
@@ -214,16 +212,19 @@ export function GroupCallView({
PosthogAnalytics.instance.eventCallEnded.track( PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId, rtcSession.room.roomId,
rtcSession.memberships.length, rtcSession.memberships.length,
sendInstantly sendInstantly,
); );
leaveRTCSession(rtcSession); await leaveRTCSession(rtcSession);
if (widget) { if (widget) {
// we need to wait until the callEnded event is tracked on posthog. // we need to wait until the callEnded event is tracked on posthog.
// Otherwise the iFrame gets killed before the callEnded event got tracked. // Otherwise the iFrame gets killed before the callEnded event got tracked.
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
widget.api.setAlwaysOnScreen(false); widget.api.setAlwaysOnScreen(false);
PosthogAnalytics.instance.logout(); PosthogAnalytics.instance.logout();
// we will always send the hangup event after the memberships have been updated
// calling leaveRTCSession.
widget.api.transport.send(ElementWidgetActions.HangupCall, {}); widget.api.transport.send(ElementWidgetActions.HangupCall, {});
} }
@@ -235,14 +236,16 @@ export function GroupCallView({
history.push("/"); history.push("/");
} }
}, },
[rtcSession, isPasswordlessUser, confineToRoom, history] [rtcSession, isPasswordlessUser, confineToRoom, history],
); );
useEffect(() => { useEffect(() => {
if (widget && isJoined) { if (widget && isJoined) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => { const onHangup = async (
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
leaveRTCSession(rtcSession); leaveRTCSession(rtcSession);
await widget!.api.transport.reply(ev.detail, {}); widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false); widget!.api.setAlwaysOnScreen(false);
}; };
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup); widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
@@ -252,11 +255,9 @@ export function GroupCallView({
} }
}, [isJoined, rtcSession]); }, [isJoined, rtcSession]);
const [e2eeEnabled] = useEnableE2EE();
const e2eeConfig = useMemo( const e2eeConfig = useMemo(
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined), () => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
[e2eeSharedKey] [e2eeSharedKey],
); );
const onReconnect = useCallback(() => { const onReconnect = useCallback(() => {
@@ -270,12 +271,12 @@ export function GroupCallView({
const [shareModalOpen, setInviteModalOpen] = useState(false); const [shareModalOpen, setInviteModalOpen] = useState(false);
const onDismissInviteModal = useCallback( const onDismissInviteModal = useCallback(
() => setInviteModalOpen(false), () => setInviteModalOpen(false),
[setInviteModalOpen] [setInviteModalOpen],
); );
const onShareClickFn = useCallback( const onShareClickFn = useCallback(
() => setInviteModalOpen(true), () => setInviteModalOpen(true),
[setInviteModalOpen] [setInviteModalOpen],
); );
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null; const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
@@ -284,17 +285,17 @@ export function GroupCallView({
ev.preventDefault(); ev.preventDefault();
history.push("/"); history.push("/");
}, },
[history] [history],
); );
const { t } = useTranslation(); const { t } = useTranslation();
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) { if (isRoomE2EE && !e2eeSharedKey) {
return ( return (
<ErrorView <ErrorView
error={ error={
new Error( new Error(
"No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button." "No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button.",
) )
} }
/> />
@@ -305,7 +306,7 @@ export function GroupCallView({
<Heading>Incompatible Browser</Heading> <Heading>Incompatible Browser</Heading>
<Text> <Text>
{t( {t(
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117" "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117",
)} )}
</Text> </Text>
<Link href="/" onClick={onHomeClick}> <Link href="/" onClick={onHomeClick}>
@@ -313,8 +314,6 @@ export function GroupCallView({
</Link> </Link>
</FullScreenView> </FullScreenView>
); );
} else if (!e2eeEnabled && isRoomE2EE) {
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
} }
const shareModal = ( const shareModal = (
@@ -381,7 +380,7 @@ export function GroupCallView({
client={client} client={client}
matrixInfo={matrixInfo} matrixInfo={matrixInfo}
muteStates={muteStates} muteStates={muteStates}
onEnter={() => enterRTCSession(rtcSession)} onEnter={(): void => enterRTCSession(rtcSession)}
confineToRoom={confineToRoom} confineToRoom={confineToRoom}
hideHeader={hideHeader} hideHeader={hideHeader}
participantCount={participantCount} participantCount={participantCount}
@@ -390,4 +389,4 @@ export function GroupCallView({
</> </>
); );
} }
} };

View File

@@ -27,7 +27,16 @@ import { ConnectionState, Room, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room"; import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; import {
FC,
ReactNode,
Ref,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -78,9 +87,6 @@ import {
import { useOpenIDSFU } from "../livekit/openIDSFU"; import { useOpenIDSFU } from "../livekit/openIDSFU";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
// or with getUsermedia and getDisplaymedia being used within the same session.
// For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// How long we wait after a focus switch before showing the real participant list again // How long we wait after a focus switch before showing the real participant list again
@@ -91,12 +97,12 @@ export interface ActiveCallProps
e2eeConfig?: E2EEConfig; e2eeConfig?: E2EEConfig;
} }
export function ActiveCall(props: ActiveCallProps) { export const ActiveCall: FC<ActiveCallProps> = (props) => {
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession); const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
const { livekitRoom, connState } = useLiveKit( const { livekitRoom, connState } = useLiveKit(
props.muteStates, props.muteStates,
sfuConfig, sfuConfig,
props.e2eeConfig props.e2eeConfig,
); );
if (!livekitRoom) { if (!livekitRoom) {
@@ -112,7 +118,7 @@ export function ActiveCall(props: ActiveCallProps) {
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} /> <InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
</RoomContext.Provider> </RoomContext.Provider>
); );
} };
export interface InCallViewProps { export interface InCallViewProps {
client: MatrixClient; client: MatrixClient;
@@ -128,7 +134,7 @@ export interface InCallViewProps {
onShareClick: (() => void) | null; onShareClick: (() => void) | null;
} }
export function InCallView({ export const InCallView: FC<InCallViewProps> = ({
client, client,
matrixInfo, matrixInfo,
rtcSession, rtcSession,
@@ -140,7 +146,7 @@ export function InCallView({
otelGroupCallMembership, otelGroupCallMembership,
connState, connState,
onShareClick, onShareClick,
}: InCallViewProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
usePreventScroll(); usePreventScroll();
useWakeLock(); useWakeLock();
@@ -163,10 +169,10 @@ export function InCallView({
[{ source: Track.Source.ScreenShare, withPlaceholder: false }], [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
{ {
room: livekitRoom, room: livekitRoom,
} },
); );
const { layout, setLayout } = useVideoGridLayout( const { layout, setLayout } = useVideoGridLayout(
screenSharingTracks.length > 0 screenSharingTracks.length > 0,
); );
const [showConnectionStats] = useShowConnectionStats(); const [showConnectionStats] = useShowConnectionStats();
@@ -179,11 +185,11 @@ export function InCallView({
const toggleMicrophone = useCallback( const toggleMicrophone = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e), () => muteStates.audio.setEnabled?.((e) => !e),
[muteStates] [muteStates],
); );
const toggleCamera = useCallback( const toggleCamera = useCallback(
() => muteStates.video.setEnabled?.((e) => !e), () => muteStates.video.setEnabled?.((e) => !e),
[muteStates] [muteStates],
); );
// This function incorrectly assumes that there is a camera and microphone, which is not always the case. // This function incorrectly assumes that there is a camera and microphone, which is not always the case.
@@ -192,7 +198,7 @@ export function InCallView({
containerRef1, containerRef1,
toggleMicrophone, toggleMicrophone,
toggleCamera, toggleCamera,
(muted) => muteStates.audio.setEnabled?.(!muted) (muted) => muteStates.audio.setEnabled?.(!muted),
); );
const onLeavePress = useCallback(() => { const onLeavePress = useCallback(() => {
@@ -204,32 +210,32 @@ export function InCallView({
layout === "grid" layout === "grid"
? ElementWidgetActions.TileLayout ? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout, : ElementWidgetActions.SpotlightLayout,
{} {},
); );
}, [layout]); }, [layout]);
useEffect(() => { useEffect(() => {
if (widget) { if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => { const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setLayout("grid"); setLayout("grid");
await widget!.api.transport.reply(ev.detail, {}); widget!.api.transport.reply(ev.detail, {});
}; };
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => { const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setLayout("spotlight"); setLayout("spotlight");
await widget!.api.transport.reply(ev.detail, {}); widget!.api.transport.reply(ev.detail, {});
}; };
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout); widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.on( widget.lazyActions.on(
ElementWidgetActions.SpotlightLayout, ElementWidgetActions.SpotlightLayout,
onSpotlightLayout onSpotlightLayout,
); );
return () => { return () => {
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout); widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget!.lazyActions.off( widget!.lazyActions.off(
ElementWidgetActions.SpotlightLayout, ElementWidgetActions.SpotlightLayout,
onSpotlightLayout onSpotlightLayout,
); );
}; };
} }
@@ -252,7 +258,7 @@ export function InCallView({
(noControls (noControls
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null ? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
: null), : null),
[fullscreenItem, noControls, items] [fullscreenItem, noControls, items],
); );
const Grid = const Grid =
@@ -295,7 +301,7 @@ export function InCallView({
disableAnimations={prefersReducedMotion || isSafari} disableAnimations={prefersReducedMotion || isSafari}
layoutStates={layoutStates} layoutStates={layoutStates}
> >
{(props) => ( {(props): ReactNode => (
<VideoTile <VideoTile
maximised={false} maximised={false}
fullscreen={false} fullscreen={false}
@@ -311,18 +317,18 @@ export function InCallView({
}; };
const rageshakeRequestModalProps = useRageshakeRequestModal( const rageshakeRequestModalProps = useRageshakeRequestModal(
rtcSession.room.roomId rtcSession.room.roomId,
); );
const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback( const openSettings = useCallback(
() => setSettingsModalOpen(true), () => setSettingsModalOpen(true),
[setSettingsModalOpen] [setSettingsModalOpen],
); );
const closeSettings = useCallback( const closeSettings = useCallback(
() => setSettingsModalOpen(false), () => setSettingsModalOpen(false),
[setSettingsModalOpen] [setSettingsModalOpen],
); );
const toggleScreensharing = useCallback(async () => { const toggleScreensharing = useCallback(async () => {
@@ -356,25 +362,29 @@ export function InCallView({
onPress={toggleCamera} onPress={toggleCamera}
disabled={muteStates.video.setEnabled === null} disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute" data-testid="incall_videomute"
/> />,
); );
if (!reducedControls) { if (!reducedControls) {
if (canScreenshare && !hideScreensharing && !isSafari) { if (canScreenshare && !hideScreensharing) {
buttons.push( buttons.push(
<ScreenshareButton <ScreenshareButton
key="3" key="3"
enabled={isScreenShareEnabled} enabled={isScreenShareEnabled}
onPress={toggleScreensharing} onPress={toggleScreensharing}
data-testid="incall_screenshare" data-testid="incall_screenshare"
/> />,
); );
} }
buttons.push(<SettingsButton key="4" onPress={openSettings} />); buttons.push(<SettingsButton key="4" onPress={openSettings} />);
} }
buttons.push( buttons.push(
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" /> <HangupButton
key="6"
onPress={onLeavePress}
data-testid="incall_leave"
/>,
); );
footer = ( footer = (
<div className={styles.footer}> <div className={styles.footer}>
@@ -434,11 +444,11 @@ export function InCallView({
/> />
</div> </div>
); );
} };
function findMatrixMember( function findMatrixMember(
room: MatrixRoom, room: MatrixRoom,
id: string id: string,
): RoomMember | undefined { ): RoomMember | undefined {
if (!id) return undefined; if (!id) return undefined;
@@ -446,7 +456,7 @@ function findMatrixMember(
// must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon // must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon
if (parts.length < 3) { if (parts.length < 3) {
logger.warn( logger.warn(
"Livekit participants ID doesn't look like a userId:deviceId combination" "Livekit participants ID doesn't look like a userId:deviceId combination",
); );
return undefined; return undefined;
} }
@@ -460,7 +470,7 @@ function findMatrixMember(
function useParticipantTiles( function useParticipantTiles(
livekitRoom: Room, livekitRoom: Room,
matrixRoom: MatrixRoom, matrixRoom: MatrixRoom,
connState: ECConnectionState connState: ECConnectionState,
): TileDescriptor<ItemData>[] { ): TileDescriptor<ItemData>[] {
const previousTiles = useRef<TileDescriptor<ItemData>[]>([]); const previousTiles = useRef<TileDescriptor<ItemData>[]>([]);
@@ -489,7 +499,7 @@ function useParticipantTiles(
// connected, this is fine and we'll be in "all ghosts" mode. // connected, this is fine and we'll be in "all ghosts" mode.
if (id !== "" && member === undefined) { if (id !== "" && member === undefined) {
logger.warn( logger.warn(
`Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!` `Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!`,
); );
} }
allGhosts &&= member === undefined; allGhosts &&= member === undefined;
@@ -533,11 +543,11 @@ function useParticipantTiles(
return screenShareTile return screenShareTile
? [userMediaTile, screenShareTile] ? [userMediaTile, screenShareTile]
: [userMediaTile]; : [userMediaTile];
} },
); );
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged( PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
tiles.length tiles.length,
); );
// If every item is a ghost, that probably means we're still connecting and // If every item is a ghost, that probably means we're still connecting and

View File

@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
const url = useMemo( const url = useMemo(
() => () =>
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined), getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
[room, roomSharedKey] [room, roomSharedKey],
); );
const [, setCopied] = useClipboard(url); const [, setCopied] = useClipboard(url);
const [toastOpen, setToastOpen] = useState(false); const [toastOpen, setToastOpen] = useState(false);
@@ -53,7 +53,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
onDismiss(); onDismiss();
setToastOpen(true); setToastOpen(true);
}, },
[setCopied, onDismiss] [setCopied, onDismiss],
); );
return ( return (

View File

@@ -36,7 +36,7 @@ export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
const onChange = useCallback( const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout), (e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
[setLayout] [setLayout],
); );
const spotlightId = useId(); const spotlightId = useId();

View File

@@ -63,22 +63,22 @@ export const LobbyView: FC<Props> = ({
const onAudioPress = useCallback( const onAudioPress = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e), () => muteStates.audio.setEnabled?.((e) => !e),
[muteStates] [muteStates],
); );
const onVideoPress = useCallback( const onVideoPress = useCallback(
() => muteStates.video.setEnabled?.((e) => !e), () => muteStates.video.setEnabled?.((e) => !e),
[muteStates] [muteStates],
); );
const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback( const openSettings = useCallback(
() => setSettingsModalOpen(true), () => setSettingsModalOpen(true),
[setSettingsModalOpen] [setSettingsModalOpen],
); );
const closeSettings = useCallback( const closeSettings = useCallback(
() => setSettingsModalOpen(false), () => setSettingsModalOpen(false),
[setSettingsModalOpen] [setSettingsModalOpen],
); );
const history = useHistory(); const history = useHistory();

View File

@@ -49,18 +49,18 @@ export interface MuteStates {
function useMuteState( function useMuteState(
device: MediaDevice, device: MediaDevice,
enabledByDefault: () => boolean enabledByDefault: () => boolean,
): MuteState { ): MuteState {
const [enabled, setEnabled] = useReactiveState<boolean>( const [enabled, setEnabled] = useReactiveState<boolean>(
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()), (prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
[device] [device],
); );
return useMemo( return useMemo(
() => () =>
device.available.length === 0 device.available.length === 0
? deviceUnavailable ? deviceUnavailable
: { enabled, setEnabled }, : { enabled, setEnabled },
[device, enabled, setEnabled] [device, enabled, setEnabled],
); );
} }
@@ -69,7 +69,7 @@ export function useMuteStates(participantCount: number): MuteStates {
const audio = useMuteState( const audio = useMuteState(
devices.audioInput, devices.audioInput,
() => participantCount <= MUTE_PARTICIPANT_COUNT () => participantCount <= MUTE_PARTICIPANT_COUNT,
); );
const video = useMuteState(devices.videoInput, () => true); const video = useMuteState(devices.videoInput, () => true);

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal, ModalProps } from "../Modal"; import { Modal, Props as ModalProps } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input"; import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake"; import { useSubmitRageshake } from "../settings/submit-rageshake";
@@ -47,13 +47,13 @@ export const RageshakeRequestModal: FC<Props> = ({
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}> <Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
<Body> <Body>
{t( {t(
"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.",
)} )}
</Body> </Body>
<FieldRow> <FieldRow>
<Button <Button
onPress={() => onPress={(): void =>
submitRageshake({ void submitRageshake({
sendLogs: true, sendLogs: true,
rageshakeRequestId, rageshakeRequestId,
roomId, roomId,

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useState } from "react"; import { FC, useCallback, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -29,7 +29,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
export function RoomAuthView() { export const RoomAuthView: FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
@@ -52,7 +52,7 @@ export function RoomAuthView() {
setError(error); setError(error);
}); });
}, },
[registerPasswordlessUser] [registerPasswordlessUser],
); );
const { t } = useTranslation(); const { t } = useTranslation();
@@ -122,4 +122,4 @@ export function RoomAuthView() {
</div> </div>
</> </>
); );
} };

View File

@@ -81,7 +81,7 @@ export const RoomPage: FC = () => {
hideHeader={hideHeader} hideHeader={hideHeader}
/> />
), ),
[client, passwordlessUser, confineToRoom, preload, hideHeader] [client, passwordlessUser, confineToRoom, preload, hideHeader],
); );
let content: ReactNode; let content: ReactNode;

View File

@@ -82,14 +82,14 @@ export const VideoPreview: FC<Props> = ({
}, },
(error) => { (error) => {
logger.error("Error while creating preview Tracks:", error); logger.error("Error while creating preview Tracks:", error);
} },
); );
const videoTrack = useMemo( const videoTrack = useMemo(
() => () =>
tracks?.find((t) => t.kind === Track.Kind.Video) as tracks?.find((t) => t.kind === Track.Kind.Video) as
| LocalVideoTrack | LocalVideoTrack
| undefined, | undefined,
[tracks] [tracks],
); );
const videoEl = useRef<HTMLVideoElement | null>(null); const videoEl = useRef<HTMLVideoElement | null>(null);

View File

@@ -20,14 +20,24 @@ import {
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { deepCompare } from "matrix-js-sdk/src/utils"; import { deepCompare } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { LivekitFocus } from "../livekit/LivekitFocus"; import { LivekitFocus } from "../livekit/LivekitFocus";
function getActiveFocus( function getActiveFocus(
rtcSession: MatrixRTCSession rtcSession: MatrixRTCSession,
): LivekitFocus | undefined { ): LivekitFocus | undefined {
const oldestMembership = rtcSession.getOldestMembership(); const oldestMembership = rtcSession.getOldestMembership();
return oldestMembership?.getActiveFoci()[0] as LivekitFocus; const focus = oldestMembership?.getActiveFoci()[0] as LivekitFocus;
if (focus) {
logger.info(
`Got active focus for call from ${oldestMembership?.sender}/${oldestMembership?.deviceId}`,
focus,
);
}
return focus;
} }
/** /**
@@ -36,10 +46,10 @@ function getActiveFocus(
* and the same focus. * and the same focus.
*/ */
export function useActiveFocus( export function useActiveFocus(
rtcSession: MatrixRTCSession rtcSession: MatrixRTCSession,
): LivekitFocus | undefined { ): LivekitFocus | undefined {
const [activeFocus, setActiveFocus] = useState(() => const [activeFocus, setActiveFocus] = useState(() =>
getActiveFocus(rtcSession) getActiveFocus(rtcSession),
); );
const onMembershipsChanged = useCallback(() => { const onMembershipsChanged = useCallback(() => {
@@ -53,13 +63,13 @@ export function useActiveFocus(
useEffect(() => { useEffect(() => {
rtcSession.on( rtcSession.on(
MatrixRTCSessionEvent.MembershipsChanged, MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged onMembershipsChanged,
); );
return () => { return () => {
rtcSession.off( rtcSession.off(
MatrixRTCSessionEvent.MembershipsChanged, MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged onMembershipsChanged,
); );
}; };
}); });

View File

@@ -22,11 +22,11 @@ import { TileDescriptor } from "../video-grid/VideoGrid";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
import { useEventTarget } from "../useEvents"; import { useEventTarget } from "../useEvents";
const isFullscreen = () => const isFullscreen = (): boolean =>
Boolean(document.fullscreenElement) || Boolean(document.fullscreenElement) ||
Boolean(document.webkitFullscreenElement); Boolean(document.webkitFullscreenElement);
function enterFullscreen() { function enterFullscreen(): void {
if (document.body.requestFullscreen) { if (document.body.requestFullscreen) {
document.body.requestFullscreen(); document.body.requestFullscreen();
} else if (document.body.webkitRequestFullscreen) { } else if (document.body.webkitRequestFullscreen) {
@@ -36,7 +36,7 @@ function enterFullscreen() {
} }
} }
function exitFullscreen() { function exitFullscreen(): void {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
} else if (document.webkitExitFullscreen) { } else if (document.webkitExitFullscreen) {
@@ -46,7 +46,7 @@ function exitFullscreen() {
} }
} }
function useFullscreenChange(onFullscreenChange: () => void) { function useFullscreenChange(onFullscreenChange: () => void): void {
useEventTarget(document.body, "fullscreenchange", onFullscreenChange); useEventTarget(document.body, "fullscreenchange", onFullscreenChange);
useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange); useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange);
} }
@@ -66,7 +66,7 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
prevItem == null prevItem == null
? null ? null
: items.find((i) => i.id === prevItem.id) ?? null, : items.find((i) => i.id === prevItem.id) ?? null,
[items] [items],
); );
const latestItems = useRef<TileDescriptor<T>[]>(items); const latestItems = useRef<TileDescriptor<T>[]>(items);
@@ -80,15 +80,15 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
setFullscreenItem( setFullscreenItem(
latestFullscreenItem.current === null latestFullscreenItem.current === null
? latestItems.current.find((i) => i.id === itemId) ?? null ? latestItems.current.find((i) => i.id === itemId) ?? null
: null : null,
); );
}, },
[setFullscreenItem] [setFullscreenItem],
); );
const exitFullscreenCallback = useCallback( const exitFullscreenCallback = useCallback(
() => setFullscreenItem(null), () => setFullscreenItem(null),
[setFullscreenItem] [setFullscreenItem],
); );
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -103,7 +103,7 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
useFullscreenChange( useFullscreenChange(
useCallback(() => { useCallback(() => {
if (!isFullscreen()) setFullscreenItem(null); if (!isFullscreen()) setFullscreenItem(null);
}, [setFullscreenItem]) }, [setFullscreenItem]),
); );
return { return {

View File

@@ -15,12 +15,14 @@ limitations under the License.
*/ */
import { useCallback } from "react"; import { useCallback } from "react";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import type { Room } from "matrix-js-sdk/src/models/room"; import type { Room } from "matrix-js-sdk/src/models/room";
import { useRoomState } from "./useRoomState"; import { useRoomState } from "./useRoomState";
export const useJoinRule = (room: Room) => export function useJoinRule(room: Room): JoinRule {
useRoomState( return useRoomState(
room, room,
useCallback((state) => state.getJoinRule(), []) useCallback((state) => state.getJoinRule(), []),
); );
}

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