Compare commits

..

143 Commits

Author SHA1 Message Date
Timo
0a009a7b1c disable turn servers in widget mode (#1396)
Signed-off-by: Timo K <toger5@hotmail.de>
2023-09-12 13:51:06 +02:00
Robin
d483dd0218 Merge pull request #1427 from robintown/button-accessibility
Improve accessibility of buttons
2023-09-11 11:24:53 -04:00
Robin
2d1e820fc6 Regenerate strings 2023-09-11 11:22:35 -04:00
Robin
26ad532532 Merge branch 'livekit' into button-accessibility 2023-09-11 11:22:10 -04:00
Robin
3438ee5711 Merge pull request #1426 from robintown/header-footer
Implement new in-call header and footer designs
2023-09-11 11:14:59 -04:00
Robin
4b3364584d Regenerate string files 2023-09-11 11:11:26 -04:00
Robin
ae2b152b3e Upgrade Compound Web 2023-09-11 11:08:46 -04:00
Robin
6c25a25f8b Make the focus ring on Compound Web buttons consistent 2023-09-11 11:07:10 -04:00
Robin
8720ccada6 Count participants in a more sensible way 2023-09-11 10:32:03 -04:00
Robin
a9585b1fcf Merge branch 'livekit' into header-footer 2023-09-11 10:29:19 -04:00
Šimon Brandner
013e3296fe Merge pull request #1387 from vector-im/SimonBrandner/task/avatar 2023-09-10 11:52:07 +02:00
Šimon Brandner
7fc0de573b Remove leftover
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-10 11:50:09 +02:00
Robin
1445c42cc3 Implement new in-call header and footer designs 2023-09-08 16:41:23 -04:00
Robin
3545a8152a Improve accessibility of buttons
I noticed that none of these buttons had accessible labels, which is obviously no good since they rely on icons alone to convey purpose when not focused.
2023-09-08 15:39:49 -04:00
David Baker
dfdb2b564a Merge pull request #1421 from vector-im/dbkr/fixptt
Fix the mute icon being incorrect when using PTT
2023-09-07 13:20:42 +01:00
David Baker
38bb2895db Tidier syntax
Co-authored-by: Robin <robin@robin.town>
2023-09-07 13:16:40 +01:00
David Baker
00f14c9814 Fix the mute icon being incorrect when using PTT
We were manipulating the participant's mute state directly for some
reason, just for setting the mute state directly, which bypased the
mutestates hook.
2023-09-06 11:45:01 +01:00
David Baker
0a7723d218 Merge pull request #1418 from vector-im/dbkr/dont_open_devices_twice
Remove extra device request on the video preview page
2023-09-06 09:01:24 +01:00
Šimon Brandner
40e31607d2 i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-06 08:04:55 +02:00
Šimon Brandner
1cb0ad2f65 Switch to Avatar from Compound
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-06 08:04:51 +02:00
David Baker
3e72604391 Remove unintentional change 2023-09-05 17:38:43 +01:00
David Baker
973d396249 Remove extra device request on the video preview page
As per comment, livekit mutates the object that's passed in, so
we ended up re-requesting the devices in the next render because we
effectively passed in different options.
2023-09-05 17:34:55 +01:00
Šimon Brandner
40283ab760 Remove unused Facepile
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 20:03:25 +02:00
Robin
3331a23373 Merge pull request #1409 from robintown/livekit-isnt-pr
Tell the CI that the livekit branch isn't a PR
2023-09-04 05:08:06 -04:00
Robin Townsend
1bdb0db6d2 Tell the CI that the livekit branch isn't a PR
This prevents it from trying to run the PR preview workflow on every build of the livekit branch.
2023-09-01 15:08:46 -04:00
Robin
e124b77f7f Merge pull request #1374 from robintown/in-call-footer
Implement new designs for in-call footer buttons
2023-09-01 13:26:19 -04:00
Robin
b96b77472d Merge pull request #1406 from vector-im/renovate/react-stately-collections-3.x-lockfile
Update dependency @react-stately/collections to v3.10.0
2023-09-01 13:25:57 -04:00
Robin Townsend
0bf96472eb Work around jsdom not providing TextEncoder 2023-09-01 13:23:24 -04:00
Robin Townsend
8e090af370 Merge branch 'livekit' into in-call-footer 2023-09-01 13:13:02 -04:00
David Baker
ba23c1e269 Merge pull request #1407 from vector-im/dbkr/add_git_sha_version
Add git sha as version in build job
2023-09-01 15:04:06 +01:00
David Baker
f6a296bc0c Add git sha as version in build job 2023-09-01 14:59:52 +01:00
renovate[bot]
1e892b452e Update dependency @react-stately/collections to v3.10.0 2023-09-01 13:23:17 +00:00
Robin
a3edf08776 Merge pull request #1371 from vector-im/renovate/react-spring-web-9.x-lockfile
Update dependency @react-spring/web to v9.7.3
2023-09-01 09:22:44 -04:00
Robin Townsend
27b5ede72e Install @react-spring/rafz
As a workaround for https://github.com/pmndrs/react-spring/issues/2173
2023-09-01 09:19:44 -04:00
Robin
29af360df9 Merge pull request #1372 from vector-im/renovate/storybook-monorepo
Update dependency @storybook/react to v6.5.16
2023-09-01 09:12:27 -04:00
Robin
dfe8f4ab2f Merge pull request #1395 from vector-im/renovate/opentelemetry-instrumentation-user-interaction-0.x
Update dependency @opentelemetry/instrumentation-user-interaction to ^0.33.0
2023-08-31 17:10:00 -04:00
renovate[bot]
b1ae9523aa Update dependency @opentelemetry/instrumentation-user-interaction to ^0.33.0 2023-08-31 21:07:27 +00:00
renovate[bot]
3e64553815 Update dependency @storybook/react to v6.5.16 2023-08-31 21:07:12 +00:00
Robin
b1b30f3d97 Merge pull request #1394 from vector-im/renovate/opentelemetry-instrumentation-document-load-0.x
Update dependency @opentelemetry/instrumentation-document-load to ^0.33.0
2023-08-31 17:06:38 -04:00
renovate[bot]
900abc7fa2 Update dependency @opentelemetry/instrumentation-document-load to ^0.33.0 2023-08-31 21:03:50 +00:00
Robin
186fe91529 Merge pull request #1390 from vector-im/renovate/opentelemetry-js-monorepo
Update opentelemetry-js monorepo
2023-08-31 17:02:46 -04:00
Robin
2fc4681f0b Merge pull request #1392 from vector-im/renovate/babel-monorepo
Update dependency @babel/core to v7.22.11
2023-08-31 17:02:02 -04:00
Robin
784f2ee99c Merge pull request #1393 from vector-im/renovate/juggle-resize-observer-3.x-lockfile
Update dependency @juggle/resize-observer to v3.4.0
2023-08-31 17:01:33 -04:00
Robin
4c0feaa30d Merge pull request #1391 from vector-im/renovate/actions-github-script-3.x
Update actions/github-script action to v3.2.0
2023-08-31 17:01:14 -04:00
renovate[bot]
58a335e8a2 Update dependency @juggle/resize-observer to v3.4.0 2023-08-31 19:12:55 +00:00
renovate[bot]
771e20bdc4 Update dependency @babel/core to v7.22.11 2023-08-31 19:12:39 +00:00
renovate[bot]
7549e24479 Update actions/github-script action to v3.2.0 2023-08-31 16:06:48 +00:00
renovate[bot]
550d7acaa8 Update opentelemetry-js monorepo 2023-08-31 16:06:42 +00:00
Robin
1789571921 Merge pull request #1378 from vector-im/renovate/vitejs-plugin-react-4.x-lockfile
Update dependency @vitejs/plugin-react to v4.0.4
2023-08-31 09:54:40 -04:00
renovate[bot]
405904ded1 Update dependency @vitejs/plugin-react to v4.0.4 2023-08-31 13:41:16 +00:00
Robin
0e0f6f7fc5 Merge pull request #1361 from vector-im/renovate/npm-mermaid-vulnerability
Update dependency mermaid to v9 [SECURITY]
2023-08-31 09:36:03 -04:00
Robin
7f5e02852c Merge pull request #1380 from vector-im/renovate/classnames-2.x-lockfile
Update dependency classnames to v2.3.2
2023-08-31 09:33:22 -04:00
Robin
65564f8662 Add types for dompurify and d3 2023-08-31 09:31:12 -04:00
Robin
d55af2b9a5 Merge pull request #1389 from vector-im/renovate/nwtgck-actions-netlify-1.x
Update nwtgck/actions-netlify action to v1.2.4
2023-08-31 09:29:20 -04:00
Robin
b04e53a9bd Merge pull request #1385 from vector-im/renovate/re-resizable-6.x-lockfile
Update dependency re-resizable to v6.9.11
2023-08-31 09:28:22 -04:00
Robin
2d5ea82ecf Fix types 2023-08-31 09:27:06 -04:00
Robin
8c09a4361a Merge pull request #1388 from vector-im/renovate/react-use-clipboard-1.x-lockfile
Update dependency react-use-clipboard to v1.0.9
2023-08-31 09:26:28 -04:00
Robin
24b652f696 Merge pull request #1384 from vector-im/renovate/i18next-http-backend-1.x-lockfile
Update dependency i18next-http-backend to v1.4.5
2023-08-31 09:24:10 -04:00
Robin
6d24473fb0 Merge pull request #1386 from vector-im/renovate/react-router-monorepo
Update dependency react-router-dom to v5.3.4
2023-08-31 09:22:09 -04:00
renovate[bot]
5d24b55d30 Update nwtgck/actions-netlify action to v1.2.4 2023-08-31 13:21:35 +00:00
Robin
a73597b30b Merge pull request #1376 from vector-im/renovate/use-gesture-react-10.x-lockfile
Update dependency @use-gesture/react to v10.2.27
2023-08-31 09:21:12 -04:00
renovate[bot]
8cf936eb5d Update dependency react-use-clipboard to v1.0.9 2023-08-31 13:20:33 +00:00
Robin
e490b39718 Merge pull request #1381 from vector-im/renovate/color-hash-2.x-lockfile
Update dependency color-hash to v2.0.2
2023-08-31 09:19:53 -04:00
renovate[bot]
3d9d19b721 Update dependency react-router-dom to v5.3.4 2023-08-31 12:11:47 +00:00
Šimon Brandner
41b28b80fc Merge pull request #1373 from vector-im/renovate/sdp-transform-2.x-lockfile 2023-08-31 14:11:14 +02:00
renovate[bot]
bc258c05d7 Update dependency re-resizable to v6.9.11 2023-08-31 11:56:26 +00:00
renovate[bot]
1305779e8e Update dependency i18next-http-backend to v1.4.5 2023-08-31 11:56:12 +00:00
Robin
a35f1cb23f Merge pull request #1369 from robintown/copy-icon-color
Use a more suitable color for copy buttons
2023-08-31 07:55:35 -04:00
Robin
3494f0fb4d Merge branch 'livekit' into in-call-footer 2023-08-31 07:53:14 -04:00
renovate[bot]
18b87bcd55 Update dependency color-hash to v2.0.2 2023-08-31 10:19:50 +00:00
renovate[bot]
eef286e71e Update dependency classnames to v2.3.2 2023-08-31 10:19:36 +00:00
David Baker
1a559b2e0d Merge pull request #1377 from vector-im/renovate/vector-im-compound-design-tokens-0.x
Update dependency @vector-im/compound-design-tokens to ^0.0.5
2023-08-31 11:19:02 +01:00
David Baker
35a69a9c51 Merge pull request #1370 from vector-im/renovate/docker-setup-buildx-action-digest
Update docker/setup-buildx-action digest to 885d146
2023-08-31 10:38:00 +01:00
David Baker
e389798baf Merge pull request #1365 from vector-im/renovate/docker-build-push-action-digest
Update docker/build-push-action digest to 9311bf5
2023-08-31 10:35:57 +01:00
David Baker
e9fab62e21 Merge pull request #1367 from vector-im/renovate/docker-login-action-digest
Update docker/login-action digest to cf8514a
2023-08-31 10:35:27 +01:00
David Baker
8a02c5fbb9 Merge pull request #1368 from vector-im/renovate/docker-metadata-action-digest
Update docker/metadata-action digest to 0f8c876
2023-08-31 10:34:50 +01:00
renovate[bot]
96857476f9 Update dependency @vector-im/compound-design-tokens to ^0.0.5 2023-08-31 09:34:16 +00:00
David Baker
8b596fe8ab Merge pull request #1364 from vector-im/renovate/actions-upload-artifact-digest
Update actions/upload-artifact digest to 65d8626
2023-08-31 10:33:45 +01:00
Šimon Brandner
c7c6e2c007 Merge pull request #1358 from vector-im/SimonBrandner/feat/e2ee-on 2023-08-31 11:32:27 +02:00
renovate[bot]
1a135f9466 Update dependency @use-gesture/react to v10.2.27 2023-08-31 08:59:46 +00:00
Šimon Brandner
39f76b65d4 Merge pull request #1375 from vector-im/renovate/uuid-9.x-lockfile 2023-08-31 10:59:10 +02:00
renovate[bot]
dedd0b07c6 Update dependency @types/uuid to v9.0.3 2023-08-31 04:34:44 +00:00
Robin Townsend
63ccd56573 Implement new designs for in-call footer buttons
This basically amounts to swapping out some CSS, the icons, and the Tooltip component.
2023-08-30 22:04:20 -04:00
renovate[bot]
3958158cf8 Update dependency @types/sdp-transform to v2.4.6 2023-08-31 00:39:36 +00:00
renovate[bot]
2e866770f6 Update dependency @react-spring/web to v9.7.3 2023-08-30 22:21:40 +00:00
renovate[bot]
6f4c549961 Update docker/setup-buildx-action digest to 885d146 2023-08-30 22:21:23 +00:00
Robin Townsend
6f4834e168 Use a more suitable color for copy buttons
This is a change that I meant to include in aa628cc63c, but forgot to commit.
2023-08-30 14:52:53 -04:00
renovate[bot]
ea3c346d39 Update docker/metadata-action digest to 0f8c876 2023-08-30 18:48:45 +00:00
renovate[bot]
6004e2d303 Update docker/login-action digest to cf8514a 2023-08-30 18:48:41 +00:00
David Baker
8a94220d74 Merge pull request #1366 from vector-im/dbkr/fix_firefox_e2ee_hopefully
Fix e2ee audio in Firefox
2023-08-30 17:24:34 +01:00
renovate[bot]
6c999e652b Update docker/build-push-action digest to 9311bf5 2023-08-30 16:17:46 +00:00
David Baker
3a0c895b7a Fix e2ee audio in Firefox
Pull in changes for livekit and disable RED as per comment.
2023-08-30 17:17:45 +01:00
renovate[bot]
3062e015c9 Update actions/upload-artifact digest to 65d8626 2023-08-30 16:17:42 +00:00
David Baker
922af1ba3e Merge pull request #1360 from vector-im/renovate/npm-posthog-js-vulnerability
Update dependency posthog-js to v1.57.2 [SECURITY]
2023-08-30 17:12:07 +01:00
renovate[bot]
11e78cf06b Update dependency mermaid to v9 [SECURITY] 2023-08-30 14:01:06 +00:00
renovate[bot]
fcf0ec9b90 Update dependency posthog-js to v1.57.2 [SECURITY] 2023-08-30 14:00:46 +00:00
David Baker
641fdbcc10 Merge pull request #1357 from vector-im/renovate/configure
Configure Renovate
2023-08-30 14:59:53 +01:00
David Baker
3aace3e4f6 Prettier 2023-08-30 14:42:58 +01:00
Robin
841a31a642 Merge pull request #1354 from robintown/colors
Adopt the Compound color system
2023-08-30 08:14:53 -04:00
Šimon Brandner
1b2287646d Enable E2EE by default
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 12:19:21 +02:00
fkwp
8ea086a971 disable renoavte for packages we want to monitor ourselves 2023-08-30 11:09:15 +02:00
David Baker
91b3cc2abb Merge pull request #1356 from vector-im/dbkr/unpin_livekit
Unpin livekit versions
2023-08-30 10:04:19 +01:00
renovate[bot]
d71264a1f2 Add renovate.json 2023-08-30 08:52:59 +00:00
David Baker
73b505987d Unpin livekit versions
and upgrade to latest
2023-08-30 09:50:12 +01:00
Robin Townsend
aa628cc63c Adopt the Compound color system
As a first step towards adopting the Compound design system and the new Element Call designs, this pulls in Compound's color tokens and applies them to all existing components. I've tried to choose tokens based on the semantics of where they're used, but in some cases, where the new and old design systems differ in semantics, it was necessary to choose tokens based on their resulting color. These hacks can be removed as we implement more of the new designs.

There were a set of environment variables that we used for custom themes, but Compound has way too many design tokens for that approach to still be a good idea, so I decided to replace them all with a single environment variable that just lets you write arbitrary custom CSS.
2023-08-28 17:14:40 -04:00
fkwp
1dc05bed7d Merge pull request #1353 from vector-im/1352-use-the-dev-hs-for-netlify-previews
1352 use the dev hs for netlify previews
2023-08-28 16:54:41 +02:00
fkwp
1ef64f3a4d use call-unstable.ems.host homeserver 2023-08-28 16:49:03 +02:00
fkwp
0315230e3a Update homeserver to use call-unstable.ems.host 2023-08-28 16:46:55 +02:00
Šimon Brandner
e635740760 Merge pull request #1351 from vector-im/SimonBrandner/fix/e2ee-embed 2023-08-25 15:38:43 +02:00
Šimon Brandner
9d142c1ea5 Fix useIsRoomE2EE()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-25 15:30:58 +02:00
Šimon Brandner
a73516380a Merge pull request #1344 from vector-im/SimonBrandner/feat/roomIds 2023-08-16 18:13:47 +02:00
Šimon Brandner
d0fe055af6 Always allow setting a password
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-16 15:39:55 +02:00
Šimon Brandner
3ea908c59f Revert "Remove optional call"
This reverts commit 74c70627dc.
2023-08-16 15:38:23 +02:00
Šimon Brandner
74c70627dc Remove optional call
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-16 15:35:49 +02:00
Šimon Brandner
b4b33563be Fix joining an existing room
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-16 15:13:28 +02:00
Šimon Brandner
11b5f537de i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-16 14:01:36 +02:00
Šimon Brandner
5cd8c0538e Revert changes to JoinExistingCallModal
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-16 13:59:41 +02:00
Šimon Brandner
54157611eb Remove log line
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-16 13:56:31 +02:00
Šimon Brandner
22690c4a0b Differentiate between E2EE and non-E2EE rooms by alias presence
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-11 17:00:05 +02:00
Šimon Brandner
edfae0138c Add useIsRoomE2EE()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-11 16:59:26 +02:00
Šimon Brandner
e246c053c1 Improve loading of passwords
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-11 16:57:55 +02:00
Šimon Brandner
c86ef40162 Goodbye, aliases
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-11 13:25:09 +02:00
Šimon Brandner
b2f5f90f37 More URL handling adjustements
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-11 13:16:35 +02:00
Šimon Brandner
506b363f38 Update E2EE texts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-11 13:14:50 +02:00
David Baker
4d394decce Merge pull request #1343 from vector-im/dbkr/add_v_back
Put the 'v' back on the docker tag for release versions
2023-08-11 10:54:10 +01:00
David Baker
96f264db87 Put the 'v' back on the docker tag for release versions 2023-08-11 09:24:11 +01:00
David Baker
e51492b3c7 Merge pull request #1342 from vector-im/dbkr/dev_version_sha
Set version to commit sha for dev builds
2023-08-11 09:18:14 +01:00
Šimon Brandner
5c3d8efebf Improve URL handling
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-10 15:19:43 +02:00
Šimon Brandner
8facf7808b Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/hash 2023-08-10 09:43:05 +02:00
Šimon Brandner
923043a58c Fix tests
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:08:23 +02:00
Šimon Brandner
6a3c02f7f7 Delint
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:37 +02:00
Šimon Brandner
819734e2dd i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:37 +02:00
Šimon Brandner
466d7ee394 Don't allow joining existing calls with E2EE enabled
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:37 +02:00
Šimon Brandner
935d2188f0 Do not allow joining a call without E2EE key
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:36 +02:00
Šimon Brandner
c4e5e1afb1 Save room shared keys to local storage
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:36 +02:00
Šimon Brandner
80f97cba32 Fix getRoomUrl()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:36 +02:00
Šimon Brandner
7ace81a7cc Make copy link button include a password
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:35 +02:00
Šimon Brandner
89ae0e1e62 Re-use variable
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:35 +02:00
Šimon Brandner
fd8f50957b i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:34 +02:00
Šimon Brandner
9cd20ed1da Delete hash once loaded
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:34 +02:00
Šimon Brandner
8fdc1e3684 Store shared keys in local storage
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:34 +02:00
Šimon Brandner
59653fea07 Update the URL the lobby view when the password changes
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:33 +02:00
Šimon Brandner
1a5898eb30 Get E2EE password from the URL
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-09 14:01:33 +02:00
100 changed files with 4361 additions and 3491 deletions

View File

@@ -10,19 +10,10 @@ LIVEKIT_SECRET="secret"
# Used for determining the homeserver to use for short urls etc.
# VITE_FALLBACK_STUN_ALLOWED=false
# VITE_CUSTOM_THEME=true
# VITE_THEME_ACCENT=#0dbd8b
# VITE_THEME_ACCENT_20=#0dbd8b33
# VITE_THEME_ALERT=#ff5b55
# VITE_THEME_ALERT_20=#ff5b5533
# VITE_THEME_LINKS=#0086e6
# VITE_THEME_PRIMARY_CONTENT=#ffffff
# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
# VITE_THEME_TERTIARY_CONTENT=#8e99a4
# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433
# VITE_THEME_QUATERNARY_CONTENT=#6f7882
# VITE_THEME_QUINARY_CONTENT=#394049
# VITE_THEME_SYSTEM=#21262c
# VITE_THEME_BACKGROUND=#15191e
# VITE_THEME_BACKGROUND_85=#15191ed9
# VITE_THEME_SUBTLE_PRIMARY=#26282D
# CSS to be injected into the page for the purpose of custom theming.
# Generally, writing a custom theme involves overriding Compound design tokens,
# which are documented here:
# https://compound.element.io/?path=/docs/foundations-design-tokens--docs
# https://compound.element.io/?path=/docs/tokens-color-palettes--docs
# https://compound.element.io/?path=/docs/tokens-semantic-colors--docs
# VITE_CUSTOM_CSS=".cpd-theme-dark.cpd-theme-dark { --cpd-color-theme-bg: #101317; }"

View File

@@ -23,6 +23,7 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
VITE_APP_VERSION: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=4096"
- name: Upload Artifact
uses: actions/upload-artifact@v2

View File

@@ -28,7 +28,7 @@ jobs:
ref: ${{ github.event.workflow_run.head_sha }}
- name: "Download artifact"
uses: actions/github-script@v3.1.0
uses: actions/github-script@v3.2.0
with:
script: |
const artifacts = await github.actions.listWorkflowRunArtifacts({
@@ -60,7 +60,7 @@ jobs:
- name: Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@v1.2.3
uses: nwtgck/actions-netlify@v1.2.4
with:
publish-dir: dist
deploy-message: "Deploy from GitHub Actions"

View File

@@ -28,7 +28,7 @@ jobs:
ref: ${{ github.event.workflow_run.head_sha }}
- name: "Download artifact"
uses: actions/github-script@v3.1.0
uses: actions/github-script@v3.2.0
with:
script: |
const artifacts = await github.actions.listWorkflowRunArtifacts({
@@ -60,7 +60,7 @@ jobs:
- name: Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@v1.2.3
uses: nwtgck/actions-netlify@v1.2.4
with:
publish-dir: dist
deploy-message: "Deploy from GitHub Actions"

View File

@@ -6,6 +6,7 @@ on:
- completed
branches-ignore:
- "main"
- "livekit"
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v2
- name: Log in to container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
uses: docker/login-action@cf8514a65188af1d4f94f8c28a7a4153af1088ce
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -54,7 +54,7 @@ jobs:
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
- name: Upload
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce
uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -62,20 +62,20 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175
uses: docker/metadata-action@0f8c876bafbf5dbce05c36682ec68e9a0274a48a
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=short,event=branch
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
type=raw,value=latest-ci,enable={{is_default_branch}}
type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
uses: docker/build-push-action@9311bf5263ae5b36f3ec67aff768790c6e2344ad
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -1,8 +1,8 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://call.ems.host",
"server_name": "call.ems.host"
"base_url": "https://call-unstable.ems.host",
"server_name": "call-unstable.ems.host"
}
},
"livekit": {

View File

@@ -20,14 +20,14 @@
},
"dependencies": {
"@juggle/resize-observer": "^3.3.1",
"@livekit/components-react": "1.1.0",
"@livekit/components-react": "^1.1.0",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/context-zone": "^1.9.1",
"@opentelemetry/exporter-jaeger": "^1.9.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.35.1",
"@opentelemetry/instrumentation-document-load": "^0.31.1",
"@opentelemetry/instrumentation-user-interaction": "^0.32.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.41.0",
"@opentelemetry/instrumentation-document-load": "^0.33.0",
"@opentelemetry/instrumentation-user-interaction": "^0.33.0",
"@opentelemetry/sdk-trace-web": "^1.9.1",
"@react-aria/button": "^3.3.4",
"@react-aria/dialog": "^3.1.4",
@@ -47,6 +47,8 @@
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^0.0.5",
"@vector-im/compound-web": "^0.4.0",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1",
"classnames": "^2.3.1",
@@ -55,11 +57,11 @@
"i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"livekit-client": "1.12.3",
"livekit-client": "^1.12.3",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"mermaid": "^9.0.0",
"normalize.css": "^8.0.1",
"pako": "^2.0.4",
"postcss-preset-env": "^7",
@@ -79,12 +81,15 @@
},
"devDependencies": {
"@babel/core": "^7.16.5",
"@react-spring/rafz": "^9.7.3",
"@sentry/vite-plugin": "^0.3.0",
"@storybook/react": "^6.5.0-alpha.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/content-type": "^1.1.5",
"@types/d3": "^7.4.0",
"@types/dom-screen-wake-lock": "^1.0.1",
"@types/dompurify": "^3.0.2",
"@types/grecaptcha": "^3.0.4",
"@types/node": "^18.13.0",
"@types/react-router-dom": "^5.3.3",
@@ -117,7 +122,7 @@
"vite-plugin-svgr": "^3.2.0"
},
"jest": {
"testEnvironment": "jsdom",
"testEnvironment": "./test/environment.ts",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
],

View File

@@ -13,7 +13,7 @@
</script>
</head>
<body>
<body class="cpd-theme-dark">
<div id="root"></div>
</body>
</html>

View File

@@ -1,9 +1,11 @@
{
"{{count, number}}|one": "{{count, number}}",
"{{count, number}}|other": "{{count, number}}",
"{{count}} stars|one": "{{count}} star",
"{{count}} stars|other": "{{count}} stars",
"{{displayName}} is presenting": "{{displayName}} is presenting",
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
@@ -22,7 +24,6 @@
"Call link copied": "Call link copied",
"Call type menu": "Call type menu",
"Camera": "Camera",
"Change layout": "Change layout",
"Close": "Close",
"Confirm password": "Confirm password",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
@@ -32,7 +33,6 @@
"Create account": "Create account",
"Debug log": "Debug log",
"Debug log request": "Debug log request",
"Details": "Details",
"Developer": "Developer",
"Developer Settings": "Developer Settings",
"Display name": "Display name",
@@ -40,42 +40,40 @@
"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",
"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",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback",
"Fetching group call timed out.": "Fetching group call timed out.",
"Freedom": "Freedom",
"Full screen": "Full screen",
"Go": "Go",
"Grid layout menu": "Grid layout menu",
"Grid": "Grid",
"Home": "Home",
"How did it go?": "How did it go?",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
"Include debug logs": "Include debug logs",
"Incompatible versions": "Incompatible versions",
"Incompatible versions!": "Incompatible versions!",
"Inspector": "Inspector",
"Invite": "Invite",
"Invite people": "Invite people",
"Join call": "Join call",
"Join call now": "Join call now",
"Join existing call?": "Join existing call?",
"Leave": "Leave",
"Loading…": "Loading…",
"Local volume": "Local volume",
"Logging in…": "Logging in…",
"Login": "Login",
"Login to your account": "Login to your account",
"Microphone": "Microphone",
"Microphone off": "Microphone off",
"Microphone on": "Microphone on",
"More": "More",
"Mute microphone": "Mute microphone",
"No": "No",
"Not encrypted": "Not encrypted",
"Not now, return to home screen": "Not now, return to home screen",
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
"Password": "Password",
"Password (if none, E2EE is disabled)": "Password (if none, E2EE is disabled)",
"Passwords must match": "Passwords must match",
"Profile": "Profile",
"Recaptcha dismissed": "Recaptcha dismissed",
@@ -91,14 +89,16 @@
"Sending debug logs…": "Sending debug logs…",
"Sending…": "Sending…",
"Settings": "Settings",
"Share": "Share",
"Share screen": "Share screen",
"Share this call": "Share this call",
"Sharing screen": "Sharing screen",
"Show call inspector": "Show call inspector",
"Show connection stats": "Show connection stats",
"Sign in": "Sign in",
"Sign out": "Sign out",
"Speaker": "Speaker",
"Spotlight": "Spotlight",
"Stop sharing screen": "Stop sharing screen",
"Submit": "Submit",
"Submit feedback": "Submit feedback",
"Submitting…": "Submitting…",
@@ -107,15 +107,14 @@
"Thanks!": "Thanks!",
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
"Turn off camera": "Turn off camera",
"Turn on camera": "Turn on camera",
"Unmute microphone": "Unmute microphone",
"User menu": "User menu",
"Username": "Username",
"Version: {{version}}": "Version: {{version}}",
"Video": "Video",
"Video call": "Video call",
"Video call name": "Video call name",
"Video off": "Video off",
"Video on": "Video on",
"Waiting for other participants…": "Waiting for other participants…",
"Walkie-talkie call": "Walkie-talkie call",
"Walkie-talkie call name": "Walkie-talkie call name",

11
renovate.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"packageRules": [
{
"description": "Disable renoavte for packages we want to monitor ourselves",
"matchPackagePatterns": ["matrix-js-sdk"],
"enabled": false
}
]
}

View File

@@ -1,76 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.avatar {
position: relative;
color: var(--primary-content);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
font-weight: 600;
overflow: hidden;
flex-shrink: 0;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar svg * {
fill: var(--primary-content);
}
.avatar span {
padding-top: 1px;
}
.xs {
width: 22px;
height: 22px;
border-radius: 22px;
font-size: 14px;
}
.sm {
width: 32px;
height: 32px;
border-radius: 32px;
font-size: 15px;
}
.md {
width: 36px;
height: 36px;
border-radius: 36px;
font-size: 20px;
}
.lg {
width: 42px;
height: 42px;
border-radius: 42px;
font-size: 24px;
}
.xl {
width: 90px;
height: 90px;
border-radius: 90px;
font-size: 48px;
}

View File

@@ -14,23 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useMemo, CSSProperties, HTMLAttributes, FC } from "react";
import classNames from "classnames";
import { useMemo, FC } from "react";
import { Avatar as CompoundAvatar } from "@vector-im/compound-web";
import { getAvatarUrl } from "./matrix-utils";
import { useClient } from "./ClientContext";
import styles from "./Avatar.module.css";
const backgroundColors = [
"#5C56F5",
"#03B381",
"#368BD6",
"#AC3BA8",
"#E64F7A",
"#FF812D",
"#2DC2C5",
"#74D12C",
];
export enum Size {
XS = "xs",
@@ -48,50 +36,28 @@ export const sizes = new Map([
[Size.XL, 90],
]);
function hashStringToArrIndex(str: string, arrLength: number) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum % arrLength;
}
interface Props extends HTMLAttributes<HTMLDivElement> {
bgKey?: string;
interface Props {
id: string;
name: string;
className?: string;
src?: string;
size?: Size | number;
className?: string;
style?: CSSProperties;
fallback: string;
}
export const Avatar: FC<Props> = ({
bgKey,
src,
fallback,
size = Size.MD,
className,
style = {},
...rest
id,
name,
src,
size = Size.MD,
}) => {
const { client } = useClient();
const [sizeClass, sizePx, sizeStyle] = useMemo(
const sizePx = useMemo(
() =>
Object.values(Size).includes(size as Size)
? [styles[size as string], sizes.get(size as Size), {}]
: [
null,
size as number,
{
width: size,
height: size,
borderRadius: size,
fontSize: Math.round((size as number) / 2),
},
],
? sizes.get(size as Size)
: (size as number),
[size]
);
@@ -100,28 +66,13 @@ export const Avatar: FC<Props> = ({
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
}, [client, src, sizePx]);
const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex(
bgKey || fallback || src || "",
backgroundColors.length
);
return backgroundColors[index];
}, [bgKey, src, fallback]);
/* eslint-disable jsx-a11y/alt-text */
return (
<div
className={classNames(styles.avatar, sizeClass, className)}
style={{ backgroundColor, ...sizeStyle, ...style }}
{...rest}
>
{resolvedSrc ? (
<img src={resolvedSrc} />
) : typeof fallback === "string" ? (
<span>{fallback}</span>
) : (
fallback
)}
</div>
<CompoundAvatar
className={className}
id={id}
name={name}
size={`${sizePx}px`}
src={resolvedSrc}
/>
);
};

View File

@@ -18,5 +18,5 @@ limitations under the License.
flex: 1;
border-radius: 8px;
padding: 16px;
background-color: var(--subtle-primary);
background-color: var(--cpd-color-bg-subtle-primary);
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
.banner {
position: absolute;
padding: 29px;
background-color: var(--quaternary-content);
background-color: var(--cpd-color-bg-subtle-primary);
vertical-align: middle;
font-size: var(--font-size-body);
text-align: center;

View File

@@ -19,8 +19,12 @@ import { Trans } from "react-i18next";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { useEnableE2EE } from "./settings/useSetting";
export const E2EEBanner = () => {
const [e2eeEnabled] = useEnableE2EE();
if (e2eeEnabled) return null;
return (
<Banner>
<div className={styles.e2eeBanner}>

View File

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

View File

@@ -14,27 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { HTMLAttributes, useMemo } from "react";
import classNames from "classnames";
import { HTMLAttributes } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next";
import { AvatarStack } from "@vector-im/compound-web";
import styles from "./Facepile.module.css";
import { Avatar, Size, sizes } from "./Avatar";
const overlapMap: Partial<Record<Size, number>> = {
[Size.XS]: 2,
[Size.SM]: 4,
[Size.MD]: 8,
};
import { Avatar, Size } from "./Avatar";
interface Props extends HTMLAttributes<HTMLDivElement> {
className: string;
className?: string;
client: MatrixClient;
members: RoomMember[];
max?: number;
size?: Size;
size?: Size | number;
}
export function Facepile({
@@ -47,51 +40,27 @@ export function Facepile({
}: Props) {
const { t } = useTranslation();
const _size = sizes.get(size)!;
const _overlap = overlapMap[size]!;
const title = useMemo(() => {
return members.reduce<string | null>(
(prev, curr) =>
prev === null
? curr.name
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
null
) as string;
}, [members, t]);
const displayedMembers = members.slice(0, max);
return (
<div
className={classNames(styles.facepile, styles[size], className)}
title={title}
style={{
width:
Math.min(members.length, max + 1) * (_size - _overlap) + _overlap,
}}
<AvatarStack
title={t("{{names, list(style: short;)}}", {
list: displayedMembers.map((m) => m.name),
})}
{...rest}
>
{members.slice(0, max).map((member, i) => {
{displayedMembers.map((member, i) => {
const avatarUrl = member.getMxcAvatarUrl();
return (
<Avatar
key={member.userId}
key={i}
id={member.userId}
name={member.name}
size={size}
src={avatarUrl ?? undefined}
fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
style={{ left: i * (_size - _overlap) }}
/>
);
})}
{members.length > max && (
<Avatar
key="additional"
size={size}
fallback={`+${members.length - max}`}
className={styles.avatar}
style={{ left: max * (_size - _overlap) }}
/>
)}
</div>
</AvatarStack>
);
}

View File

@@ -28,8 +28,8 @@ limitations under the License.
flex: 1;
align-items: center;
white-space: nowrap;
padding: 0 20px;
height: 64px;
padding-inline: var(--inline-content-inset);
height: 80px;
}
.headerLogo {
@@ -66,81 +66,56 @@ limitations under the License.
margin-right: 0;
}
.roomHeaderInfo {
display: grid;
column-gap: var(--cpd-space-4x);
grid-template-columns: auto auto;
grid-template-rows: 1fr auto;
}
.roomHeaderInfo[data-size="sm"] {
grid-template-areas: "avatar name" ". participants";
}
.roomHeaderInfo[data-size="lg"] {
grid-template-areas: "avatar name" "avatar participants";
}
.roomAvatar {
position: relative;
display: none;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
border-radius: 36px;
background-color: #5c56f5;
align-self: flex-start;
grid-area: avatar;
}
.roomAvatar > * {
fill: white;
stroke: white;
}
.backButton {
background: transparent;
border: none;
.nameLine {
grid-area: name;
flex-grow: 1;
display: flex;
color: var(--primary-content);
cursor: pointer;
align-items: center;
gap: var(--cpd-space-1x);
}
.backButton h3 {
.nameLine > h1 {
margin: 0;
}
.backButton > * {
margin-right: 12px;
}
.backButton > :last-child {
margin-right: 0;
}
.userName {
font-weight: 600;
margin-right: 8px;
text-overflow: ellipsis;
/* XXX I can't actually get this ellipsis overflow to trigger, because
constraint propagation in a nested flexbox layout is a massive pain */
overflow: hidden;
flex-shrink: 1;
text-overflow: ellipsis;
}
.signOutButton {
background: transparent;
border: none;
color: rgb(255, 75, 85);
cursor: pointer;
font-weight: 600;
.nameLine > svg {
flex-shrink: 0;
}
.versionMismatchWarning {
padding-left: 15px;
}
.versionMismatchWarning::before {
content: "";
display: inline-block;
position: relative;
top: 1px;
width: 16px;
height: 16px;
mask-image: url("./icons/AlertTriangleFilled.svg");
mask-repeat: no-repeat;
mask-size: contain;
background-color: var(--alert);
padding-right: 5px;
.participantsLine {
grid-area: participants;
display: flex;
align-items: center;
gap: var(--cpd-space-1-5x);
font: var(--cpd-font-body-sm-medium);
}
@media (min-width: 800px) {
.headerLogo,
.roomAvatar,
.leftNav.hideMobile,
.rightNav.hideMobile {
display: flex;
@@ -149,8 +124,4 @@ limitations under the License.
.leftNav h3 {
font-size: var(--font-size-subtitle);
}
.nav {
height: 76px;
}
}

View File

@@ -1,105 +0,0 @@
import { GridLayoutMenu } from "./room/GridLayoutMenu";
import {
Header,
HeaderLogo,
LeftNav,
RightNav,
RoomHeaderInfo,
} from "./Header";
import { UserMenu } from "./UserMenu";
export default {
title: "Header",
component: Header,
parameters: {
layout: "fullscreen",
},
};
export const HomeAnonymous = () => (
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav>
<UserMenu />
</RightNav>
</Header>
);
export const HomeNamedGuest = () => (
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav>
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
</RightNav>
</Header>
);
export const HomeLoggedIn = () => (
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav>
<UserMenu isAuthenticated displayName="Yara" />
</RightNav>
</Header>
);
export const LobbyNamedGuest = () => (
<Header>
<LeftNav>
<RoomHeaderInfo roomName="Q4Roadmap" />
</LeftNav>
<RightNav>
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
</RightNav>
</Header>
);
export const LobbyLoggedIn = () => (
<Header>
<LeftNav>
<RoomHeaderInfo roomName="Q4Roadmap" />
</LeftNav>
<RightNav>
<UserMenu isAuthenticated displayName="Yara" />
</RightNav>
</Header>
);
export const InRoomNamedGuest = () => (
<Header>
<LeftNav>
<RoomHeaderInfo roomName="Q4Roadmap" />
</LeftNav>
<RightNav>
<GridLayoutMenu layout="freedom" />
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
</RightNav>
</Header>
);
export const InRoomLoggedIn = () => (
<Header>
<LeftNav>
<RoomHeaderInfo roomName="Q4Roadmap" />
</LeftNav>
<RightNav>
<GridLayoutMenu layout="freedom" />
<UserMenu isAuthenticated displayName="Yara" />
</RightNav>
</Header>
);
export const CreateAccount = () => (
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav></RightNav>
</Header>
);

View File

@@ -15,17 +15,18 @@ limitations under the License.
*/
import classNames from "classnames";
import { HTMLAttributes, ReactNode, useCallback } from "react";
import { FC, HTMLAttributes, ReactNode } from "react";
import { Link } from "react-router-dom";
import { Room } from "matrix-js-sdk/src/models/room";
import { useTranslation } from "react-i18next";
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import { Heading } from "@vector-im/compound-web";
import styles from "./Header.module.css";
import { useModalTriggerState } from "./Modal";
import { Button } from "./button";
import { ReactComponent as Logo } from "./icons/Logo.svg";
import { Subtitle } from "./typography/Typography";
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
import { Avatar, Size } from "./Avatar";
import { Facepile } from "./Facepile";
import { EncryptionLock } from "./room/EncryptionLock";
import { useMediaQuery } from "./useMediaQuery";
interface HeaderProps extends HTMLAttributes<HTMLElement> {
children: ReactNode;
@@ -112,47 +113,52 @@ export function HeaderLogo({ className }: HeaderLogoProps) {
);
}
interface RoomHeaderInfo {
roomName: string;
interface RoomHeaderInfoProps {
id: string;
name: string;
avatarUrl: string | null;
encrypted: boolean;
participants: RoomMember[];
client: MatrixClient;
}
export function RoomHeaderInfo({ roomName }: RoomHeaderInfo) {
return (
<>
<Subtitle data-testid="roomHeader_roomName" fontWeight="semiBold">
{roomName}
</Subtitle>
</>
);
}
interface VersionMismatchWarningProps {
users: Set<string>;
room: Room;
}
export function VersionMismatchWarning({
users,
room,
}: VersionMismatchWarningProps) {
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
id,
name,
avatarUrl,
encrypted,
participants,
client,
}) => {
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const onDetailsClick = useCallback(() => {
modalState.open();
}, [modalState]);
if (users.size === 0) return null;
const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg";
return (
<span className={styles.versionMismatchWarning}>
{t("Incompatible versions!")}
<Button variant="link" onClick={onDetailsClick}>
{t("Details")}
</Button>
{modalState.isOpen && (
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
<div className={styles.roomHeaderInfo} data-size={size}>
<Avatar
className={styles.roomAvatar}
id={id}
name={name}
size={size === "sm" ? Size.SM : 56}
src={avatarUrl ?? undefined}
/>
<div className={styles.nameLine}>
<Heading
type={size === "sm" ? "body" : "heading"}
size={size === "sm" ? "lg" : "md"}
weight="semibold"
data-testid="roomHeader_roomName"
>
{name}
</Heading>
<EncryptionLock encrypted={encrypted} />
</div>
{participants.length > 0 && (
<div className={styles.participantsLine}>
<Facepile client={client} members={participants} size={20} />
{t("{{count, number}}", { count: participants.length })}
</div>
)}
</span>
</div>
);
}
};

View File

@@ -21,8 +21,8 @@ limitations under the License.
overflow-y: auto;
list-style: none;
background-color: transparent;
border: 1px solid var(--quinary-content);
background-color: var(--background);
border: 1px solid var(--cpd-color-border-interactive-secondary);
background-color: var(--cpd-color-bg-canvas-default);
border-radius: 8px;
}
@@ -31,7 +31,7 @@ limitations under the License.
align-items: center;
justify-content: space-between;
background-color: transparent;
color: var(--primary-content);
color: var(--cpd-color-text-primary);
padding: 8px 16px;
outline: none;
cursor: pointer;
@@ -44,6 +44,6 @@ limitations under the License.
}
.option.disabled {
color: var(--quaternary-content);
background-color: var(--bgColor3);
color: var(--cpd-color-text-disabled);
background-color: var(--stopgap-bgColor3);
}

View File

@@ -27,7 +27,7 @@ limitations under the License.
display: flex;
align-items: center;
padding: 0 12px;
color: var(--primary-content);
color: var(--cpd-color-text-primary);
font-size: var(--font-size-body);
text-overflow: ellipsis;
overflow: hidden;
@@ -44,7 +44,11 @@ limitations under the License.
.menuItem.focused,
.menuItem:hover {
background-color: var(--quinary-content);
background-color: var(--cpd-color-bg-action-secondary-hovered);
}
.menuItem:active {
background-color: var(--cpd-color-bg-action-secondary-pressed);
}
.menuItem.focused:first-child,
@@ -65,5 +69,5 @@ limitations under the License.
}
.checkIcon * {
stroke: var(--primary-content);
stroke: var(--cpd-color-text-primary);
}

View File

@@ -25,7 +25,7 @@ import { Node } from "@react-types/shared";
import styles from "./Menu.module.css";
interface MenuProps<T> extends AriaMenuOptions<T> {
className?: String;
className?: string;
onClose: () => void;
onAction: (value: Key) => void;
label?: string;

View File

@@ -28,7 +28,7 @@ limitations under the License.
}
.modal {
background: #21262c;
background: var(--cpd-color-bg-subtle-secondary);
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
border-radius: 8px;
max-width: 90vw;

View File

@@ -15,12 +15,12 @@ limitations under the License.
*/
.tooltip {
background-color: var(--system);
background-color: var(--cpd-color-bg-subtle-secondary);
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10px;
color: var(--primary-content);
color: var(--cpd-color-text-primary);
border-radius: 8px;
max-width: 135px;
width: max-content;

View File

@@ -19,6 +19,8 @@ import { useLocation } from "react-router-dom";
import { Config } from "./config/Config";
export const PASSWORD_STRING = "password=";
interface UrlParams {
roomAlias: string | null;
roomId: string | null;
@@ -86,6 +88,10 @@ interface UrlParams {
* user's homeserver doesn't provide any.
*/
allowIceFallback: boolean;
/**
* E2EE password
*/
password: string | null;
}
/**
@@ -102,9 +108,12 @@ export const getUrlParams = (
pathname = window.location.pathname,
hash = window.location.hash
): UrlParams => {
// This is legacy code - we're moving away from using aliases
let roomAlias: string | null = null;
if (!ignoreRoomAlias) {
if (hash === "") {
// Here we handle the beginning of the alias and make sure it starts with a
// "#"
if (hash === "" || hash.startsWith("#?")) {
roomAlias = pathname.substring(1); // Strip the "/"
// Delete "/room/", if present
@@ -119,17 +128,17 @@ export const getUrlParams = (
roomAlias = hash;
}
// Add server part, if not present
if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
}
// Delete "?" and what comes afterwards
roomAlias = roomAlias.split("?")[0];
// Make roomAlias undefined, if empty
if (roomAlias.length <= 1) {
// Make roomAlias is null, if it only is a "#"
roomAlias = null;
} else {
// Add server part, if not present
if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
}
}
}
@@ -164,6 +173,7 @@ export const getUrlParams = (
return {
roomAlias,
roomId,
password: getParam("password"),
viaServers: getAllParams("via"),
isEmbedded: hasParam("embed"),
preload: hasParam("preload"),

View File

@@ -22,19 +22,5 @@ limitations under the License.
}
.userButton svg * {
fill: var(--primary-content);
}
.avatar {
width: 24px;
height: 24px;
font-size: var(--font-size-caption);
}
@media (min-width: 800px) {
.avatar {
width: 32px;
height: 32px;
font-size: var(--font-size-body);
}
fill: var(--cpd-color-icon-primary);
}

View File

@@ -35,6 +35,7 @@ interface UserMenuProps {
preventNavigation: boolean;
isAuthenticated: boolean;
isPasswordlessUser: boolean;
userId: string;
displayName: string;
avatarUrl?: string;
onAction: (value: string) => void;
@@ -44,6 +45,7 @@ export function UserMenu({
preventNavigation,
isAuthenticated,
isPasswordlessUser,
userId,
displayName,
avatarUrl,
onAction,
@@ -109,10 +111,10 @@ export function UserMenu({
>
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
<Avatar
id={userId}
name={displayName}
size={Size.SM}
className={styles.avatar}
src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
) : (
<UserIcon />

View File

@@ -67,6 +67,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
isPasswordlessUser={passwordlessUser}
avatarUrl={avatarUrl}
onAction={onAction}
userId={client?.getUserId() ?? ""}
displayName={displayName || (userName ? userName.replace("@", "") : "")}
/>
{modalState.isOpen && client && (

View File

@@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk";
import { Buffer } from "buffer";
import { widget } from "../widget";
import { getSetting, setSetting, settingsBus } from "../settings/useSetting";
import { getSetting, setSetting, getSettingKey } from "../settings/useSetting";
import {
CallEndedTracker,
CallStartedTracker,
@@ -34,6 +34,7 @@ import {
} from "./PosthogEvents";
import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams";
import { localStorageBus } from "../useLocalStorage";
/* Posthog analytics tracking.
*
@@ -413,7 +414,7 @@ export class PosthogAnalytics {
// * When the user changes their preferences on this device
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
settingsBus.on("opt-in-analytics", (optInAnalytics) => {
localStorageBus.on(getSettingKey("opt-in-analytics"), (optInAnalytics) => {
this.updateAnonymityAndIdentifyUser(optInAnalytics);
});
}

View File

@@ -81,7 +81,7 @@ limitations under the License.
}
.authLinks a {
color: var(--accent);
color: var(--cpd-color-text-action-accent);
text-decoration: none;
font-weight: normal;
}

View File

@@ -46,8 +46,8 @@ limitations under the License.
}
.button {
color: var(--primary-content);
background-color: var(--accent);
color: var(--stopgap-color-on-solid-accent);
background-color: var(--cpd-color-text-action-accent);
}
.button:focus,
@@ -62,7 +62,8 @@ limitations under the License.
}
.toolbarButton:disabled {
opacity: 0.55;
background-color: var(--cpd-color-bg-action-primary-disabled);
box-shadow: none;
}
.toolbarButton,
@@ -70,72 +71,76 @@ limitations under the License.
width: 50px;
height: 50px;
border-radius: 50px;
background-color: var(--system);
}
.toolbarButton:hover,
.toolbarButtonSecondary:hover {
background-color: var(--quinary-content);
background-color: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-icon-primary);
border: 1px solid var(--cpd-color-gray-400);
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
}
.toolbarButton.on,
.toolbarButton.off {
background-color: var(--primary-content);
background-color: var(--cpd-color-bg-action-primary-rest);
color: var(--cpd-color-icon-on-solid-primary);
}
.toolbarButtonSecondary.on {
background-color: var(--accent);
background-color: var(--cpd-color-text-success-primary);
}
.toolbarButton:active,
.toolbarButtonSecondary:active {
background-color: var(--cpd-color-bg-subtle-primary);
border: none;
box-shadow: none;
}
.toolbarButton.on:active,
.toolbarButton.off:active {
background-color: var(--cpd-color-bg-action-primary-pressed);
}
.iconButton:not(.stroke) svg * {
fill: var(--primary-content);
fill: var(--cpd-color-bg-action-primary-rest);
}
.iconButton:not(.stroke):hover svg * {
fill: var(--accent);
.iconButton:not(.stroke):tertiary svg * {
fill: var(--cpd-color-icon-accent-tertiary);
}
.iconButton.on:not(.stroke) svg * {
fill: var(--accent);
fill: var(--cpd-color-icon-accent-tertiary);
}
.iconButton.on.stroke svg * {
stroke: var(--accent);
stroke: var(--cpd-color-icon-accent-tertiary);
}
.hangupButton,
.hangupButton:hover {
background-color: var(--alert);
.hangupButton {
background-color: var(--cpd-color-bg-critical-primary);
border-color: var(--cpd-color-border-critical-subtle);
color: var(--stopgap-color-on-solid-accent);
}
.toolbarButton.on svg * {
fill: var(--accent);
}
.toolbarButton.off svg * {
fill: #21262c;
}
.toolbarButtonSecondary.on svg * {
fill: var(--primary-content);
.hangupButton:active {
background-color: var(--cpd-color-bg-critical-pressed);
}
.secondary,
.copyButton {
color: var(--accent);
border: 2px solid var(--accent);
color: var(--cpd-color-text-action-accent);
border: 2px solid var(--cpd-color-text-action-accent);
background-color: transparent;
}
.secondaryHangup {
color: var(--alert);
border: 2px solid var(--alert);
color: var(--cpd-color-text-critical-primary);
border: 2px solid var(--cpd-color-border-critical-primary);
background-color: transparent;
}
.copyButton.secondaryCopy {
color: var(--primary-content);
border-color: var(--primary-content);
color: var(--cpd-color-text-primary);
border-color: var(--cpd-color-border-interactive-primary);
}
.copyButton {
@@ -158,12 +163,12 @@ limitations under the License.
}
.copyButton:not(.on) svg * {
fill: var(--accent);
fill: var(--cpd-color-icon-accent-tertiary);
}
.copyButton.on {
border-color: transparent;
background-color: var(--accent);
background-color: var(--cpd-color-text-action-accent);
color: white;
}
@@ -172,32 +177,28 @@ limitations under the License.
}
.copyButton.secondaryCopy:not(.on) svg * {
fill: var(--primary-content);
fill: var(--cpd-color-bg-action-primary-rest);
}
.iconCopyButton svg * {
fill: var(--tertiary-content);
}
.iconCopyButton:hover svg * {
fill: var(--accent);
fill: var(--cpd-color-icon-secondary);
}
.iconCopyButton.on svg *,
.iconCopyButton.on:hover svg * {
fill: transparent;
stroke: var(--accent);
stroke: var(--cpd-color-text-action-accent);
}
.dropdownButton {
color: var(--primary-content);
color: var(--cpd-color-text-primary);
padding: 2px 8px;
border-radius: 8px;
}
.dropdownButton:hover,
.dropdownButton:active,
.dropdownButton.on {
background-color: var(--quinary-content);
background-color: var(--cpd-color-bg-action-secondary-pressed);
}
.dropdownButton svg {
@@ -205,7 +206,7 @@ limitations under the License.
}
.dropdownButton svg * {
fill: var(--primary-content);
fill: var(--cpd-color-icon-primary);
}
.lg {
@@ -215,6 +216,36 @@ limitations under the License.
.linkButton {
background-color: transparent;
border: none;
color: var(--accent);
color: var(--cpd-color-text-action-accent);
cursor: pointer;
}
@media (hover: hover) {
.toolbarButton:hover,
.toolbarButtonSecondary:hover {
background-color: var(--cpd-color-bg-subtle-primary);
border: none;
box-shadow: none;
}
.toolbarButton.on:hover,
.toolbarButton.off:hover {
background-color: var(--cpd-color-bg-action-primary-hovered);
}
.iconButton:not(.stroke):hover svg * {
fill: var(--cpd-color-icon-accent-tertiary);
}
.hangupButton:hover {
background-color: var(--cpd-color-bg-critical-hovered);
}
.iconCopyButton:hover svg * {
fill: var(--cpd-color-icon-accent-tertiary);
}
.dropdownButton:hover {
background-color: var(--cpd-color-bg-action-secondary-hovered);
}
}

View File

@@ -13,26 +13,25 @@ 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 { forwardRef, useCallback } from "react";
import { forwardRef } from "react";
import { PressEvent } from "@react-types/shared";
import classNames from "classnames";
import { useButton } from "@react-aria/button";
import { mergeProps, useObjectRef } from "@react-aria/utils";
import { useTranslation } from "react-i18next";
import { Tooltip } from "@vector-im/compound-web";
import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg";
import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg";
import { ReactComponent as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.svg";
import { ReactComponent as VideoCallOffIcon } from "@vector-im/compound-design-tokens/icons/video-call-off.svg";
import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens/icons/end-call.svg";
import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg";
import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg";
import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg";
import styles from "./Button.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
import { TooltipTrigger } from "../Tooltip";
import { VolumeIcon } from "./VolumeIcon";
export type ButtonVariant =
@@ -129,7 +128,7 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
>
<>
{children}
{variant === "dropdown" && <ArrowDownIcon />}
{variant === "dropdown" && <ChevronDownIcon />}
</>
</button>
);
@@ -145,15 +144,15 @@ export function MicButton({
[index: string]: unknown;
}) {
const { t } = useTranslation();
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
const label = muted ? t("Microphone off") : t("Microphone on");
return (
<TooltipTrigger
tooltip={() => (muted ? t("Unmute microphone") : t("Mute microphone"))}
>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <MuteMicIcon /> : <MicIcon />}
<Tooltip label={label}>
<Button variant="toolbar" {...rest} on={!muted}>
<Icon aria-label={label} />
</Button>
</TooltipTrigger>
</Tooltip>
);
}
@@ -166,15 +165,15 @@ export function VideoButton({
[index: string]: unknown;
}) {
const { t } = useTranslation();
const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
const label = muted ? t("Video off") : t("Video on");
return (
<TooltipTrigger
tooltip={() => (muted ? t("Turn on camera") : t("Turn off camera"))}
>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
<Tooltip label={label}>
<Button variant="toolbar" {...rest} on={!muted}>
<Icon aria-label={label} />
</Button>
</TooltipTrigger>
</Tooltip>
);
}
@@ -189,15 +188,14 @@ export function ScreenshareButton({
[index: string]: unknown;
}) {
const { t } = useTranslation();
const label = enabled ? t("Sharing screen") : t("Share screen");
return (
<TooltipTrigger
tooltip={() => (enabled ? t("Stop sharing screen") : t("Share screen"))}
>
<Button variant="toolbarSecondary" {...rest} on={enabled}>
<ScreenshareIcon />
<Tooltip label={label}>
<Button variant="toolbar" {...rest} on={enabled}>
<ShareScreenSolidIcon aria-label={label} />
</Button>
</TooltipTrigger>
</Tooltip>
);
}
@@ -210,18 +208,17 @@ export function HangupButton({
[index: string]: unknown;
}) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Leave"), [t]);
return (
<TooltipTrigger tooltip={tooltip}>
<Tooltip label={t("End call")}>
<Button
variant="toolbar"
className={classNames(styles.hangupButton, className)}
{...rest}
>
<HangupIcon />
<EndCallIcon aria-label={t("End call")} />
</Button>
</TooltipTrigger>
</Tooltip>
);
}
@@ -234,36 +231,13 @@ export function SettingsButton({
[index: string]: unknown;
}) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Settings"), [t]);
return (
<TooltipTrigger tooltip={tooltip}>
<Tooltip label={t("Settings")}>
<Button variant="toolbar" {...rest}>
<SettingsIcon width={20} height={20} />
<SettingsSolidIcon aria-label={t("Settings")} />
</Button>
</TooltipTrigger>
);
}
export function InviteButton({
className,
variant = "toolbar",
...rest
}: {
className?: string;
variant?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Invite"), [t]);
return (
<TooltipTrigger tooltip={tooltip}>
<Button variant={variant} {...rest}>
<AddUserIcon />
</Button>
</TooltipTrigger>
</Tooltip>
);
}
@@ -276,14 +250,13 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Local volume"), [t]);
return (
<TooltipTrigger tooltip={tooltip}>
<Tooltip label={t("Local volume")}>
<Button variant="icon" {...rest}>
<VolumeIcon volume={volume} />
<VolumeIcon volume={volume} aria-label={t("Local volume")} />
</Button>
</TooltipTrigger>
</Tooltip>
);
}
@@ -296,15 +269,14 @@ export function FullscreenButton({
...rest
}: FullscreenButtonProps) {
const { t } = useTranslation();
const tooltip = useCallback(() => {
return fullscreen ? t("Exit full screen") : t("Full screen");
}, [fullscreen, t]);
const Icon = fullscreen ? FullscreenExit : Fullscreen;
const label = fullscreen ? t("Exit full screen") : t("Full screen");
return (
<TooltipTrigger tooltip={tooltip}>
<Tooltip label={label}>
<Button variant="icon" {...rest}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
<Icon aria-label={label} />
</Button>
</TooltipTrigger>
</Tooltip>
);
}

View File

@@ -0,0 +1,31 @@
/*
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 { ComponentPropsWithoutRef, FC } from "react";
import { Button } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
export const ShareButton: FC<
Omit<ComponentPropsWithoutRef<"button">, "children">
> = (props) => {
const { t } = useTranslation();
return (
<Button kind="secondary" size="sm" Icon={UserAddSolidIcon} {...props}>
{t("Share")}
</Button>
);
};

View File

@@ -15,19 +15,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ComponentPropsWithoutRef, FC } from "react";
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
import { ReactComponent as Audio } from "../icons/Audio.svg";
interface Props {
interface Props extends ComponentPropsWithoutRef<"svg"> {
/**
* Number between 0 and 1
*/
volume: number;
}
export function VolumeIcon({ volume }: Props) {
if (volume <= 0) return <AudioMuted />;
if (volume <= 0.5) return <AudioLow />;
return <Audio />;
}
export const VolumeIcon: FC<Props> = ({ volume, ...rest }) => {
if (volume <= 0) return <AudioMuted {...rest} />;
if (volume <= 0.5) return <AudioLow {...rest} />;
return <Audio {...rest} />;
};

View File

@@ -0,0 +1,85 @@
/*
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 { useEffect, useMemo } from "react";
import { useEnableE2EE } from "../settings/useSetting";
import { useLocalStorage } from "../useLocalStorage";
import { useClient } from "../ClientContext";
import { PASSWORD_STRING, useUrlParams } from "../UrlParams";
export const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
`room-shared-key-${roomId}`;
export const useInternalRoomSharedKey = (
roomId: string
): [string | null, (value: string) => void] => {
const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]);
const [e2eeEnabled] = useEnableE2EE();
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key);
return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey];
};
export const useRoomSharedKey = (roomId: string): string | null => {
return useInternalRoomSharedKey(roomId)[0];
};
export const useManageRoomSharedKey = (roomId: string): string | null => {
const { password } = useUrlParams();
const [e2eeSharedKey, setE2EESharedKey] = useInternalRoomSharedKey(roomId);
useEffect(() => {
if (!password) return;
if (password === "") return;
if (password === e2eeSharedKey) return;
setE2EESharedKey(password);
}, [password, e2eeSharedKey, setE2EESharedKey]);
useEffect(() => {
const hash = location.hash;
if (!hash.includes("?")) return;
if (!hash.includes(PASSWORD_STRING)) return;
if (password !== e2eeSharedKey) return;
const [hashStart, passwordStart] = hash.split(PASSWORD_STRING);
const hashEnd = passwordStart.split("&")[1];
location.replace((hashStart ?? "") + (hashEnd ?? ""));
}, [password, e2eeSharedKey]);
return e2eeSharedKey;
};
export const useIsRoomE2EE = (roomId: string): boolean | null => {
const { isEmbedded } = useUrlParams();
const client = useClient();
const room = useMemo(
() => client.client?.getRoom(roomId) ?? null,
[roomId, client.client]
);
const isE2EE = useMemo(() => {
if (isEmbedded) {
return false;
} else {
return room ? !room?.getCanonicalAlias() : null;
}
}, [isEmbedded, room]);
return isE2EE;
};

View File

@@ -26,7 +26,7 @@ limitations under the License.
.callTile {
height: 95px;
padding: 12px;
background-color: var(--system);
background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: 8px;
overflow: hidden;
box-sizing: border-box;
@@ -52,7 +52,7 @@ limitations under the License.
flex-direction: column;
flex: 1;
padding: 0 16px;
color: var(--primary-content);
color: var(--cpd-color-text-primary);
min-width: 0;
}

View File

@@ -19,31 +19,29 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { CopyButton } from "../button";
import { Facepile } from "../Facepile";
import { Avatar, Size } from "../Avatar";
import styles from "./CallList.module.css";
import { getRoomUrl } from "../matrix-utils";
import { Body, Caption } from "../typography/Typography";
import { Body } from "../typography/Typography";
import { GroupCallRoom } from "./useGroupCallRooms";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface CallListProps {
rooms: GroupCallRoom[];
client: MatrixClient;
disableFacepile?: boolean;
}
export function CallList({ rooms, client, disableFacepile }: CallListProps) {
export function CallList({ rooms, client }: CallListProps) {
return (
<>
<div className={styles.callList}>
{rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => (
{rooms.map(({ room, roomAlias, roomName, avatarUrl, participants }) => (
<CallTile
key={roomAlias}
client={client}
name={roomName}
avatarUrl={avatarUrl}
roomAlias={roomAlias}
roomId={room.roomId}
participants={participants}
disableFacepile={disableFacepile}
/>
))}
{rooms.length > 3 && (
@@ -59,51 +57,28 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
interface CallTileProps {
name: string;
avatarUrl: string;
roomAlias: string;
roomId: string;
participants: RoomMember[];
client: MatrixClient;
disableFacepile?: boolean;
}
function CallTile({
name,
avatarUrl,
roomAlias,
participants,
client,
disableFacepile,
}: CallTileProps) {
function CallTile({ name, avatarUrl, roomId }: CallTileProps) {
const roomSharedKey = useRoomSharedKey(roomId);
return (
<div className={styles.callTile}>
<Link
to={`/${roomAlias.substring(1).split(":")[0]}`}
className={styles.callTileLink}
>
<Avatar
size={Size.LG}
bgKey={name}
src={avatarUrl}
fallback={name.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
<Avatar id={roomId} name={name} size={Size.LG} src={avatarUrl} />
<div className={styles.callInfo}>
<Body overflowEllipsis fontWeight="semiBold">
{name}
</Body>
<Caption overflowEllipsis>{getRoomUrl(roomAlias)}</Caption>
{participants && !disableFacepile && (
<Facepile
className={styles.facePile}
client={client}
members={participants}
/>
)}
</div>
<div className={styles.copyButtonSpacer} />
</Link>
<CopyButton
className={styles.copyButton}
variant="icon"
value={getRoomUrl(roomAlias)}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
/>
</div>
);

View File

@@ -39,5 +39,5 @@ limitations under the License.
}
.notice {
color: var(--secondary-content);
color: var(--cpd-color-text-secondary);
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next";
import {
@@ -37,9 +38,11 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption, Title } from "../typography/Typography";
import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import { useOptInAnalytics } from "../settings/useSetting";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
import { setLocalStorageItem } from "../useLocalStorage";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
interface Props {
client: MatrixClient;
@@ -54,6 +57,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => {
@@ -70,16 +74,23 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
setError(undefined);
setLoading(true);
const [roomAlias] = await createRoom(client, roomName, ptt);
const roomId = (
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
)[1];
if (roomAlias) {
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
if (e2eeEnabled) {
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(roomId),
randomString(32)
);
}
history.push(`/room/#?roomId=${roomId}`);
}
submit().catch((error) => {
if (error.errcode === "M_ROOM_IN_USE") {
setExistingRoomId(roomAliasLocalpartFromRoomName(roomName));
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
setLoading(false);
setError(undefined);
modalState.open();
@@ -90,15 +101,15 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
}
});
},
[client, history, modalState, callType]
[client, history, modalState, callType, e2eeEnabled]
);
const recentRooms = useGroupCallRooms(client);
const [existingRoomId, setExistingRoomId] = useState<string>();
const [existingAlias, setExistingAlias] = useState<string>();
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);
history.push(`/${existingAlias}`);
}, [history, existingAlias]);
const callNameLabel =
callType === CallType.Video
@@ -159,7 +170,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
<Title className={styles.recentCallsTitle}>
{t("Your recent calls")}
</Title>
<CallList rooms={recentRooms} client={client} disableFacepile />
<CallList rooms={recentRooms} client={client} />
</>
)}
</main>

View File

@@ -47,5 +47,5 @@ limitations under the License.
}
.notice {
color: var(--secondary-content);
color: var(--cpd-color-text-secondary);
}

View File

@@ -40,9 +40,11 @@ import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useOptInAnalytics } from "../settings/useSetting";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
import { setLocalStorageItem } from "../useLocalStorage";
export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
@@ -58,6 +60,8 @@ export const UnauthenticatedView: FC = () => {
const history = useHistory();
const { t } = useTranslation();
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e) => {
e.preventDefault();
@@ -79,9 +83,18 @@ export const UnauthenticatedView: FC = () => {
true
);
let roomAlias: string;
let roomId: string;
try {
[roomAlias] = await createRoom(client, roomName, ptt);
roomId = (
await createRoom(client, roomName, ptt, e2eeEnabled ?? false)
)[1];
if (e2eeEnabled) {
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(roomId),
randomString(32)
);
}
} catch (error) {
if (!setClient) {
throw error;
@@ -110,7 +123,7 @@ export const UnauthenticatedView: FC = () => {
}
setClient({ client, session });
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
history.push(`/room/#?roomId=${roomId}`);
}
submit().catch((error) => {
@@ -120,7 +133,16 @@ export const UnauthenticatedView: FC = () => {
reset();
});
},
[register, reset, execute, history, callType, modalState, setClient]
[
register,
reset,
execute,
history,
callType,
modalState,
setClient,
e2eeEnabled,
]
);
const callNameLabel =

View File

@@ -89,8 +89,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const groupCalls = client.groupCallEventHandler.groupCalls.values();
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias
const sortedRooms = sortRooms(client, filteredRooms);
const sortedRooms = sortRooms(client, rooms);
const items = sortedRooms.map((room) => {
const groupCall = client.getGroupCallForRoom(room.roomId)!;

View File

@@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.8914 9.0784C18.5132 9.0784 18.2066 8.777 18.2066 8.4052V5.71251H15.4678C15.0896 5.71251 14.783 5.41111 14.783 5.03931C14.783 4.66751 15.0896 4.36611 15.4678 4.36611H18.2066V1.6732C18.2066 1.3014 18.5132 1 18.8914 1C19.2696 1 19.5761 1.3014 19.5761 1.6732V4.36611H22.3153C22.6934 4.36611 23 4.66751 23 5.03931C23 5.41111 22.6934 5.71251 22.3153 5.71251H19.5761V8.4052C19.5761 8.777 19.2696 9.0784 18.8914 9.0784Z" fill="#ffffff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5002 11.5094C19.8949 11.5094 20.281 11.4736 20.6555 11.4051C20.7505 11.9558 20.8 12.5221 20.8 13.1C20.8 18.5676 16.3676 23 10.9 23C5.43238 23 1 18.5676 1 13.1C1 7.63238 5.43238 3.2 10.9 3.2C11.7973 3.2 12.6666 3.31937 13.4931 3.54308C13.3177 4.11118 13.2234 4.71396 13.2234 5.33841C13.2234 8.74655 16.0336 11.5094 19.5002 11.5094ZM11.1308 21.4653C13.2927 21.4653 15.2551 20.6258 16.6993 19.2603C15.7939 17.0769 13.6417 15.5412 11.1308 15.5412C8.61988 15.5412 6.46768 17.0769 5.56225 19.2603C7.00643 20.6258 8.96888 21.4653 11.1308 21.4653ZM11.2799 7.81768C9.78854 7.9515 8.62035 9.18434 8.62035 10.6854C8.62035 12.2759 9.93179 13.5652 11.5495 13.5652C12.6276 13.5652 13.5696 12.9927 14.0781 12.1401C12.6655 11.1035 11.6462 9.57751 11.2799 7.81768Z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,3 +0,0 @@
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.47012 18H17.5301C19.0701 18 20.0301 16.33 19.2601 15L11.7301 1.98999C10.9601 0.659993 9.04012 0.659993 8.27012 1.98999L0.740121 15C-0.0298788 16.33 0.930121 18 2.47012 18ZM10.0001 11C9.45012 11 9.00012 10.55 9.00012 9.99999V7.99999C9.00012 7.44999 9.45012 6.99999 10.0001 6.99999C10.5501 6.99999 11.0001 7.44999 11.0001 7.99999V9.99999C11.0001 10.55 10.5501 11 10.0001 11ZM11.0001 15H9.00012V13H11.0001V15Z" fill="#737D8C"/>
</svg>

Before

Width:  |  Height:  |  Size: 540 B

View File

@@ -1,3 +0,0 @@
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.6663 8H1.33301M1.33301 8L7.83301 14.5M1.33301 8L7.83301 1.5" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,6 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.290472 1.37627C0.677768 0.985743 1.3057 0.985743 1.69299 1.37627L16.569 16.3762C16.9563 16.7668 16.9563 17.3999 16.569 17.7904C16.1817 18.181 15.5538 18.181 15.1665 17.7904L0.290472 2.79048C-0.096824 2.39995 -0.096824 1.76679 0.290472 1.37627Z" fill="#394049"/>
<path d="M0.597515 5.19186C0.323238 5.646 0.165249 6.17941 0.165249 6.75001V14.0833C0.165249 15.7402 1.49729 17.0833 3.14045 17.0833H12.363L0.639137 5.2371C0.624608 5.22242 0.610733 5.20733 0.597515 5.19186Z" fill="#394049"/>
<path d="M14.2148 6.75002V11.9031L6.14598 3.75002H11.2396C12.8828 3.75002 14.2148 5.09317 14.2148 6.75002Z" fill="#394049"/>
<path d="M18.3887 5.88312L15.8677 7.91669V12.9167L18.3887 14.9503C19.038 15.4741 19.9999 15.0079 19.9999 14.1694V6.66399C19.9999 5.82548 19.038 5.35931 18.3887 5.88312Z" fill="#394049"/>
</svg>

Before

Width:  |  Height:  |  Size: 915 B

View File

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="13" y="11" width="5" height="5" rx="1.16667" fill="white"/>
<rect x="13" y="5" width="5" height="5" rx="1.16667" fill="white"/>
<rect x="7" y="5" width="5" height="5" rx="1.16667" fill="white"/>
<rect x="4" y="11" width="8" height="8" rx="1.14286" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 375 B

View File

@@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.68883 13.3012C7.63969 14.3278 9.92871 16.1054 10.5526 16.4702C10.5895 16.4917 10.6317 16.5168 10.679 16.5449C11.631 17.1109 14.6141 18.8842 16.7875 17.2246C18.4714 15.9389 17.9239 14.4926 17.3571 14.0626C16.9691 13.7608 15.8258 12.9288 14.7502 12.1795C13.694 11.4436 13.1053 12.0332 12.7073 12.4318C12.7 12.4391 12.6927 12.4464 12.6856 12.4536L11.8848 13.2544C11.6808 13.4583 11.3706 13.3839 11.0735 13.1505C10.0074 12.3385 9.22308 11.555 8.8307 11.1626L8.82739 11.1593C8.43506 10.767 7.66123 9.99241 6.84932 8.92627C6.61592 8.62914 6.54149 8.31893 6.74542 8.11501L7.54622 7.31421C7.55341 7.30702 7.56067 7.29977 7.568 7.29245C7.96663 6.89444 8.55618 6.30581 7.82033 5.24958C7.07097 4.17394 6.239 3.03068 5.93717 2.64265C5.50723 2.07588 4.06088 1.52836 2.77514 3.21225C1.11556 5.38572 2.88891 8.36878 3.45484 9.32078C3.48293 9.36803 3.50805 9.41028 3.52962 9.44717C3.8944 10.0711 5.66224 12.3503 6.68883 13.3012Z" fill="white"/>
<path d="M17.2473 2.56504C17.4497 2.3595 17.4497 2.02626 17.2473 1.82072C17.0448 1.61518 16.7166 1.61518 16.5142 1.82072L14.1663 4.20452L11.8185 1.82072C11.6161 1.61518 11.2878 1.61518 11.0854 1.82072C10.883 2.02626 10.883 2.3595 11.0854 2.56504L13.4332 4.94884L10.9848 7.43475C10.7824 7.64029 10.7824 7.97354 10.9848 8.17908C11.1873 8.38462 11.5155 8.38462 11.7179 8.17908L14.1663 5.69316L16.6148 8.17907C16.8172 8.38461 17.1454 8.38461 17.3478 8.17907C17.5503 7.97354 17.5503 7.64029 17.3478 7.43475L14.8994 4.94884L17.2473 2.56504Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

11
src/icons/LogoMark.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Logo Mark">
<rect width="47.86" height="48" rx="23.93" fill="#0DBD8B"/>
<g id="Union">
<path d="M21.3075 9.42871C20.3396 9.42871 19.5549 10.214 19.5549 11.1828C19.5549 12.1516 20.3396 12.9369 21.3075 12.9369C25.9321 12.9369 29.6811 16.689 29.6811 21.3175C29.6811 22.2863 30.4657 23.0716 31.4337 23.0716C32.4016 23.0716 33.1863 22.2863 33.1863 21.3175C33.1863 14.7515 27.868 9.42871 21.3075 9.42871Z" fill="white"/>
<path d="M38.4591 21.3174C38.4591 20.3486 37.6745 19.5633 36.7065 19.5633C35.7386 19.5633 34.9539 20.3486 34.9539 21.3174C34.9539 25.9459 31.2049 29.698 26.5804 29.698C25.6124 29.698 24.8277 30.4833 24.8277 31.4521C24.8277 32.4209 25.6124 33.2062 26.5804 33.2062C33.1408 33.2062 38.4591 27.8834 38.4591 21.3174Z" fill="white"/>
<path d="M28.3329 36.8173C28.3329 37.786 27.5482 38.5714 26.5803 38.5714C20.0198 38.5714 14.7015 33.2486 14.7015 26.6826C14.7015 25.7138 15.4862 24.9285 16.4541 24.9285C17.4221 24.9285 18.2067 25.7138 18.2067 26.6826C18.2067 31.3111 21.9557 35.0632 26.5803 35.0632C27.5482 35.0632 28.3329 35.8485 28.3329 36.8173Z" fill="white"/>
<path d="M9.40112 26.6827C9.40112 27.6514 10.1858 28.4368 11.1537 28.4368C12.1217 28.4368 12.9064 27.6514 12.9064 26.6827C12.9064 22.0542 16.6553 18.3021 21.2799 18.3021C22.2478 18.3021 23.0325 17.5167 23.0325 16.548C23.0325 15.5792 22.2478 14.7939 21.2799 14.7939C14.7194 14.7939 9.40112 20.1167 9.40112 26.6827Z" fill="white"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

17
src/icons/LogoType.svg Normal file
View File

@@ -0,0 +1,17 @@
<svg width="160" height="22" viewBox="0 0 160 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Logo Type">
<g id="Vector">
<path d="M14.8673 15.1575H3.39742C3.53293 16.3508 3.96849 17.3036 4.70411 18.0157C5.43974 18.7087 6.40766 19.0551 7.60789 19.0551C8.40159 19.0551 9.11785 18.8626 9.75668 18.4777C10.3955 18.0927 10.8504 17.5731 11.1215 16.9186H14.606C14.1414 18.4392 13.2702 19.671 11.9926 20.6142C10.7343 21.5381 9.24368 22 7.52078 22C5.27519 22 3.45549 21.259 2.06168 19.7769C0.687227 18.2948 0 16.4182 0 14.147C0 11.9335 0.696906 10.0761 2.09072 8.5748C3.48453 7.07349 5.28487 6.32283 7.49174 6.32283C9.69861 6.32283 11.4796 7.06387 12.8347 8.54593C14.2091 10.0087 14.8964 11.8565 14.8964 14.0892L14.8673 15.1575ZM7.49174 9.12336C6.40766 9.12336 5.50749 9.44095 4.79123 10.0761C4.07496 10.7113 3.62972 11.5582 3.45549 12.6168H11.4699C11.315 11.5582 10.8892 10.7113 10.1922 10.0761C9.49534 9.44095 8.59517 9.12336 7.49174 9.12336Z" fill="currentColor"/>
<path d="M17.2743 17.1785V0H20.7298V17.2362C20.7298 18.0061 21.1557 18.3911 22.0074 18.3911L22.6172 18.3622V21.6247C22.2881 21.6824 21.9397 21.7113 21.5719 21.7113C20.0813 21.7113 18.9875 21.336 18.2906 20.5853C17.6131 19.8346 17.2743 18.699 17.2743 17.1785Z" fill="currentColor"/>
<path d="M38.71 15.1575H27.2401C27.3756 16.3508 27.8112 17.3036 28.5468 18.0157C29.2824 18.7087 30.2504 19.0551 31.4506 19.0551C32.2443 19.0551 32.9606 18.8626 33.5994 18.4777C34.2382 18.0927 34.6931 17.5731 34.9642 16.9186H38.4487C37.9841 18.4392 37.113 19.671 35.8353 20.6142C34.577 21.5381 33.0864 22 31.3635 22C29.1179 22 27.2982 21.259 25.9044 19.7769C24.5299 18.2948 23.8427 16.4182 23.8427 14.147C23.8427 11.9335 24.5396 10.0761 25.9334 8.5748C27.3272 7.07349 29.1276 6.32283 31.3344 6.32283C33.5413 6.32283 35.3223 7.06387 36.6774 8.54593C38.0518 10.0087 38.7391 11.8565 38.7391 14.0892L38.71 15.1575ZM31.3344 9.12336C30.2504 9.12336 29.3502 9.44095 28.6339 10.0761C27.9177 10.7113 27.4724 11.5582 27.2982 12.6168H35.3126C35.1577 11.5582 34.7319 10.7113 34.035 10.0761C33.3381 9.44095 32.4379 9.12336 31.3344 9.12336Z" fill="currentColor"/>
<path d="M54.3001 13.0499V21.6535H50.8446V12.6745C50.8446 10.4033 49.8961 9.26772 47.9989 9.26772C46.9729 9.26772 46.1502 9.59493 45.5307 10.2493C44.9306 10.9038 44.6306 11.7988 44.6306 12.9344V21.6535H41.1751V6.66929H44.3692V8.66142C44.737 7.98775 45.2984 7.42957 46.0534 6.98688C46.8084 6.54418 47.7473 6.32283 48.8701 6.32283C50.9608 6.32283 52.4707 7.11199 53.4 8.69029C54.6776 7.11199 56.3812 6.32283 58.5106 6.32283C60.2722 6.32283 61.6273 6.87139 62.5759 7.9685C63.5244 9.04637 63.9987 10.4707 63.9987 12.2415V21.6535H60.5432V12.6745C60.5432 10.4033 59.5947 9.26772 57.6975 9.26772C56.6522 9.26772 55.8198 9.60455 55.2003 10.2782C54.6002 10.9326 54.3001 11.8565 54.3001 13.0499Z" fill="currentColor"/>
<path d="M81.1834 15.1575H69.7135C69.849 16.3508 70.2846 17.3036 71.0202 18.0157C71.7558 18.7087 72.7237 19.0551 73.924 19.0551C74.7177 19.0551 75.4339 18.8626 76.0728 18.4777C76.7116 18.0927 77.1665 17.5731 77.4375 16.9186H80.9221C80.4575 18.4392 79.5863 19.671 78.3087 20.6142C77.0504 21.5381 75.5598 22 73.8369 22C71.5913 22 69.7716 21.259 68.3778 19.7769C67.0033 18.2948 66.3161 16.4182 66.3161 14.147C66.3161 11.9335 67.013 10.0761 68.4068 8.5748C69.8006 7.07349 71.601 6.32283 73.8078 6.32283C76.0147 6.32283 77.7957 7.06387 79.1508 8.54593C80.5252 10.0087 81.2124 11.8565 81.2124 14.0892L81.1834 15.1575ZM73.8078 9.12336C72.7237 9.12336 71.8236 9.44095 71.1073 10.0761C70.391 10.7113 69.9458 11.5582 69.7716 12.6168H77.786C77.6311 11.5582 77.2052 10.7113 76.5083 10.0761C75.8114 9.44095 74.9113 9.12336 73.8078 9.12336Z" fill="currentColor"/>
<path d="M86.8426 6.66929V8.66142C87.191 8.007 87.7621 7.45844 88.5558 7.01575C89.3689 6.55381 90.3465 6.32283 91.4886 6.32283C93.2696 6.32283 94.6441 6.86177 95.612 7.93963C96.5993 9.0175 97.0929 10.4514 97.0929 12.2415V21.6535H93.6374V12.6745C93.6374 11.6159 93.3858 10.7883 92.8824 10.1916C92.3985 9.57568 91.6532 9.26772 90.6465 9.26772C89.5431 9.26772 88.672 9.59493 88.0331 10.2493C87.4137 10.9038 87.1039 11.8084 87.1039 12.9633V21.6535H83.6484V6.66929H86.8426Z" fill="currentColor"/>
<path d="M107.185 18.5932V21.5669C106.759 21.6824 106.159 21.7402 105.384 21.7402C102.442 21.7402 100.971 20.2677 100.971 17.3228V9.41208H98.6766V6.66929H100.971V2.77165H104.426V6.66929H107.243V9.41208H104.426V16.9764C104.426 18.1505 104.987 18.7375 106.11 18.7375L107.185 18.5932Z" fill="currentColor"/>
<path d="M116.115 18.9881C114.474 17.2035 113.653 14.9429 113.653 12.2064C113.653 9.4699 114.474 7.21782 116.115 5.45015C117.773 3.66548 119.953 2.77314 122.654 2.77314C124.876 2.77314 126.756 3.38503 128.295 4.6088C129.833 5.83258 130.816 7.47277 131.244 9.52939H129.269C128.91 7.99967 128.132 6.7844 126.936 5.88357C125.739 4.98273 124.312 4.53232 122.654 4.53232C120.534 4.53232 118.824 5.23769 117.525 6.64842C116.243 8.05916 115.602 9.91182 115.602 12.2064C115.602 14.501 116.243 16.3536 117.525 17.7644C118.824 19.1751 120.534 19.8805 122.654 19.8805C124.312 19.8805 125.739 19.4301 126.936 18.5292C128.132 17.6284 128.91 16.4131 129.269 14.8834H131.244C130.816 16.94 129.833 18.5802 128.295 19.804C126.756 21.0278 124.876 21.6397 122.654 21.6397C119.953 21.6397 117.773 20.7558 116.115 18.9881Z" fill="currentColor"/>
<path d="M143.174 15.0874C140.832 15.0874 139.233 15.1384 138.379 15.2403C137.541 15.3253 136.926 15.4698 136.532 15.6738C135.831 16.0647 135.481 16.6936 135.481 17.5604C135.481 19.2261 136.473 20.0589 138.456 20.0589C139.977 20.0589 141.139 19.719 141.943 19.0391C142.763 18.3593 143.174 17.4499 143.174 16.3111V15.0874ZM138.25 21.5632C136.763 21.5632 135.626 21.2062 134.84 20.4924C134.071 19.7615 133.686 18.8012 133.686 17.6114C133.686 16.8295 133.891 16.1327 134.301 15.5208C134.729 14.9089 135.31 14.4585 136.045 14.1695C136.661 13.9316 137.455 13.7786 138.43 13.7106C139.404 13.6256 140.986 13.5831 143.174 13.5831V12.7418C143.174 10.6002 141.943 9.52939 139.481 9.52939C137.361 9.52939 136.131 10.3877 135.789 12.1044H134.019C134.207 10.8466 134.746 9.84383 135.635 9.09597C136.541 8.34811 137.849 7.97418 139.558 7.97418C141.387 7.97418 142.746 8.3991 143.635 9.24894C144.541 10.0988 144.994 11.2716 144.994 12.7673V21.2572H143.251V19.3706C142.345 20.8323 140.678 21.5632 138.25 21.5632Z" fill="currentColor"/>
<path d="M149.358 18.4018V2.13576H151.178V18.1978C151.178 18.7247 151.264 19.0901 151.435 19.2941C151.623 19.498 151.956 19.6 152.435 19.6L152.948 19.549V21.2062C152.657 21.2572 152.341 21.2827 151.999 21.2827C150.238 21.2827 149.358 20.3224 149.358 18.4018Z" fill="currentColor"/>
<path d="M155.944 18.4018V2.13576H157.764V18.1978C157.764 18.7247 157.85 19.0901 158.021 19.2941C158.209 19.498 158.542 19.6 159.021 19.6L159.534 19.549V21.2062C159.243 21.2572 158.927 21.2827 158.585 21.2827C156.824 21.2827 155.944 20.3224 155.944 18.4018Z" fill="currentColor"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="17" viewBox="0 0 24 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 0.5C2.09315 0.5 0.75 1.84314 0.75 3.5V13.5713C0.75 15.2282 2.09315 16.5713 3.75 16.5713H20.2499C21.9067 16.5713 23.2499 15.2282 23.2499 13.5713V3.5C23.2499 1.84315 21.9067 0.5 20.2499 0.5H3.75ZM12.7998 11.9106C12.7998 12.3524 12.4416 12.7106 11.9998 12.7106C11.558 12.7106 11.1998 12.3524 11.1998 11.9106L11.1998 7.09206L9.43158 8.86029C9.11916 9.17271 8.61263 9.17271 8.30021 8.86029C7.98779 8.54787 7.98779 8.04134 8.30021 7.72892L11.4341 4.59501C11.7465 4.28259 12.2531 4.28259 12.5655 4.59501L15.6994 7.72892C16.0118 8.04134 16.0118 8.54787 15.6994 8.86029C15.387 9.17271 14.8805 9.17271 14.568 8.86029L12.7998 7.09206L12.7998 11.9106Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 814 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 17.8689V2.51551C4 2.06669 4.53728 1.83452 4.86986 2.13591C8.18767 5.14263 10.9111 7.48209 13.102 9.36399L13.102 9.36403C18.3295 13.8544 20.5243 15.7398 20.5243 17.8689C20.5243 19.4181 19.6538 20.0153 18.1044 20.79C16.5549 21.5648 14.4534 22 12.2621 22C10.0709 22 7.96938 21.5648 6.41992 20.79C4.87047 20.0153 4 18.9646 4 17.8689ZM12.2621 20.9673C16.2548 20.9673 19.4915 19.5801 19.4915 17.869C19.4915 16.1578 16.2548 14.7707 12.2621 14.7707C8.26947 14.7707 5.03277 16.1578 5.03277 17.869C5.03277 19.5801 8.26947 20.9673 12.2621 20.9673ZM16.2618 8.67876C16.1718 8.64549 16.1718 8.51831 16.2618 8.48504L17.84 7.90103C17.8683 7.89057 17.8906 7.86828 17.901 7.84001L18.4851 6.26174C18.5183 6.17182 18.6455 6.17182 18.6788 6.26174L19.2628 7.84001C19.2733 7.86828 19.2955 7.89057 19.3238 7.90103L20.9021 8.48504C20.992 8.51831 20.992 8.64549 20.9021 8.67876L19.3238 9.26277C19.2955 9.27323 19.2733 9.29552 19.2628 9.32379L18.6788 10.9021C18.6455 10.992 18.5183 10.992 18.4851 10.9021L17.901 9.32379C17.8906 9.29552 17.8683 9.27323 17.84 9.26277L16.2618 8.67876ZM13.2618 5.45232C13.1718 5.48559 13.1718 5.61276 13.2618 5.64604L14.0862 5.95111C14.1145 5.96157 14.1368 5.98386 14.1472 6.01213L14.4523 6.83657C14.4856 6.92649 14.6127 6.92649 14.646 6.83657L14.9511 6.01213C14.9615 5.98386 14.9838 5.96157 15.0121 5.95111L15.8365 5.64603C15.9265 5.61276 15.9265 5.48559 15.8365 5.45232L15.0121 5.14725C14.9838 5.13679 14.9615 5.1145 14.9511 5.08623L14.646 4.26178C14.6127 4.17187 14.4856 4.17187 14.4523 4.26178L14.1472 5.08623C14.1368 5.1145 14.1145 5.13679 14.0862 5.14725L13.2618 5.45232Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,6 +0,0 @@
<svg data-testid="videoTile_muted" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.20333 0.963373C0.474437 0.690007 0.913989 0.690007 1.1851 0.963373L11.5983 11.4633C11.8694 11.7367 11.8694 12.1799 11.5983 12.4533C11.3272 12.7267 10.8876 12.7267 10.6165 12.4533L0.20333 1.95332C-0.0677768 1.67995 -0.0677768 1.23674 0.20333 0.963373Z" fill="white"/>
<path d="M0.418261 3.63429C0.226267 3.95219 0.115674 4.32557 0.115674 4.725V9.85832C0.115674 11.0181 1.0481 11.9583 2.19831 11.9583H8.65411L0.447396 3.66596C0.437225 3.65568 0.427513 3.64511 0.418261 3.63429Z" fill="white"/>
<path d="M9.95036 4.725V8.33212L4.30219 2.625H7.86772C9.01793 2.625 9.95036 3.5652 9.95036 4.725Z" fill="white"/>
<path d="M12.8721 4.11817L11.1074 5.54167V9.04166L12.8721 10.4652C13.3266 10.8318 14 10.5055 14 9.91855V4.66478C14 4.07782 13.3266 3.7515 12.8721 4.11817Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 922 B

View File

@@ -21,6 +21,8 @@ limitations under the License.
(to avoid having to maintain a fork of Inter). */
@import "normalize.css/normalize.css";
@import "@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css";
@import "@vector-im/compound-web/dist/style.css";
:root {
--font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
@@ -37,22 +39,21 @@ limitations under the License.
--font-size-title: calc(24px * var(--font-scale));
--font-size-headline: calc(32px * var(--font-scale));
--accent: #0dbd8b;
--accent-20: #0dbd8b33;
--alert: #ff5b55;
--alert-20: #ff5b5533;
--links: #0086e6;
--primary-content: #ffffff;
--secondary-content: #a9b2bc;
--tertiary-content: #8e99a4;
--tertiary-content-20: #8e99a433;
--quaternary-content: #6f7882;
--quinary-content: #394049;
--system: #21262c;
--background: #15191e;
--background-85: rgba(23, 25, 28, 0.85);
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
--subtle-primary: #26282d;
--cpd-color-border-accent: var(--cpd-color-green-1100);
/* These colors are needed during the transitionary period between the old and
new Compound design systems, but should be removed ASAP */
--stopgap-color-on-solid-accent: var(--cpd-color-bg-canvas-default);
--stopgap-background-85: rgba(255, 255, 255, 0.85);
--stopgap-bgColor3: #444;
/* The distance to inset non-full-width content from the edge of the window
along the inline axis */
--inline-content-inset: max(var(--cpd-space-4x), calc((100vw - 1180px) / 2));
}
.cpd-theme-dark {
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
--stopgap-background-85: rgba(16, 19, 23, 0.85);
}
@font-face {
@@ -136,8 +137,8 @@ limitations under the License.
}
body {
background-color: var(--background);
color: var(--primary-content);
background-color: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-text-primary);
color-scheme: dark;
margin: 0;
font-family: var(--font-family);
@@ -204,7 +205,7 @@ p {
}
a {
color: var(--accent);
color: var(--cpd-color-text-action-accent);
text-decoration: none;
}
@@ -216,8 +217,8 @@ a:active {
hr {
width: calc(100% - 24px);
border: none;
border-top: 1px solid var(--quinary-content);
color: var(--quaternary-content);
border-top: 1px solid var(--cpd-color-border-interactive-secondary);
color: var(--cpd-color-border-interactive-secondary);
overflow: visible;
text-align: center;
height: 5px;
@@ -243,3 +244,13 @@ details[open] > summary {
position: relative;
height: 100%;
}
/* normalize.css sets the focus rings on buttons in Firefox to an unusual custom
outline, which is inconsistent with our other components and is not sufficiently
visible to be accessible. This resets it back to 'auto'. */
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: auto;
}

View File

@@ -87,62 +87,10 @@ export class Initializer {
});
// Custom Themeing
if (import.meta.env.VITE_CUSTOM_THEME) {
const style = document.documentElement.style;
style.setProperty(
"--accent",
import.meta.env.VITE_THEME_ACCENT as string
);
style.setProperty(
"--accent-20",
import.meta.env.VITE_THEME_ACCENT_20 as string
);
style.setProperty("--alert", import.meta.env.VITE_THEME_ALERT as string);
style.setProperty(
"--alert-20",
import.meta.env.VITE_THEME_ALERT_20 as string
);
style.setProperty("--links", import.meta.env.VITE_THEME_LINKS as string);
style.setProperty(
"--primary-content",
import.meta.env.VITE_THEME_PRIMARY_CONTENT as string
);
style.setProperty(
"--secondary-content",
import.meta.env.VITE_THEME_SECONDARY_CONTENT as string
);
style.setProperty(
"--tertiary-content",
import.meta.env.VITE_THEME_TERTIARY_CONTENT as string
);
style.setProperty(
"--tertiary-content-20",
import.meta.env.VITE_THEME_TERTIARY_CONTENT_20 as string
);
style.setProperty(
"--quaternary-content",
import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string
);
style.setProperty(
"--quinary-content",
import.meta.env.VITE_THEME_QUINARY_CONTENT as string
);
style.setProperty(
"--system",
import.meta.env.VITE_THEME_SYSTEM as string
);
style.setProperty(
"--background",
import.meta.env.VITE_THEME_BACKGROUND as string
);
style.setProperty(
"--background-85",
import.meta.env.VITE_THEME_BACKGROUND_85 as string
);
style.setProperty(
"--subtle-primary",
import.meta.env.VITE_THEME_SUBTLE_PRIMARY as string
);
if (import.meta.env.VITE_CUSTOM_CSS) {
const style = document.createElement("style");
style.textContent = import.meta.env.VITE_CUSTOM_CSS;
document.head.appendChild(style);
}
// Custom fonts

View File

@@ -42,7 +42,7 @@ limitations under the License.
position: absolute;
bottom: 11px;
right: -4px;
background-color: var(--quinary-content);
background-color: var(--cpd-color-subtle-primary);
width: 20px;
height: 20px;
border-radius: 10px;
@@ -53,7 +53,7 @@ limitations under the License.
}
.removeButton {
color: var(--accent);
color: var(--cpd-color-text-action-accent);
font-size: var(--font-size-caption);
padding: 6px 0;
}

View File

@@ -35,13 +35,23 @@ interface Props extends AllHTMLAttributes<HTMLInputElement> {
id: string;
label: string;
avatarUrl: string | undefined;
userId: string;
displayName: string;
onRemoveAvatar: () => void;
}
export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
(
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
{
id,
label,
className,
avatarUrl,
userId,
displayName,
onRemoveAvatar,
...rest
},
ref
) => {
const { t } = useTranslation();
@@ -80,9 +90,10 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
<div className={classNames(styles.avatarInputField, className)}>
<div className={styles.avatarContainer}>
<Avatar
id={userId}
name={displayName}
size={Size.XL}
src={removed ? undefined : objUrl || avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
<input
id={id}

View File

@@ -42,7 +42,7 @@ limitations under the License.
.inputField {
border-radius: 4px;
transition: border-color 0.25s;
border: 1px solid var(--quinary-content);
border: 1px solid var(--cpd-color-border-interactive-primary);
}
.inputField input,
@@ -52,8 +52,8 @@ limitations under the License.
border: none;
border-radius: 4px;
padding: 12px 9px 10px 9px;
color: var(--primary-content);
background-color: var(--background);
color: var(--cpd-color-text-primary);
background-color: var(--cpd-color-bg-canvas-default);
flex: 1;
min-width: 0;
}
@@ -61,7 +61,7 @@ limitations under the License.
.inputField.disabled input,
.inputField.disabled textarea,
.inputField.disabled span {
color: var(--quaternary-content);
color: var(--cpd-color-text-disabled);
}
.inputField span {
@@ -81,13 +81,13 @@ limitations under the License.
.inputField input:placeholder-shown:focus::placeholder,
.inputField textarea:placeholder-shown:focus::placeholder {
transition: color 0.25s ease-in 0.1s;
color: var(--quaternary-content);
color: var(--cpd-color-text-placeholder);
}
.inputField label {
transition: 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(--tertiary-content);
color: var(--cpd-color-text-secondary);
background-color: transparent;
font-size: var(--font-size-body);
position: absolute;
@@ -103,7 +103,7 @@ limitations under the License.
}
.inputField:focus-within {
border-color: var(--links);
border-color: var(--cpd-color-text-link-external);
}
.inputField input:focus,
@@ -117,7 +117,7 @@ limitations under the License.
.inputField textarea:focus + label,
.inputField textarea:not(:placeholder-shown) + label,
.inputField.prefix textarea + label {
background-color: var(--system);
background-color: var(--cpd-color-bg-canvas-default);
transition: 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);
@@ -128,7 +128,7 @@ limitations under the License.
.inputField input:focus + label,
.inputField textarea:focus + label {
color: var(--links);
color: var(--cpd-color-border-focused);
}
.checkboxField {
@@ -169,11 +169,11 @@ limitations under the License.
.checkboxField.disabled,
.checkboxField.disabled .description {
color: var(--quinary-content);
color: var(--cpd-color-text-disabled);
}
.checkboxField.disabled .checkbox {
border-color: var(--quinary-content);
border-color: var(--cpd-color-border-disabled);
}
.checkbox svg {
@@ -181,12 +181,12 @@ limitations under the License.
}
.checkbox svg * {
stroke: var(--primary-content);
stroke: var(--stopgap-color-on-solid-accent);
}
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
background: var(--accent);
border-color: var(--accent);
background: var(--cpd-color-text-action-accent);
border-color: var(--cpd-color-text-action-accent);
}
.checkboxField input[type="checkbox"]:checked + label > .checkbox svg {
@@ -194,18 +194,18 @@ limitations under the License.
}
.checkboxField:focus-within .checkbox {
border: 1.5px solid var(--links) !important;
border: 1.5px solid var(--cpd-color-text-link-external) !important;
}
.errorMessage {
margin: 0;
font-size: var(--font-size-caption);
color: var(--alert);
color: var(--cpd-color-text-critical-primary);
font-weight: 600;
}
.description {
color: var(--secondary-content);
color: var(--cpd-color-text-secondary);
margin-left: 26px;
width: 100%; /* Ensure that it breaks onto the next row */
}

View File

@@ -31,11 +31,11 @@ limitations under the License.
align-items: center;
justify-content: space-between;
padding: 0 12px;
background-color: var(--background);
background-color: var(--cpd-color-bg-canvas-default);
border-radius: 8px;
border: 1px solid var(--quinary-content);
border: 1px solid var(--cpd-color-border-interactive-primary);
font-size: var(--font-size-body);
color: var(--primary-content);
color: var(--cpd-color-text-primary);
height: 40px;
max-width: 100%;
width: 100%;

View File

@@ -11,7 +11,9 @@ import {
const defaultLiveKitPublishOptions: TrackPublishDefaults = {
audioPreset: AudioPresets.music,
dtx: true,
red: true,
// disable red because the livekit server strips out red packets for clients
// that don't support it (firefox) but of course that doesn't work with e2ee.
red: false,
forceStereo: false,
simulcast: true,
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360] as VideoPreset[],

View File

@@ -32,7 +32,7 @@ import {
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room";
import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getUrlParams } from "./UrlParams";
import { getUrlParams, PASSWORD_STRING } from "./UrlParams";
import { loadOlm } from "./olm";
import { Config } from "./config/Config";
@@ -273,14 +273,15 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
export async function createRoom(
client: MatrixClient,
name: string,
ptt: boolean
ptt: boolean,
e2ee: boolean
): Promise<[string, string]> {
logger.log(`Creating room for group call`);
const createPromise = client.createRoom({
visibility: Visibility.Private,
preset: Preset.PublicChat,
name,
room_alias_name: roomAliasLocalpartFromRoomName(name),
room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name),
power_level_content_override: {
invite: 100,
kick: 100,
@@ -342,15 +343,16 @@ export async function createRoom(
return [fullAliasFromRoomName(name, client), result.room_id];
}
// Returns a URL to that will load Element Call with the given room
export function getRoomUrl(roomIdOrAlias: string): string {
if (roomIdOrAlias.startsWith("#")) {
return `${window.location.protocol}//${window.location.host}/${
roomIdOrAlias.substring(1).split(":")[0]
}`;
} else {
return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`;
}
/**
* Returns a URL to that will load Element Call with the given room
* @param roomId of the room
* @param password
* @returns
*/
export function getRoomUrl(roomId: string, password?: string): string {
return `${window.location.protocol}//${
window.location.host
}/room/#?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`;
}
export function getAvatarUrl(

View File

@@ -18,7 +18,7 @@ limitations under the License.
display: flex;
flex-direction: column;
width: 194px;
background: var(--system);
background: var(--cpd-color-bg-subtle-secondary);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 8px;
}

View File

@@ -14,15 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.e2eeLock {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 8px;
border-radius: 100%;
background-color: var(--subtle-primary);
.lock {
padding: var(--cpd-space-1x);
border-radius: var(--cpd-radius-pill-effect);
}
.lock[data-encrypted="true"] {
color: var(--cpd-color-icon-success-primary);
}
.lock[data-encrypted="false"] {
color: var(--cpd-color-icon-secondary);
}
@media (hover: hover) {
.lock:hover {
background: var(--cpd-color-bg-subtle-primary);
}
}

View File

@@ -0,0 +1,46 @@
/*
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 { Tooltip } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { ReactComponent as LockIcon } from "@vector-im/compound-design-tokens/icons/lock.svg";
import { ReactComponent as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
import styles from "./EncryptionLock.module.css";
interface Props {
encrypted: boolean;
}
export const EncryptionLock: FC<Props> = ({ encrypted }) => {
const { t } = useTranslation();
const Icon = encrypted ? LockIcon : LockOffIcon;
return (
<Tooltip
label={encrypted ? t("Encrypted") : t("Not encrypted")}
side="right"
>
<Icon
width={16}
height={16}
className={styles.lock}
data-encrypted={encrypted}
/>
</Tooltip>
);
};

View File

@@ -1,82 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu";
import { TooltipTrigger } from "../Tooltip";
export type Layout = "freedom" | "spotlight";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
}
export function GridLayoutMenu({ layout, setLayout }: Props) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Change layout"), [t]);
const onAction = useCallback(
(key: React.Key) => {
setLayout(key.toString() as Layout);
},
[setLayout]
);
const onClose = useCallback(() => {}, []);
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={tooltip}>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu
{...props}
label={t("Grid layout menu")}
onAction={onAction}
onClose={onClose}
>
<Item key="freedom" textValue={t("Freedom")}>
<FreedomIcon />
<span>Freedom</span>
{layout === "freedom" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
<Item key="spotlight" textValue={t("Spotlight")}>
<SpotlightIcon />
<span>Spotlight</span>
{layout === "spotlight" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
</Menu>
)}
</PopoverMenuTrigger>
);
}

View File

@@ -15,7 +15,7 @@ limitations under the License.
*/
.inspector {
background-color: var(--system);
background-color: var(--cpd-color-bg-subtle-secondary);
}
.scrollContainer {
@@ -36,6 +36,6 @@ limitations under the License.
.sequenceDiagramViewer :global(.messageText) {
font-size: var(--font-size-caption);
fill: var(--primary-content) !important;
stroke: var(--primary-content) !important;
fill: var(--cpd-color-text-primary) !important;
stroke: var(--cpd-color-text-primary) !important;
}

View File

@@ -21,6 +21,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Room } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
@@ -32,13 +33,22 @@ import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { E2EEConfig } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
import { Config } from "../config/Config";
import { MuteStates, useMuteStates } from "./MuteStates";
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
import {
useManageRoomSharedKey,
useIsRoomE2EE,
} from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useModalTriggerState } from "../Modal";
import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal";
declare global {
interface Window {
@@ -63,15 +73,11 @@ export function GroupCallView({
hideHeader,
groupCall,
}: Props) {
const {
state,
error,
enter,
leave,
participants,
unencryptedEventsFromUsers,
otelGroupCallMembership,
} = useGroupCall(groupCall, client);
const { state, error, enter, leave, participants, otelGroupCallMembership } =
useGroupCall(groupCall, client);
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
const { t } = useTranslation();
@@ -83,15 +89,39 @@ export function GroupCallView({
}, [groupCall]);
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(groupCall.room);
const roomAvatar = useRoomAvatar(groupCall.room);
const roomEncrypted = useIsRoomE2EE(groupCall.room.roomId)!;
const matrixInfo = useMemo((): MatrixInfo => {
return {
userId: client.getUserId()!,
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: groupCall.room.roomId,
roomName: groupCall.room.name,
roomName,
roomAlias: groupCall.room.getCanonicalAlias(),
roomAvatar,
roomEncrypted,
};
}, [displayName, avatarUrl, groupCall]);
}, [
displayName,
avatarUrl,
groupCall,
roomName,
roomAvatar,
roomEncrypted,
client,
]);
const participatingMembers = useMemo(() => {
const members: RoomMember[] = [];
for (const [member, deviceMap] of participants.entries()) {
// Count each member only once, regardless of how many devices they use
if (deviceMap.size > 0) members.push(member);
}
return members;
}, [participants]);
const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices>();
@@ -241,8 +271,11 @@ export function GroupCallView({
}
}, [groupCall, state, leave]);
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
undefined
const [e2eeEnabled] = useEnableE2EE();
const e2eeConfig = useMemo(
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
[e2eeSharedKey]
);
const onReconnect = useCallback(() => {
@@ -251,12 +284,43 @@ export function GroupCallView({
groupCall.enter();
}, [groupCall]);
const joinRule = useJoinRule(groupCall.room);
const { modalState: shareModalState, modalProps: shareModalProps } =
useModalTriggerState();
const onShareClickFn = useCallback(
() => shareModalState.open(),
[shareModalState]
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
return (
<ErrorView
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."
)
}
/>
);
}
if (!e2eeEnabled && isRoomE2EE) {
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
}
const livekitServiceURL =
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
if (!livekitServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
const shareModal = shareModalState.isOpen && (
<ShareModal roomId={groupCall.room.roomId} {...shareModalProps} />
);
if (error) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
@@ -266,16 +330,19 @@ export function GroupCallView({
groupCall={groupCall}
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
>
{shareModal}
<ActiveCall
client={client}
matrixInfo={matrixInfo}
groupCall={groupCall}
participants={participants}
participatingMembers={participatingMembers}
onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
muteStates={muteStates}
e2eeConfig={e2eeConfig}
otelGroupCallMembership={otelGroupCallMembership}
onShareClick={onShareClick}
/>
</OpenIDLoader>
);
@@ -316,16 +383,19 @@ export function GroupCallView({
);
} else {
return (
<LobbyView
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={(e2eeConfig?: E2EEConfig) => {
setE2EEConfig(e2eeConfig);
enter();
}}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>
<>
{shareModal}
<LobbyView
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => enter()}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
participatingMembers={participatingMembers}
onShareClick={onShareClick}
/>
</>
);
}
}

View File

@@ -49,47 +49,52 @@ limitations under the License.
left: 0;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-areas: "logo buttons layout";
align-items: center;
padding: var(--footerPadding) 0;
/* TODO: Un-hardcode these colors */
gap: var(--cpd-space-3x);
padding: var(--footerPadding) var(--inline-content-inset);
background: linear-gradient(
360deg,
#15191e 0%,
rgba(21, 25, 30, 0.9) 37%,
rgba(21, 25, 30, 0.8) 49.68%,
rgba(21, 25, 30, 0.7) 56.68%,
rgba(21, 25, 30, 0.427397) 72.92%,
rgba(21, 25, 30, 0.257534) 81.06%,
rgba(21, 25, 30, 0.136986) 87.29%,
rgba(21, 25, 30, 0.0658079) 92.4%,
rgba(21, 25, 30, 0) 100%
180deg,
rgba(0, 0, 0, 0) 0%,
var(--cpd-color-bg-canvas-default) 100%
);
}
.footer > * {
margin-right: 30px;
.logo {
grid-area: logo;
justify-self: start;
display: flex;
align-items: center;
gap: var(--cpd-space-2x);
padding-inline-start: var(--cpd-space-1x);
}
.footer > :last-child {
margin-right: 0px;
.buttons {
grid-area: buttons;
display: flex;
gap: var(--cpd-space-3x);
}
.maximised .footer {
position: absolute;
width: 100%;
bottom: 0;
.layout {
grid-area: layout;
justify-self: end;
}
@media (min-height: 300px) {
.inRoom {
--footerPadding: 24px;
--footerPadding: 40px;
}
}
@media (min-width: 800px) {
.inRoom {
--footerPadding: 32px;
--footerPadding: 60px;
}
.buttons {
gap: var(--cpd-space-4x);
}
}

View File

@@ -23,7 +23,6 @@ import {
useTracks,
} from "@livekit/components-react";
import { usePreventScroll } from "@react-aria/overlays";
import classNames from "classnames";
import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@@ -32,10 +31,11 @@ import { Ref, useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger";
import { RoomEventCallbacks } from "livekit-client/dist/src/room/Room";
import { ReactComponent as LogoMark } from "../icons/LogoMark.svg";
import { ReactComponent as LogoType } from "../icons/LogoType.svg";
import type { IWidgetApiRequest } from "matrix-widget-api";
import {
HangupButton,
@@ -43,15 +43,8 @@ import {
VideoButton,
ScreenshareButton,
SettingsButton,
InviteButton,
} from "../button";
import {
Header,
LeftNav,
RightNav,
RoomHeaderInfo,
VersionMismatchWarning,
} from "../Header";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import {
useVideoGridLayout,
TileDescriptor,
@@ -67,27 +60,26 @@ import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css";
import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader";
import { E2EELock } from "../E2EELock";
import { useEventEmitterThree } from "../useEvents";
import { useWakeLock } from "../useWakeLock";
import { useMergedRefs } from "../useMergedRefs";
import { MuteStates } from "./MuteStates";
import { MatrixInfo } from "./VideoPreview";
import { ShareButton } from "../button/ShareButton";
import { LayoutToggle } from "./LayoutToggle";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -120,26 +112,30 @@ export function ActiveCall(props: ActiveCallProps) {
export interface InCallViewProps {
client: MatrixClient;
matrixInfo: MatrixInfo;
groupCall: GroupCall;
livekitRoom: Room;
muteStates: MuteStates;
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
participatingMembers: RoomMember[];
onLeave: (error?: Error) => void;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
onShareClick: (() => void) | null;
}
export function InCallView({
client,
matrixInfo,
groupCall,
livekitRoom,
muteStates,
participants,
participatingMembers,
onLeave,
unencryptedEventsFromUsers,
hideHeader,
otelGroupCallMembership,
onShareClick,
}: InCallViewProps) {
const { t } = useTranslation();
usePreventScroll();
@@ -179,15 +175,13 @@ export function InCallView({
[muteStates]
);
const joinRule = useJoinRule(groupCall.room);
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
useCallViewKeyboardShortcuts(
containerRef1,
toggleMicrophone,
toggleCamera,
async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
(muted) => muteStates.audio.setEnabled?.(!muted)
);
const onDisconnected = useCallback(
@@ -213,7 +207,7 @@ export function InCallView({
useEffect(() => {
widget?.api.transport.send(
layout === "freedom"
layout === "grid"
? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout,
{}
@@ -223,7 +217,7 @@ export function InCallView({
useEffect(() => {
if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("freedom");
setLayout("grid");
await widget!.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
@@ -247,7 +241,8 @@ export function InCallView({
}
}, [setLayout]);
const reducedControls = boundsValid && bounds.width <= 400;
const mobile = boundsValid && bounds.width <= 660;
const reducedControls = boundsValid && bounds.width <= 340;
const noControls = reducedControls && bounds.height <= 400;
const items = useParticipantTiles(livekitRoom, participants);
@@ -267,7 +262,7 @@ export function InCallView({
);
const Grid =
items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid;
items.length > 12 && layout === "grid" ? NewVideoGrid : VideoGrid;
const prefersReducedMotion = usePrefersReducedMotion();
@@ -341,25 +336,6 @@ export function InCallView({
settingsModalState.open();
}, [settingsModalState]);
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openInvite = useCallback(() => {
inviteModalState.open();
}, [inviteModalState]);
const containerClasses = classNames(styles.inRoom, {
[styles.maximised]: undefined,
});
const toggleScreensharing = useCallback(async () => {
exitFullscreen();
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
@@ -378,19 +354,19 @@ export function InCallView({
const buttons: JSX.Element[] = [];
buttons.push(
<MicButton
key="1"
muted={!muteStates.audio.enabled}
onPress={toggleMicrophone}
disabled={muteStates.audio.setEnabled === null}
data-testid="incall_mute"
/>,
<VideoButton
key="2"
muted={!muteStates.video.enabled}
onPress={toggleCamera}
disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute"
/>,
<MicButton
key="1"
muted={!muteStates.audio.enabled}
onPress={toggleMicrophone}
disabled={muteStates.audio.setEnabled === null}
data-testid="incall_mute"
/>
);
@@ -411,25 +387,43 @@ export function InCallView({
buttons.push(
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
);
footer = <div className={styles.footer}>{buttons}</div>;
footer = (
<div className={styles.footer}>
{!mobile && !hideHeader && (
<div className={styles.logo}>
<LogoMark width={24} height={24} />
<LogoType width={80} height={11} />
</div>
)}
<div className={styles.buttons}>{buttons}</div>
{!mobile && !hideHeader && (
<LayoutToggle
className={styles.layout}
layout={layout}
setLayout={setLayout}
/>
)}
</div>
);
}
return (
<div className={containerClasses} ref={containerRef}>
<div className={styles.inRoom} ref={containerRef}>
{!hideHeader && maximisedParticipant === null && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={groupCall.room.name} />
<VersionMismatchWarning
users={unencryptedEventsFromUsers}
room={groupCall.room}
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
/>
<E2EELock />
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
{joinRule === JoinRule.Public && (
<InviteButton variant="icon" onClick={openInvite} />
{!reducedControls && onShareClick !== null && (
<ShareButton onClick={onShareClick} />
)}
</RightNav>
</Header>
@@ -460,14 +454,6 @@ export function InCallView({
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && (
<InviteModal
roomIdOrAlias={
groupCall.room.getCanonicalAlias() ?? groupCall.room.roomId
}
{...inviteModalProps}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,77 @@
/*
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.
*/
.toggle {
padding: 2px;
border: 1px solid var(--cpd-color-border-interactive-secondary);
border-radius: var(--cpd-radius-pill-effect);
box-shadow: 0px 0px 40px 0px rgba(0, 0, 0, 0.5);
display: flex;
}
.toggle input {
appearance: none;
outline: none !important;
}
.toggle label {
display: block;
padding: calc(2.5 * var(--cpd-space-1x));
cursor: pointer;
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-primary);
background: var(--cpd-color-bg-action-secondary-rest);
box-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
}
@media (hover: hover) {
.toggle label:hover {
background: var(--cpd-color-bg-action-secondary-hovered);
box-shadow: none;
}
}
.toggle label:active {
background: var(--cpd-color-bg-action-secondary-hovered);
box-shadow: none;
}
.toggle input:checked + label {
color: var(--cpd-color-icon-on-solid-primary);
background: var(--cpd-color-bg-action-primary-rest);
}
@media (hover: hover) {
.toggle input:checked + label:hover {
background: var(--cpd-color-bg-action-primary-hovered);
}
}
.toggle input:checked + label:active {
background: var(--cpd-color-bg-action-primary-hovered);
}
.toggle label > svg {
display: block;
}
.toggle label:last-child {
margin-inline-start: 5px;
}
.toggle input:focus-visible + label {
outline: auto;
}

75
src/room/LayoutToggle.tsx Normal file
View File

@@ -0,0 +1,75 @@
/*
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 { ChangeEvent, FC, useCallback, useId } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "@vector-im/compound-web";
import { ReactComponent as SpotlightViewIcon } from "@vector-im/compound-design-tokens/icons/spotlight-view.svg";
import { ReactComponent as GridViewIcon } from "@vector-im/compound-design-tokens/icons/grid-view.svg";
import classNames from "classnames";
import styles from "./LayoutToggle.module.css";
export type Layout = "spotlight" | "grid";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
className?: string;
}
export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
const { t } = useTranslation();
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
[setLayout]
);
const spotlightId = useId();
const gridId = useId();
return (
<div className={classNames(styles.toggle, className)}>
<input
id={spotlightId}
type="radio"
name="layout"
value="spotlight"
checked={layout === "spotlight"}
onChange={onChange}
/>
<Tooltip label={t("Spotlight")}>
<label htmlFor={spotlightId}>
<SpotlightViewIcon aria-label={t("Spotlight")} />
</label>
</Tooltip>
<input
id={gridId}
type="radio"
name="layout"
value="grid"
checked={layout === "grid"}
onChange={onChange}
/>
<Tooltip label={t("Grid")}>
<label htmlFor={gridId}>
<GridViewIcon aria-label={t("Grid")} />
</label>
</Tooltip>
</div>
);
};

View File

@@ -14,49 +14,46 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
useRef,
useEffect,
useState,
useCallback,
ChangeEvent,
FC,
} from "react";
import { useRef, useEffect, FC } from "react";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { getRoomUrl } from "../matrix-utils";
import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { E2EEConfig } from "../livekit/useLiveKit";
import { InputField } from "../input/Input";
import { useEnableE2EE } from "../settings/useSetting";
import { MuteStates } from "./MuteStates";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { ShareButton } from "../button/ShareButton";
interface Props {
client: MatrixClient;
matrixInfo: MatrixInfo;
muteStates: MuteStates;
onEnter: (e2eeConfig?: E2EEConfig) => void;
onEnter: () => void;
isEmbedded: boolean;
hideHeader: boolean;
participatingMembers: RoomMember[];
onShareClick: (() => void) | null;
}
export const LobbyView: FC<Props> = ({
client,
matrixInfo,
muteStates,
onEnter,
isEmbedded,
hideHeader,
participatingMembers,
onShareClick,
}) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
useLocationNavigation();
const [enableE2EE] = useEnableE2EE();
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (joinCallButtonRef.current) {
@@ -64,52 +61,34 @@ export const LobbyView: FC<Props> = ({
}
}, [joinCallButtonRef]);
const [e2eeSharedKey, setE2EESharedKey] = useState<string | undefined>(
undefined
);
const onE2EESharedKeyChanged = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setE2EESharedKey(value === "" ? undefined : value);
},
[setE2EESharedKey]
);
return (
<div className={styles.room}>
{!hideHeader && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={matrixInfo.roomName} />
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.roomEncrypted}
participants={participatingMembers}
client={client}
/>
</LeftNav>
<RightNav>
<UserMenuContainer />
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
</RightNav>
</Header>
)}
<div className={styles.joinRoom}>
<div className={styles.joinRoomContent}>
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates} />
{enableE2EE && (
<InputField
className={styles.passwordField}
label={t("Password (if none, E2EE is disabled)")}
type="text"
onChange={onE2EESharedKeyChanged}
value={e2eeSharedKey}
/>
)}
<Trans>
<Button
ref={joinCallButtonRef}
className={styles.copyButton}
size="lg"
onPress={() =>
onEnter(
e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined
)
}
onPress={() => onEnter()}
data-testid="lobby_joinCall"
>
Join call now
@@ -117,7 +96,7 @@ export const LobbyView: FC<Props> = ({
<Body>Or</Body>
<CopyButton
variant="secondaryCopy"
value={getRoomUrl(matrixInfo.roomAlias ?? matrixInfo.roomId)}
value={getRoomUrl(matrixInfo.roomId, roomSharedKey ?? undefined)}
className={styles.copyButton}
copiedMessage={t("Call link copied")}
data-testid="lobby_inviteLink"

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,18 +20,20 @@ import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props extends Omit<ModalProps, "title" | "children"> {
roomIdOrAlias: string;
roomId: string;
}
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(roomId);
return (
<Modal
title={t("Invite people")}
title={t("Share this call")}
isDismissable
className={styles.inviteModal}
{...rest}
@@ -40,7 +42,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
data-testid="modal_inviteLink"
/>
</ModalContent>

View File

@@ -20,7 +20,7 @@ limitations under the License.
height: 50vh;
border-radius: 24px;
overflow: hidden;
background-color: var(--bgColor3);
background-color: var(--stopgap-bgColor3);
margin: 20px;
}
@@ -41,7 +41,7 @@ limitations under the License.
display: flex;
justify-content: center;
align-items: center;
background-color: var(--bgColor3);
background-color: var(--stopgap-bgColor3);
}
.cameraPermissions {
@@ -62,7 +62,7 @@ limitations under the License.
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(23, 25, 28, 0.9);
background-color: var(--stopgap-background-85);
}
.previewButtons > * {

View File

@@ -35,11 +35,14 @@ import { useMediaDevices } from "../livekit/MediaDevicesContext";
import { MuteStates } from "./MuteStates";
export type MatrixInfo = {
userId: string;
displayName: string;
avatarUrl: string;
roomId: string;
roomName: string;
roomAlias: string | null;
roomAvatar: string | null;
roomEncrypted: boolean;
};
interface Props {
@@ -68,6 +71,9 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
const devices = useMediaDevices();
// Capture the audio options as they were when we first mounted, because
// we're not doing anything with the audio anyway so we don't need to
// re-open the devices when they change (see below).
const initialAudioOptions = useRef<CreateLocalTracksOptions["audio"]>();
initialAudioOptions.current ??= muteStates.audio.enabled && {
deviceId: devices.audioInput.selectedId,
@@ -79,7 +85,9 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
// request over with at the same time. But changing the audio settings
// shouldn't cause this hook to recreate the track, which is why we
// reference the initial values here.
audio: initialAudioOptions.current,
// We also pass in a clone because livekit mutates the object passed in,
// which would cause the devices to be re-opened on the next render.
audio: Object.assign({}, initialAudioOptions.current),
video: muteStates.video.enabled && {
deviceId: devices.videoInput.selectedId,
},
@@ -124,23 +132,24 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
{!muteStates.video.enabled && (
<div className={styles.avatarContainer}>
<Avatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={(previewBounds.height - 66) / 2}
src={matrixInfo.avatarUrl}
fallback={matrixInfo.displayName.slice(0, 1).toUpperCase()}
/>
</div>
)}
<div className={styles.previewButtons}>
<MicButton
muted={!muteStates.audio.enabled}
onPress={onAudioPress}
disabled={muteStates.audio.setEnabled === null}
/>
<VideoButton
muted={!muteStates.video.enabled}
onPress={onVideoPress}
disabled={muteStates.video.setEnabled === null}
/>
<MicButton
muted={!muteStates.audio.enabled}
onPress={onAudioPress}
disabled={muteStates.audio.setEnabled === null}
/>
<SettingsButton onPress={openSettings} />
</div>
</>

View File

@@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useReducer, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import * as Sentry from "@sentry/react";
import {
GroupCallEvent,
GroupCallState,
GroupCall,
GroupCallErrorCode,
GroupCallUnknownDeviceError,
GroupCallError,
GroupCallStatsReportEvent,
GroupCallStatsReport,
@@ -77,7 +75,6 @@ interface UseGroupCallReturnType {
screenshareFeeds: CallFeed[];
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>;
otelGroupCallMembership?: OTelGroupCallMembership;
}
@@ -188,13 +185,6 @@ export function useGroupCall(
}
}
const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer(
(state: Set<string>, newVal: string) => {
return new Set(state).add(newVal);
},
new Set<string>()
);
const updateState = useCallback(
(state: Partial<State>) => setState((prev) => ({ ...prev, ...state })),
[setState]
@@ -333,10 +323,6 @@ export function useGroupCall(
function onError(e: GroupCallError): void {
Sentry.captureException(e);
if (e.code === GroupCallErrorCode.UnknownDevice) {
const unknownDeviceError = e as GroupCallUnknownDeviceError;
addUnencryptedEventUser(unknownDeviceError.userId);
}
}
function onConnectionStatsReport(
@@ -638,7 +624,6 @@ export function useGroupCall(
screenshareFeeds,
participants,
hasLocalParticipant,
unencryptedEventsFromUsers,
otelGroupCallMembership: groupCallOTelMembership,
};
}

View File

@@ -25,12 +25,16 @@ import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { SyncState } from "matrix-js-sdk/src/sync";
import { useTranslation } from "react-i18next";
import { randomString } from "matrix-js-sdk/src/randomstring";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { setLocalStorageItem } from "../useLocalStorage";
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
import { translatedError } from "../TranslatedError";
import { widget } from "../widget";
import { useEnableE2EE } from "../settings/useSetting";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
@@ -67,6 +71,8 @@ export const useLoadGroupCall = (
const { t } = useTranslation();
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
const [e2eeEnabled] = useEnableE2EE();
useEffect(() => {
const fetchOrCreateRoom = async (): Promise<Room> => {
try {
@@ -104,8 +110,17 @@ export const useLoadGroupCall = (
const [, roomId] = await createRoom(
client,
roomNameFromRoomId(roomIdOrAlias),
createPtt
createPtt,
e2eeEnabled ?? false
);
if (e2eeEnabled) {
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(roomId),
randomString(32)
);
}
// likewise, wait for the room
await client.waitUntilRoomReadyForGroupCalls(roomId);
return client.getRoom(roomId)!;
@@ -194,7 +209,7 @@ export const useLoadGroupCall = (
.then(fetchOrCreateGroupCall)
.then((groupCall) => setState({ kind: "loaded", groupCall }))
.catch((error) => setState({ kind: "failed", error }));
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
return state;
};

View File

@@ -1,11 +1,11 @@
/*
Copyright 2022 New Vector Ltd
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
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,
@@ -14,29 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.facepile {
width: 100%;
position: relative;
}
import { Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { useState } from "react";
.facepile.xs {
height: 24px;
}
import { useTypedEventEmitter } from "../useEvents";
.facepile.sm {
height: 32px;
}
.facepile.md {
height: 36px;
}
.facepile .avatar {
position: absolute;
top: 0;
border: 1px solid var(--system);
}
.facepile.md .avatar {
border-width: 2px;
export function useRoomName(room: Room): string {
const [, setNumUpdates] = useState(0);
// Whenever the name changes, force an update
useTypedEventEmitter(room, RoomEvent.Name, () => setNumUpdates((n) => n + 1));
return room.name;
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
@@ -29,6 +29,7 @@ interface Props {
export function ProfileSettingsTab({ client }: Props) {
const { t } = useTranslation();
const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
const userId = useMemo(() => client.getUserId(), [client]);
const formRef = useRef<HTMLFormElement | null>(null);
@@ -77,12 +78,13 @@ export function ProfileSettingsTab({ client }: Props) {
return (
<form onChange={onFormChange} ref={formRef} className={styles.content}>
<FieldRow className={styles.avatarFieldRow}>
{displayName && (
{userId && displayName && (
<AvatarInputField
id="avatar"
name="avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
userId={userId}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>

View File

@@ -20,7 +20,7 @@ limitations under the License.
}
.settingsModal p {
color: var(--secondary-content);
color: var(--cpd-color-text-secondary);
}
.tabContainer {

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,59 +14,49 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { useMemo, useState, useEffect, useCallback } from "react";
import { useCallback, useMemo } from "react";
import { isE2EESupported } from "livekit-client";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import {
getLocalStorageItem,
setLocalStorageItem,
useLocalStorage,
} from "../useLocalStorage";
type Setting<T> = [T, (value: T) => void];
type DisableableSetting<T> = [T, ((value: T) => void) | null];
// Bus to notify other useSetting consumers when a setting is changed
export const settingsBus = new EventEmitter();
const getSettingKey = (name: string): string => {
export const getSettingKey = (name: string): string => {
return `matrix-setting-${name}`;
};
// Like useState, but reads from and persists the value to localStorage
const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
export const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
const key = useMemo(() => getSettingKey(name), [name]);
const [value, setValue] = useState<T>(() => {
const item = localStorage.getItem(key);
return item == null ? defaultValue : JSON.parse(item);
});
const [item, setItem] = useLocalStorage(key);
useEffect(() => {
settingsBus.on(name, setValue);
return () => {
settingsBus.off(name, setValue);
};
}, [name, setValue]);
const value = useMemo(
() => (item == null ? defaultValue : JSON.parse(item)),
[item, defaultValue]
);
const setValue = useCallback(
(value: T) => {
setItem(JSON.stringify(value));
},
[setItem]
);
return [
value,
useCallback(
(newValue: T) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
settingsBus.emit(name, newValue);
},
[name, key, setValue]
),
];
return [value, setValue];
};
export const getSetting = <T>(name: string, defaultValue: T): T => {
const item = localStorage.getItem(getSettingKey(name));
const item = getLocalStorageItem(getSettingKey(name));
return item === null ? defaultValue : JSON.parse(item);
};
export const setSetting = <T>(name: string, newValue: T) => {
localStorage.setItem(getSettingKey(name), JSON.stringify(newValue));
settingsBus.emit(name, newValue);
};
export const setSetting = <T>(name: string, newValue: T) =>
setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue));
const canEnableSpatialAudio = () => {
const { userAgent } = navigator;
@@ -102,7 +92,7 @@ export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
export const useEnableE2EE = (): DisableableSetting<boolean | null> => {
const settingVal = useSetting<boolean | null>(
"enable-end-to-end-encryption",
false
true
);
if (isE2EESupported()) return settingVal;

View File

@@ -44,12 +44,12 @@ limitations under the License.
}
.tab > * {
color: var(--secondary-content);
color: var(--cpd-color-text-secondary);
margin: 0 8px 0 0;
}
.tab svg * {
fill: var(--secondary-content);
fill: var(--cpd-color-text-secondary);
}
.tab > :last-child {
@@ -57,15 +57,15 @@ limitations under the License.
}
.tab.selected {
background-color: var(--accent);
background-color: var(--cpd-color-text-action-accent);
}
.tab.selected * {
color: var(--primary-content);
color: var(--stopgap-color-on-solid-accent);
}
.tab.selected svg * {
fill: var(--primary-content);
fill: var(--stopgap-color-on-solid-accent);
}
.tab.disabled {

View File

@@ -37,7 +37,7 @@ limitations under the License.
}
.link {
color: var(--links);
color: var(--cpd-color-text-link-external);
}
.link:hover {
@@ -46,7 +46,7 @@ limitations under the License.
}
.primary {
color: var(--accent);
color: var(--cpd-color-text-action-accent);
}
.overflowEllipsis {

59
src/useLocalStorage.ts Normal file
View File

@@ -0,0 +1,59 @@
/*
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 EventEmitter from "events";
import { useCallback, useEffect, useState } from "react";
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
// Bus to notify other useLocalStorage consumers when an item is changed
export const localStorageBus = new EventEmitter();
// Like useState, but reads from and persists the value to localStorage
export const useLocalStorage = (
key: string
): [LocalStorageItem, (value: string) => void] => {
const [value, setValue] = useState<LocalStorageItem>(() =>
localStorage.getItem(key)
);
useEffect(() => {
localStorageBus.on(key, setValue);
return () => {
localStorageBus.off(key, setValue);
};
}, [key, setValue]);
return [
value,
useCallback(
(newValue: string) => {
setValue(newValue);
localStorage.setItem(key, newValue);
localStorageBus.emit(key, newValue);
},
[key, setValue]
),
];
};
export const getLocalStorageItem = (key: string): LocalStorageItem =>
localStorage.getItem(key);
export const setLocalStorageItem = (key: string, value: string): void => {
localStorage.setItem(key, value);
localStorageBus.emit(key, value);
};

37
src/useMediaQuery.ts Normal file
View File

@@ -0,0 +1,37 @@
/*
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 { useCallback, useMemo, useState } from "react";
import { useEventTarget } from "./useEvents";
/**
* React hook that tracks whether the given media query matches.
*/
export const useMediaQuery = (query: string) => {
const mediaQuery = useMemo(() => matchMedia(query), [query]);
const [numChanges, setNumChanges] = useState(0);
useEventTarget(
mediaQuery,
"change",
useCallback(() => setNumChanges((n) => n + 1), [setNumChanges])
);
// We want any change to the update counter to trigger an update here
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]);
};

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,29 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useRef, useState } from "react";
import { useEventTarget } from "./useEvents";
import { useMediaQuery } from "./useMediaQuery";
/**
* @returns Whether the user has requested reduced motion.
*/
export const usePrefersReducedMotion = () => {
const mediaQuery = useRef<MediaQueryList>();
if (mediaQuery.current === undefined)
mediaQuery.current = matchMedia("(prefers-reduced-motion)");
const [prefersReducedMotion, setPrefersReducedMotion] = useState(
mediaQuery.current.matches
);
useEventTarget(
mediaQuery.current!,
"change",
useCallback(
() => setPrefersReducedMotion(mediaQuery.current!.matches),
[setPrefersReducedMotion]
)
);
return prefersReducedMotion;
};
export const usePrefersReducedMotion = () =>
useMediaQuery("(prefers-reduced-motion)");

View File

@@ -18,7 +18,9 @@ limitations under the License.
contain: strict;
position: relative;
flex-grow: 1;
padding: 0 20px var(--footerHeight);
margin-inline: var(--inline-content-inset);
padding-block-end: var(--footerHeight);
margin-block-start: var(--cpd-space-4x);
overflow-y: auto;
overflow-x: hidden;
}
@@ -30,9 +32,3 @@ limitations under the License.
.slot {
contain: strict;
}
@media (min-width: 800px) {
.grid {
padding: 0 22px var(--footerHeight);
}
}

View File

@@ -40,7 +40,7 @@ import useMeasure from "react-use-measure";
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
import styles from "./VideoGrid.module.css";
import { Layout } from "../room/GridLayoutMenu";
import { Layout } from "../room/LayoutToggle";
import { TileWrapper } from "./TileWrapper";
import { LayoutStatesMap } from "./Layout";
@@ -81,8 +81,8 @@ export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
layout: Layout;
setLayout: (layout: Layout) => void;
} {
const layoutRef = useRef<Layout>("freedom");
const revertLayoutRef = useRef<Layout>("freedom");
const layoutRef = useRef<Layout>("grid");
const revertLayoutRef = useRef<Layout>("grid");
const prevHasScreenshareFeeds = useRef(hasScreenshareFeeds);
const [, forceUpdate] = useState({});
@@ -151,7 +151,7 @@ function getTilePositions(
pipYRatio: number,
layout: Layout
): TilePosition[] {
if (layout === "freedom") {
if (layout === "grid") {
if (tileCount === 2 && focusedTileCount === 0) {
return getOneOnOneLayoutTilePositions(
gridWidth,
@@ -700,7 +700,7 @@ function displayedTileCount(
gridHeight: number
): number {
let displayedTile = -1;
if (layout === "freedom") {
if (layout === "grid") {
return displayedTile;
}
if (tileCount < 2) {
@@ -734,7 +734,7 @@ function reorderTiles<T>(tiles: Tile<T>[], layout: Layout, displayedTile = -1) {
// can assign multiple remote tiles order '1' and this persists through
// subsequent reorders because we preserve the order of the tiles.
if (
layout === "freedom" &&
layout === "grid" &&
tiles.length === 2 &&
tiles.filter((t) => t.item.local).length === 1 &&
!tiles.some((t) => t.focused)
@@ -1143,7 +1143,7 @@ export function VideoGrid<T>({
lastTappedRef.current[tileKey] = 0;
const tile = tiles.find((tile) => tile.key === tileKey);
if (!tile || layout !== "freedom") return;
if (!tile || layout !== "grid") return;
const item = tile.item;
setTileState(({ tiles, ...state }) => {
@@ -1216,7 +1216,7 @@ export function VideoGrid<T>({
return;
}
if (layout !== "freedom") return;
if (layout !== "grid") return;
const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
const dragTile = tiles[dragTileIndex];
@@ -1392,5 +1392,5 @@ export function VideoGrid<T>({
}
VideoGrid.defaultProps = {
layout: "freedom",
layout: "grid",
};

View File

@@ -53,7 +53,7 @@ limitations under the License.
bottom: -1px;
content: "";
border-radius: var(--tileRadius);
box-shadow: inset 0 0 0 4px var(--accent) !important;
box-shadow: inset 0 0 0 4px var(--cpd-color-border-accent) !important;
opacity: 0;
transition: opacity ease 0.15s;
}
@@ -77,8 +77,8 @@ limitations under the License.
position: absolute;
height: 24px;
padding: 0 8px;
color: var(--primary-content);
background-color: var(--background-85);
color: var(--cpd-color-text-primary);
background-color: var(--stopgap-background-85);
display: flex;
align-items: center;
justify-content: center;
@@ -95,6 +95,10 @@ limitations under the License.
margin-right: 4px;
}
.infoBubble > svg * {
fill: var(--cpd-color-icon-primary);
}
.toolbar {
position: absolute;
top: 0;
@@ -103,8 +107,8 @@ limitations under the License.
width: 100%;
height: 42px;
color: var(--primary-content);
background-color: var(--background-85);
color: var(--cpd-color-text-primary);
background-color: var(--stopgap-background-85);
display: flex;
align-items: center;
@@ -154,7 +158,7 @@ limitations under the License.
.videoMutedOverlay {
width: 100%;
height: 100%;
background-color: #21262c;
background-color: var(--cpd-color-bg-subtle-secondary);
}
.presenterLabel {
@@ -162,7 +166,7 @@ limitations under the License.
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #17191c;
background-color: var(--stopgap-background-85);
border-radius: 4px;
display: flex;
justify-content: center;

View File

@@ -169,9 +169,10 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
<div className={styles.videoMutedOverlay} />
<Avatar
key={member?.userId}
id={member?.userId ?? displayName}
name={displayName}
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
src={member?.getMxcAvatarUrl()}
fallback={displayName.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
</>

View File

@@ -40,9 +40,9 @@ limitations under the License.
appearance: none;
background-color: transparent;
--slider-color: var(--quinary-content);
--slider-color: var(--cpd-color-bg-subtle-primary);
--slider-height: 4px;
--thumb-color: var(--accent);
--thumb-color: var(--cpd-color-text-action-accent);
--thumb-radius: 100%;
--thumb-size: 16px;
--thumb-margin-top: -6px;

View File

@@ -147,7 +147,7 @@ export const widget: WidgetHelpers | null = (() => {
receiveState,
sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice,
turnServers: true,
turnServers: false,
},
roomId,
{

18
test/environment.ts Normal file
View File

@@ -0,0 +1,18 @@
import { TextEncoder } from "util";
import JSDOMEnvironment_, {
TestEnvironment as TestEnvironment_,
} from "jest-environment-jsdom";
import { JestEnvironmentConfig, EnvironmentContext } from "@jest/environment";
// This is a patched version of jsdom that adds TextEncoder, as a workaround for
// https://github.com/jsdom/jsdom/issues/2524
// Once that issue is resolved, this custom environment file can be deleted
export default class JSDOMEnvironment extends JSDOMEnvironment_ {
constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
super(config, context);
this.global.TextEncoder ??= TextEncoder;
}
}
export const TestEnvironment =
TestEnvironment_ === JSDOMEnvironment_ ? JSDOMEnvironment : TestEnvironment_;

View File

@@ -34,7 +34,13 @@ describe("CallList", () => {
it("should show room", async () => {
const rooms = [
{ roomName: "Room #1", roomAlias: "#room-name:server.org" },
{
roomName: "Room #1",
roomAlias: "#room-name:server.org",
room: {
roomId: "!roomId",
},
},
] as GroupCallRoom[];
const result = renderComponent(rooms);

View File

@@ -28,7 +28,13 @@ export default defineConfig(({ mode }) => {
const plugins = [
react(),
basicSsl(),
svgrPlugin(),
svgrPlugin({
svgrOptions: {
// This enables ref forwarding on SVGR components, which is needed, for
// example, to make tooltips on icons work
ref: true,
},
}),
htmlTemplate.default({
data: {
title: env.VITE_PRODUCT_NAME || "Element Call",

5228
yarn.lock

File diff suppressed because it is too large Load Diff