Compare commits

...

523 Commits

Author SHA1 Message Date
Enrico Schwendig
ec810cde5e Force to allow calls without video and audio in embedded mode (#1134)
* force to allow calls without video and audio in embedded mode
2023-06-29 15:29:46 +02:00
Robin
8c2229b236 Merge pull request #1163 from robintown/retry-cd
Empty commit to retry CD
2023-06-28 09:55:54 -04:00
Michael Kaye
beeb418496 Merge pull request #1143 from vector-im/michaelkaye/testy_on_main
Run tests for codecoverage after push to main
2023-06-27 08:57:09 +01:00
Michael Kaye
0dc362a5dc primary branch is now livekit, not main 2023-06-26 19:28:11 +01:00
Robin Townsend
5b9cace410 Empty commit to retry CD 2023-06-26 14:08:52 -04:00
Robin
2981a9ddd8 Merge pull request #1138 from robintown/update-js-sdk
Update matrix-js-sdk
2023-06-26 09:53:09 -04:00
Michael Kaye
1971c18034 Run tests, for codecoverage after push to main
This allows us to generate a regular baseline to compare upcoming push requests against.
2023-06-26 10:53:29 +01:00
Robin Townsend
eb8f6ef902 Update matrix-js-sdk 2023-06-23 15:31:31 -04:00
Robin
d2e2d3e768 Merge pull request #1137 from robintown/call-backend-full-mesh
Note the call backend in rageshake and analytics data
2023-06-23 15:13:10 -04:00
Robin Townsend
4eadfed9af Note the call backend in rageshake and analytics data 2023-06-23 15:00:15 -04:00
Michael Kaye
e446039d1f Merge pull request #1117 from vector-im/michaelk/report_coverage
Push code coverage percentages to codecov.io.
2023-06-23 12:52:41 +01:00
Michael Kaye
f64df3dcf1 Fix typo in github action config. 2023-06-22 09:18:17 +01:00
Robin
612449066d Merge pull request #1128 from robintown/fix-livekit-deployment
Use the right config for the livekit-experiment deployment
2023-06-20 16:21:55 +00:00
Robin Townsend
18bcc9ee37 Use the right config for the livekit-experiment deployment 2023-06-20 12:19:38 -04:00
Robin
c34fcfedda Merge pull request #1127 from robintown/livekit-experiment-cd
Add persistent CD for the livekit-experiment branch
2023-06-20 16:06:10 +00:00
Robin Townsend
11f8ec03bc Add persistent CD for the livekit-experiment branch
This is basically just a copy of the main branch CD - untested but is supposed to deploy to element-call-livekit.netlify.app
2023-06-20 11:46:10 -04:00
Robin
50718e47ca Merge pull request #1124 from robintown/grid-interactions
Improved large grid interactions
2023-06-20 03:55:00 +00:00
Timo
2ffe000bf5 Connection lost banner (#1101)
* connection lost banner
if there is no connection to the home server

Signed-off-by: Timo K <toger5@hotmail.de>
2023-06-19 15:36:03 +02:00
Robin Townsend
cd7ab00d80 Don't try to promote the same speaker multiple times 2023-06-18 11:45:01 -04:00
Robin Townsend
ddeb36db47 Promote speakers to the first page of the grid 2023-06-18 11:35:13 -04:00
Robin Townsend
4e5a75074a Fix tiles not collapsing toward their center 2023-06-18 01:01:24 -04:00
Robin Townsend
391ba5196c Make screenshares appear near the presenter's tile and be larger 2023-06-18 00:47:37 -04:00
Robin Townsend
3e56d0a656 Make it possible again to drag a tile into the top left corner 2023-06-18 00:28:08 -04:00
Robin Townsend
afbcea7b66 Allow the grid to resize with the window width 2023-06-17 22:31:07 -04:00
Robin Townsend
4f582c6ad7 Don't change tile size when dragging 2023-06-17 22:31:07 -04:00
Robin Townsend
8b8d6fd0e0 Push large tiles upwards back into the grid 2023-06-17 22:31:01 -04:00
Robin
cabad628b4 Merge pull request #1121 from robintown/grid-performance
Improve the performance of dragging tiles in the large grid
2023-06-16 12:55:15 -04:00
Robin Townsend
f4f454f58e Improve the performance of dragging tiles in the large grid
By only updating the one spring of the tile that's being interacted with
2023-06-16 10:20:24 -04:00
Michael Kaye
97a6313e03 Push code coverage percentages to codecov.io. 2023-06-15 15:23:10 +01:00
Robin
5510719fb2 Merge pull request #1111 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-06-14 11:42:45 -04:00
Robin Townsend
7cae785351 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-06-14 15:34:43 +00:00
Ihor Hordiichuk
69526b67eb Translated using Weblate (Ukrainian)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-06-14 14:01:48 +00:00
Robin
bde13e0fab Merge pull request #1104 from robintown/decouple-grid
Decouple video grid from video tile components
2023-06-13 09:56:47 -04:00
Robin Townsend
1207ecc9d7 Decouple video grid from video tile components
This is an attempt to address the feedback in https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404 that the video grid and video tile components have become too tightly coupled. After this change, the only requirements that the video grid makes of its child components are:

- They accept ref, style, and item props
- They attach the ref and styles to a react-spring animated element

Note: I removed the video grid Storybook file, because I'm not aware of anyone using Storybook for development of Element Call beyond Robert, and it would take some effort to fix to work with these changes.
2023-06-12 18:21:45 -04:00
Robin
8c21dbaade Don't require textual feedback (#1097)
We want to encourage scoring as much as possible for the purpose of our KPIs, even if it means we don't always get detailed textual feedback.
2023-06-12 12:52:29 +02:00
Robin
825cb75cb7 Merge pull request #1098 from robintown/fix-new-grid
Fix tiles not animating in the new grid layout
2023-06-10 17:09:21 -04:00
Robin Townsend
554da08628 Fix tiles not animating in the new grid layout
The new grid layout has been broken ever since upgrading react-spring, because it was apparently relying on a buggy behavior of react-spring that started transitions automatically even in imperative mode. react-spring 9.5.1 fixed that behavior, which means we now need to manually start the animations.
2023-06-09 13:52:21 -04:00
Robin
f070ab7f67 Merge pull request #1095 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-06-08 10:26:15 -04:00
Robin
1bfbb80f6d Merge pull request #1094 from robintown/widget-join-delay
Don't prematurely claim that we've joined the call in widget mode
2023-06-08 10:24:32 -04:00
Robin
d3d21fa86c Merge pull request #1093 from robintown/widget-feedback-hotfix
Don't show the quality survey if the app is a widget
2023-06-08 10:19:49 -04:00
raspin0
515ee72945 Translated using Weblate (Polish)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-06-08 08:35:14 +00:00
Glandos
6a6b62216d Translated using Weblate (French)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-06-08 08:35:14 +00:00
Linerly
95b0a6a1ae Translated using Weblate (Indonesian)
Currently translated at 99.3% (143 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-06-08 05:25:27 +00:00
Vri
28ffd591b7 Translated using Weblate (German)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-06-08 05:25:26 +00:00
Jeff Huang
5c17988e5b Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-06-08 03:01:53 +00:00
Jozef Gaal
9696b21b1b Translated using Weblate (Slovak)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-06-07 21:09:18 +00:00
Priit Jõerüüt
6deeb76124 Translated using Weblate (Estonian)
Currently translated at 100.0% (144 of 144 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-06-07 21:09:18 +00:00
Weblate
1da3d5e2c6 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-06-07 19:47:26 +00:00
Someone
b71a118db0 Translated using Weblate (Vietnamese)
Currently translated at 74.8% (104 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/vi/
2023-06-07 19:47:26 +00:00
raspin0
d0962d77e1 Translated using Weblate (Polish)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-06-07 19:47:26 +00:00
waclaw66
170e18af1c Translated using Weblate (Czech)
Currently translated at 94.2% (131 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/cs/
2023-06-07 19:47:26 +00:00
Robin Townsend
e4a3dbd7f7 Don't prematurely claim that we've joined the call in widget mode
In GroupCallView we do 'await enter()' when responding to a widget API join request, but it turns out enter wasn't actually returning a promise until now. The consequence of this was that in Element Web, when you click the join button you get shown a blank screen for a moment. This fixes that half-second moment of the UI being broken, allowing Element Web to show the intermediate 'joining' state.
2023-06-07 14:33:41 -04:00
Robin Townsend
47a4c5aa88 Don't show the quality survey if the app is a widget
As explained by the comment, we don't yet have designs that account for this combo.
2023-06-07 14:19:53 -04:00
Timo
efc25fd4ec hotfix Quality survey button interaction (#1091)
Signed-off-by: Timo K <toger5@hotmail.de>
2023-06-07 17:12:24 +02:00
Enrico Schwendig
8f8dd5f803 Display active tracks in OTel metrics (#1085)
* Add track, feed and transceiver spans under call span
2023-06-07 16:40:47 +02:00
Timo
2a6981c58d Add quality survey at the end of the call (#1084)
Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
2023-06-07 16:22:44 +02:00
Timo
eff8847586 add splitbrain params to MediaReceived event (#1089)
Signed-off-by: Timo K <toger5@hotmail.de>
2023-06-07 15:59:42 +02:00
Robin
166b9fede5 Merge pull request #1086 from robintown/ice-fallback
Add a URL parameter for allowing fallback ICE servers
2023-06-07 09:29:34 -04:00
Robin
5dd7c3ad8d Merge pull request #1088 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-06-07 09:23:28 -04:00
Someone
cc2402e61c Translated using Weblate (Vietnamese)
Currently translated at 25.8% (36 of 139 strings)

Translation: Element Call/element-call
Translate-URL: http://translate.element.io/projects/element-call/element-call/vi/
2023-06-06 06:35:08 +00:00
Linerly
66a582dd5f Translated using Weblate (Indonesian)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: http://translate.element.io/projects/element-call/element-call/id/
2023-06-06 06:35:08 +00:00
Enrico Schwendig
f0a6f5919e move webrtc etc. events from groupCall to matrix.call span (#1080)
* add new linked span for connection stats

* move stats span under call span and add user attribute

* Update matrix-js-sdk
2023-06-06 08:28:53 +02:00
Robin Townsend
5ef0486eff Add a URL parameter for allowing fallback ICE servers 2023-06-05 15:52:05 -04:00
Robin
b1a5417b63 Merge pull request #1083 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-06-05 14:36:38 -04:00
Someone
e129e90dd8 Added translation using Weblate (Vietnamese) 2023-06-05 04:16:05 +00:00
Weblate
5af7c9e7c7 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-06-04 13:07:31 +00:00
Jozef Gaal
9f924aef64 Translated using Weblate (Slovak)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-06-04 13:07:30 +00:00
phardyle
a0da11ea78 Translated using Weblate (Chinese (Simplified))
Currently translated at 90.6% (126 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hans/
2023-06-04 13:07:30 +00:00
Ihor Hordiichuk
48cf604bd1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-06-04 13:07:30 +00:00
Vri
00c44fb38a Translated using Weblate (German)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-06-04 13:07:30 +00:00
Robin
062802b3e9 Merge pull request #1082 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-06-02 10:51:46 -04:00
Jeff Huang
ce3d315d50 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-06-02 14:46:17 +00:00
Robin Townsend
0e50679db5 Translated using Weblate (Chinese (Simplified))
Currently translated at 89.9% (125 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hans/
2023-06-02 14:46:17 +00:00
Priit Jõerüüt
3db081440e Translated using Weblate (Estonian)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-06-02 14:46:17 +00:00
Glandos
a9b2ca01a7 Translated using Weblate (French)
Currently translated at 100.0% (139 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-06-02 14:46:17 +00:00
Vri
af4f27cbbf Translated using Weblate (German)
Currently translated at 96.4% (134 of 139 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-06-02 14:46:16 +00:00
Weblate
3f8848981d Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-06-01 23:07:30 +00:00
joemama
aa519f3c67 Translated using Weblate (Chinese (Simplified))
Currently translated at 95.7% (134 of 140 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hans/
2023-06-01 23:07:30 +00:00
Avery
99f06b0322 Translated using Weblate (Spanish)
Currently translated at 100.0% (140 of 140 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/es/
2023-06-01 23:07:30 +00:00
Robin
e67290550c Merge pull request #1037 from vector-im/SimonBrandner/feat/settings
Settings improvements
2023-05-25 22:28:26 -04:00
Robin Townsend
3c118f0cf7 Add a comment 2023-05-22 15:44:39 -04:00
Robin Townsend
9c2f4be17c Bring back the rageshake request modal 2023-05-22 15:30:29 -04:00
Robin Townsend
dc8d0fd81b Update strings 2023-05-22 15:12:41 -04:00
Robin Townsend
ae40dea7ec Make the profile form autosave 2023-05-22 15:12:41 -04:00
Robin Townsend
85380c8142 Make width of profile tab conform to designs 2023-05-22 15:12:41 -04:00
Robin Townsend
6560d9eb1a Make remove avatar button target area larger 2023-05-22 15:12:41 -04:00
Robin Townsend
69099772e0 Make settings button icon size match designs 2023-05-22 15:12:41 -04:00
Robin Townsend
cf1a7f2e21 Match settings modal to design nuances better 2023-05-22 15:12:41 -04:00
Robin Townsend
eeb1f4baaf Merge branch 'main' into SimonBrandner/feat/settings 2023-05-22 12:49:57 -04:00
Robin
568c989ff7 Merge pull request #1069 from robintown/hide-rageshake-request
Hide the rageshake request modal when the window is too small
2023-05-22 09:21:30 -04:00
Michael Kaye
8451296f3a Merge pull request #1052 from vector-im/michaelk/test_hanging_up_call
Add ability to explicitly hang up the call
2023-05-18 18:32:38 +01:00
Robin
606358c51b Merge pull request #1070 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-05-17 23:33:38 -04:00
raspin0
fd0956bbc5 Translated using Weblate (Polish)
Currently translated at 100.0% (140 of 140 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-05-17 22:34:03 +00:00
Robin Townsend
c01e363639 Hide the rageshake request modal when the window is too small 2023-05-17 13:59:15 -04:00
Enrico Schwendig
0114db7d2d update matrix-js-sdk (#1067) 2023-05-17 18:00:37 +02:00
Robin
e93dfb54d2 Merge pull request #1065 from robintown/resist-fingerprinting
Make Element Call work in Firefox's resist fingerprinting mode
2023-05-17 10:32:00 -04:00
Robin Townsend
f1ee3604de Make Element Call work in Firefox's resist fingerprinting mode
This one is gonna take some explaining:

When in resist fingerprinting mode, Firefox exhibits some funny behavior: when we ask for the the list of media devices, it gives us fake device IDs. But when the js-sdk requests a stream for any of those devices, Firefox associates the stream with the real device ID.

Now, in order to get the names of devices included in their metadata when you query the device list, you need to be holding a stream. For this reason, useMediaHandler was set up to reload the device list whenever matrix-js-sdk got a new local stream. But because of the inconsistency in device IDs, it would enter an infinite cycle telling matrix-js-sdk to request a stream for the fake device ID, but with matrix-js-sdk always responding with the real device ID.

I already wasn't happy with useMediaHandler's use of @ts-ignore comments to inspect private js-sdk fields, and in the meantime we've come up with a simpler function for requesting device names, so I decided to refactor useMediaHandler to use it instead. Importantly, it doesn't break in resist fingerprinting mode.

This created a new UX issue though: now, when on the lobby screen, useMediaHandler would request microphone access so it could get device names, followed immediately by a *second* pop-up for the lobby screen to request camera access. That's 1 pop-up too many, so I changed useMediaHandler to only request device names when a component is mounted that actually wants to show them. Currently, the settings modal is the only such component, and users normally only open it *after* granting full audio/video access, so this solution works out quite nicely.
2023-05-15 23:13:18 -04:00
Robin
d3111758b7 Merge pull request #1064 from robintown/update-js-sdk
Update matrix-js-sdk
2023-05-15 11:10:15 -04:00
Robin Townsend
abdb85226f Update matrix-js-sdk 2023-05-15 11:04:57 -04:00
David Baker
a56ef52eb0 Merge pull request #1062 from vector-im/toger5/peerConnectionCount
Update js sdk
2023-05-15 15:29:02 +01:00
Timo K
01f20bf6c0 update matrix-js-sdk 2023-05-15 16:21:00 +02:00
Robin
aae86ed830 Merge pull request #1055 from robintown/update-matrix-widget-api
Update matrix-widget-api
2023-05-15 08:43:07 -04:00
Timo
172ff266a6 Peer connections count in posthog media summary (#1046) 2023-05-15 10:55:36 +02:00
Timo K
d7ecbff9ed Merge branch 'main' into peerConnectionCount 2023-05-14 16:37:44 +02:00
Timo
f0f2ffe972 add audio concealment to posthog exporter (#1042)
* add audio concealment to posthog exporter
2023-05-14 16:36:41 +02:00
Šimon Brandner
491b0991cb Merge pull request #1059 from RiotTranslateBot/weblate-element-call-element-call 2023-05-14 09:11:01 +02:00
Robin
a961647e86 Merge pull request #1058 from robintown/update-js-sdk
Update matrix-js-sdk
2023-05-14 01:33:55 -04:00
Robin Townsend
207554f067 Fix tests 2023-05-14 01:32:11 -04:00
Weblate
b0ba366a2c Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-05-14 00:34:03 +00:00
raspin0
047fc822d6 Translated using Weblate (Polish)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-05-14 00:34:02 +00:00
Robin Townsend
7d454645d0 Fix types 2023-05-13 14:27:01 -04:00
Robin Townsend
fcb923f6db Update matrix-js-sdk 2023-05-13 14:11:59 -04:00
Robin
e3c34a7145 Merge pull request #1053 from robintown/local-tile-size
Cap the size of the local tile in 1:1 calls
2023-05-13 13:54:40 -04:00
Robin
1c9b2a24d9 Merge pull request #1054 from robintown/local-feed-contrast
Use a more noticeable shadow when displaying one tile on top another
2023-05-13 13:54:12 -04:00
Robin
7ab94cb003 Merge pull request #1056 from robintown/mute-icon
Show audio mute status next to people's names
2023-05-13 13:53:51 -04:00
Robin
239095321a Merge pull request #1057 from robintown/speaking-transition
Subtly animate active speaker indicators
2023-05-13 13:53:03 -04:00
Robin Townsend
caf90d851e Subtly animate active speaker indicators
A light touch of animation here is consistent with what the designs call for, and what we've done with the toolbars on video tiles.
2023-05-12 14:49:55 -04:00
Robin Townsend
a74733f6bc Show audio mute status next to people's names
Previously we were showing a combination of audio and video status icons on people's name badges, which meant there was no way to tell whether someone who had their video off was muted or not. The designs call for only microphone icons to be shown here.
2023-05-12 14:32:16 -04:00
Robin Townsend
440c617738 Use a more noticeable shadow when displaying one tile on top another
So that the local tile in 1:1 calls is more discernable against the background, especially when both participants have their video off
2023-05-12 14:16:33 -04:00
Robin
0f7ee3b860 Merge pull request #1045 from robintown/update-react-spring
Update react-spring
2023-05-12 14:11:53 -04:00
Robin Townsend
dc15fbc8c9 Update matrix-widget-api
To fix a TypeScript error I'm seeing when running the linter locally
2023-05-12 14:10:34 -04:00
Timo K
d7e6c8c913 rename to percentage 2023-05-12 18:31:31 +02:00
Timo K
7c5c4d1870 rename to percentage 2023-05-12 18:24:19 +02:00
Robin Townsend
2df8488c20 Cap the size of the local tile in 1:1 calls
So that it doesn't cover up too much of the remote tile at small window sizes
2023-05-12 11:43:17 -04:00
Michael Kaye
d270756443 Useful to be able to hang up rather than close the window. 2023-05-12 16:25:24 +01:00
Michael Kaye
8dd58d7e5c Merge pull request #1047 from vector-im/michaelk/data_id_tags_three
Test tags for invite links; joining call by URL; getting call name.
2023-05-12 08:51:59 +01:00
Enrico Schwendig
d148a81f91 Reconnect on network switch (#1029) 2023-05-11 16:47:05 +02:00
Michael Kaye
9587dd7352 Prettier 2023-05-11 15:16:17 +01:00
Michael Kaye
099dcd28c7 Test tags for invite links; joining call by UR:; getting call name. 2023-05-11 14:30:32 +01:00
Timo K
e925e7e060 peer connections count in posthog media summary 2023-05-10 17:58:05 +02:00
Robin Townsend
e3d5c84b17 Update react-spring
Just in case this has a chance of fixing https://github.com/vector-im/element-call/issues/960
2023-05-10 10:53:52 -04:00
Robin
6f4ab0d3ab Merge pull request #900 from abhijain2003/abhijain2003/loadingui
changes Loading room... to Loading...
2023-05-10 09:36:33 -04:00
Michael Kaye
283b5d4504 Merge pull request #1036 from vector-im/michaelk/data_id_tags_two
Add data-testid tags for registration, login, displayname & logout
2023-05-10 13:15:12 +01:00
Timo K
6dc26392d7 add audio concealment to posthog exporter 2023-05-09 20:33:49 +02:00
Robin
4572e35339 Merge pull request #1034 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-05-09 01:28:45 -04:00
Šimon Brandner
6cad89b20c Add success message
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-05-05 19:36:23 +02:00
Šimon Brandner
4c1168aaf7 Feedback copy
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-05-05 19:29:11 +02:00
Šimon Brandner
57e79862a5 User ID -> Username
Co-authored-by: Robin <robin@robin.town>
2023-05-05 19:18:34 +02:00
Šimon Brandner
93a47e7009 Fix casing
Co-authored-by: Robin <robin@robin.town>
2023-05-05 19:17:59 +02:00
Šimon Brandner
d4f0300c82 Match designs
Co-authored-by: Robin <robin@robin.town>
2023-05-05 19:17:49 +02:00
Enrico Schwendig
4181780040 change stats interval to 10s (#1038) 2023-05-05 16:14:19 +02:00
Michael Kaye
b19150bbba RegisteredView needs data tags as not reused from UnregisteredView. 2023-05-05 11:46:05 +01:00
Šimon Brandner
f11e1fac6b i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-05-05 12:04:48 +02:00
Šimon Brandner
0269753f59 Settings improvements
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-05-05 11:44:35 +02:00
Šimon Brandner
f6c95461e4 Translated using Weblate (Czech)
Currently translated at 98.5% (139 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/cs/
2023-05-03 21:37:41 +00:00
Przemysław Romanik
bfe65adb51 Translated using Weblate (Polish)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-05-03 21:37:41 +00:00
raspin0
e031340ccb Translated using Weblate (Polish)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-05-03 21:37:41 +00:00
Michael Kaye
c70536996e Work around data-testid issue by tagging the icon not the Item. 2023-05-03 18:07:06 +01:00
Michael Kaye
a71a1c5c93 (non-working) UserMenu data-testids. 2023-05-03 14:42:44 +01:00
Michael Kaye
f7fc03cdc9 Initial cut of required tags for registration and login flows. 2023-05-03 14:42:44 +01:00
Michael Kaye
52dccff229 Merge pull request #1026 from vector-im/michaelk/data_id_tags
Add data ID tags for trafficlight adapter to find
2023-05-03 14:38:55 +01:00
Michael Kaye
9c8692d99e Remove NODE_OPTIONS from GHA commands, as defaulted in package.json now. 2023-05-03 12:57:03 +01:00
David Baker
69826b4de0 Merge pull request #1035 from vector-im/revert-1018-revert-1016-gzip_extension
Re-apply "Add .gz extension to to traces.json"
2023-05-03 10:56:32 +01:00
David Baker
f2a7de2d8e Revert "Revert "Add .gz extension to to traces.json"" 2023-05-03 10:43:46 +01:00
Michael Kaye
2318f2c4a0 prettier -w 2023-05-02 17:33:56 +01:00
David Baker
515e00b763 Merge pull request #1030 from vector-im/dbkr/include_org_matrix_call
Include org.matrix.call events in sent events for OTel
2023-04-28 09:11:18 +01:00
David Baker
836c3b5614 Merge pull request #1028 from toger5/otel_read_through
Remove duplicated code in OpenTelemetry files
2023-04-27 18:11:24 +01:00
David Baker
403ee79d61 Include org.matrix.call events in sent events for OTel 2023-04-27 18:01:40 +01:00
Timo K
ecf5f9916c otel read through code 2023-04-27 14:00:40 +02:00
Michael Kaye
4e8146bc9c Add screenshare tags 2023-04-27 09:42:25 +01:00
Michael Kaye
323dba620d Add a range of data-testid tags to be able to cleanly identify parts of the application. 2023-04-27 09:42:25 +01:00
Michael Kaye
8d56a8e8fc Fix #978 issue with builds not building. 2023-04-27 09:42:25 +01:00
Enrico Schwendig
e3a3859739 Make webrtc stats configurable (#1019)
* stats: make webrtc stats configurable
2023-04-27 09:30:34 +02:00
Robin
15350b6989 Merge pull request #1021 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-04-25 10:32:28 -04:00
David Baker
4c33a29aef Merge pull request #1025 from vector-im/dbkr/remove_console_exporter
Remove the console exporter
2023-04-25 10:37:26 +01:00
David Baker
ecc3693c47 Remove accidental log line 2023-04-25 10:32:36 +01:00
David Baker
7153ead8cb Remove the console exporter
I'm not sure how this got left in, presumably it was not intended.
2023-04-24 19:52:56 +01:00
David Baker
4ddf9ce29c Merge pull request #1024 from vector-im/dbkr/otel_detect_call_changed
End spans when calls are replaced by a different call
2023-04-24 19:52:01 +01:00
David Baker
32476571fb End spans when calls are replaced by a different call 2023-04-24 17:57:05 +01:00
SmallJinn
0df028888e Translated using Weblate (Russian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ru/
2023-04-24 15:33:55 +00:00
Šimon Brandner
8ab7ee9298 i18n
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-04-24 12:24:27 +02:00
Weblate
3c7cb3cb96 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-04-23 12:33:56 +00:00
Priit Jõerüüt
c59c9053c9 Translated using Weblate (Estonian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-04-23 12:33:56 +00:00
Ihor Hordiichuk
f1ad2e2f8a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-04-23 12:33:56 +00:00
Linerly
f30dc9593f Translated using Weblate (Indonesian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-04-23 12:33:56 +00:00
Vri
a0bed20576 Translated using Weblate (German)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-04-23 12:33:56 +00:00
Weblate
742482d0a8 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-04-23 12:33:56 +00:00
Theo
ac17ae6557 Translated using Weblate (Greek)
Currently translated at 69.2% (99 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/el/
2023-04-23 12:33:56 +00:00
Jozef Gaal
1c0dedc27f Translated using Weblate (Slovak)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-04-23 12:33:56 +00:00
Jeff Huang
4224916b81 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-04-23 12:33:56 +00:00
Ihor Hordiichuk
12633f8e12 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-04-23 12:33:56 +00:00
Glandos
6b14e622dd Translated using Weblate (French)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-04-23 12:33:56 +00:00
David Baker
060a50d27a Merge pull request #1020 from vector-im/dbkr/rageshake_processor_event_name
Add the event name to the rageshake span processor export
2023-04-21 10:24:31 +01:00
David Baker
a9e63ddbcc Merge pull request #1022 from vector-im/dbkr/fix_mute_on_blur
Fix audio mute on window blur
2023-04-21 10:22:04 +01:00
David Baker
4459eaeb9d Fix audio mute on window blur
https://github.com/vector-im/element-call/pull/1015/files missed a
change of spacebarHeld to a ref.
2023-04-21 10:18:43 +01:00
David Baker
f5e2161a9e Add the event name to the rageshake span processor export 2023-04-20 17:18:06 +01:00
Robin
8437e263af Merge pull request #1013 from robintown/controls-overlay
Keep inspector from covering call buttons
2023-04-20 10:45:36 -04:00
Robin
4719a92ffc Merge pull request #1015 from robintown/shortcut-focus
Make keyboard shortcuts accessible by default
2023-04-20 10:45:11 -04:00
David Baker
9b398590b9 Merge pull request #1017 from vector-im/dbkr/use_native_resizeobserver
Use the native ResizeObserver where available
2023-04-20 14:33:44 +01:00
David Baker
cfcd7e6b22 Merge pull request #1018 from vector-im/revert-1016-gzip_extension
Revert "Add .gz extension to to traces.json"
2023-04-20 13:56:14 +01:00
David Baker
d96643d003 Revert "Add .gz extension to to traces.json" 2023-04-20 13:47:48 +01:00
David Baker
d1aa34b2e0 Use the native ResizeObserver where available
My dev env suddenly, with no apparent prompt, went into a mode where
it wouldn't display nay video tiles which was because they were 0x0
in the top left corner, which in turn was because the ResizeObserver
was never returning the actual bounds of the video tile container.

As per comment, this uses the native impl in preference to the ponyfill,
although in practice it looks like all our target browsers should support
it, so perhaps we could just remove the ponyfill entirely.
2023-04-20 13:39:25 +01:00
David Baker
1792ef7e38 Merge pull request #1016 from vector-im/gzip_extension
Add .gz extension to to traces.json
2023-04-20 11:09:00 +01:00
David Baker
1dd70ea22d Add .gz extension to to traces.json
As we are sending a gzipped file. We could make the rageshake server
look for this and gunzip it, but either way this seems like as good a
way as any to signal that the file is gzipped.
2023-04-20 09:55:57 +01:00
Robin Townsend
28368da60a Update strings 2023-04-19 16:20:53 -04:00
Robin Townsend
4114622d44 Remove the keyboard shortcut setting 2023-04-19 16:15:38 -04:00
Robin
6f2b32ead5 Merge pull request #1014 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-04-19 15:59:20 -04:00
Robin Townsend
6f13989819 Fix lint errors 2023-04-19 15:55:55 -04:00
Robin Townsend
1184b71396 Format with Prettier 2023-04-19 15:54:39 -04:00
Robin Townsend
56bd54a645 Disable keyboard shortcuts when focus is in a modal 2023-04-19 15:51:44 -04:00
Robin Townsend
18fa1371d3 Use a ref for spacebarHeld
because we can and it means fewer renders
2023-04-19 15:17:32 -04:00
Weblate
f6f0fce2b2 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-04-19 18:56:57 +00:00
Theo
a1b6e91354 Added translation using Weblate (Greek) 2023-04-19 18:56:56 +00:00
Robin Townsend
6ad4663508 Keep inspector from covering call buttons 2023-04-19 14:43:37 -04:00
Robin
30a224e20e Merge pull request #1010 from robintown/speaking-border
Make audio activity border match the tile border radius
2023-04-19 10:32:39 -04:00
Robin Townsend
3c7f01a510 Make audio activity border match the tile border radius 2023-04-19 10:24:47 -04:00
Enrico Schwendig
2b5de6db03 Add new calculation of received media stats (#1009) 2023-04-19 10:14:29 +02:00
David Baker
8eafb1ae4a Merge pull request #1005 from robintown/parallel-calls
Detect split-brains caused by parallel calls
2023-04-18 17:29:23 +01:00
Enrico Schwendig
3da4b4eeef Add jitter and packet loss info in summary report (#1006)
* stats: add jitter and packet loss
2023-04-18 15:20:06 +02:00
David Baker
c31185ffef Merge pull request #1007 from vector-im/dbkr/rageshake_exporter_microseconds
Use microseconds in the rageshake exporter
2023-04-18 13:18:50 +01:00
David Baker
0de1aa74ee Use microseconds in the rageshake exporter
Fixes times being off by a factor of 1000
2023-04-18 12:48:34 +01:00
Robin Townsend
838137c83b Detect split-brains caused by parallel calls
This is another KPI for PostHog.
2023-04-17 16:58:51 -04:00
David Baker
f627835646 Merge pull request #1002 from vector-im/dbkr/fix_posthog_embedded_mode
Fix PostHog in embedded mode
2023-04-17 19:42:02 +01:00
David Baker
9442b314b2 Fix PostHog in embedded mode
Embedded mode has a differtent path to join the call and we missed
changing the groupCall.enter() function for the wrapper that does
analytics.
2023-04-17 18:47:46 +01:00
Robin
7221b7c3a2 Merge pull request #998 from robintown/rageshake-processor
Include unended spans in rageshakes
2023-04-14 11:55:16 -04:00
Enrico Schwendig
370a6579fb Add jitter in webrtc stats (#1000)
* stats: add jitter in webrtc stats
2023-04-14 08:49:33 +02:00
Robin
5bec960112 Merge pull request #999 from robintown/posthog-processor
Send rejoin events to PostHog in realtime
2023-04-13 10:21:44 -04:00
Robin Townsend
da7760d7ab Send rejoin events to PostHog in realtime
By converting PosthogSpanExporter to a SpanProcessor just like the RageshakeSpanProcessor, it can now monitor spans in realtime as they are started.
2023-04-12 18:14:59 -04:00
Robin Townsend
a17ffcc327 Include unended spans in rageshakes
By turning the RageshakeSpanExporter into a SpanProcessor, it can now be notified of spans as soon as they're started.
2023-04-12 17:12:02 -04:00
Robin
d211d27817 Merge pull request #997 from robintown/telemetry-crash
Fix a crash when adding call events to telemetry
2023-04-12 09:26:03 -04:00
Robin Townsend
0637804d61 Fix a crash when adding call events to telemetry
Since typeof null is 'object', the flattenVoipEventRecursive function was mistakenly casting nulls to Record<string, unknown> in its typeof v === "object" case, causing Object.entries to explode.
2023-04-11 23:05:37 -04:00
Robin
a2b3e098b6 Merge pull request #995 from robintown/rageshake-traces
Include OpenTelemetry traces in rageshakes
2023-04-11 16:24:09 -04:00
Robin
4bcddad316 Merge pull request #994 from robintown/lock
Save lockfile
2023-04-11 16:20:21 -04:00
Enrico Schwendig
e2293665f9 Add posthog event for summary report (#992)
* stats: add posthog event for summary report

* stats: remove console log
2023-04-11 09:06:13 +02:00
Robin Townsend
95eca18207 Include OpenTelemetry traces in rageshakes 2023-04-11 01:13:19 -04:00
Robin Townsend
2f33902ea9 Save lockfile 2023-04-10 15:04:42 -04:00
David Baker
6999765f39 Merge pull request #991 from vector-im/dbkr/add_release_note_script
Add tiny release notes script
2023-04-06 18:03:17 +01:00
David Baker
480e46c5b2 Fix my lazy regexing
Co-authored-by: Robin <robin@robin.town>
2023-04-06 17:59:48 +01:00
Enrico Schwendig
bb5c382fd0 separate summary report from stats report (#986)
* stats: separate summary report from stats report

* stats: switch to last summery stats builder

* stats: update matrix-js-sdk
2023-04-06 13:19:39 +02:00
David Baker
2b71a6c4f4 Add tiny release notes script 2023-04-06 11:12:13 +01:00
David Baker
dd1485a277 Merge pull request #988 from vector-im/dbkr/enable_otel_by_collector
Allow different OpenTelemetry collectors to be enabled/disabled
2023-04-05 20:05:41 +01:00
David Baker
caea22fa89 Remove the recheck callback since it isn't necessary for now 2023-04-05 19:00:07 +01:00
Robin
858c68baf1 Merge pull request #971 from robintown/audio-observability
End-to-end audio observability
2023-04-05 13:55:12 -04:00
Robin Townsend
de3bad3810 Remove temporary config file 2023-04-05 13:38:49 -04:00
David Baker
88f3b30040 Allow different OpenTelemetry collectors to be enabled/disabled
Always enable OpenTelemetry, but conditionally enable the OTLP
exporter, as per comment.

Fixes https://github.com/vector-im/element-call/issues/987
2023-04-05 18:21:29 +01:00
Robin Townsend
928f1c1d6f Address review feedback 2023-04-05 12:56:50 -04:00
Robin Townsend
711cdf9a60 Merge branch 'main' into audio-observability 2023-04-05 12:50:38 -04:00
David Baker
b2317dac84 Merge pull request #985 from vector-im/dbkr/fix_posthog_exception
Fix exception when loading PostHog
2023-04-05 15:23:54 +01:00
David Baker
fec299ab20 Skip whole block if no otel instance 2023-04-05 15:11:51 +01:00
David Baker
5e4aa53997 Don't call posthog before its initialised 2023-04-05 15:00:14 +01:00
David Baker
0dcaa90650 Fix exception when loading PostHog
PostHog was expecting the matrix client object to be initialised at
the point it ran its setup, which wasn't the case. Check to see if it's
there on login and add an onLoginStatusChanged hook that to re-check.

Also make a few methods private that didn't need to be public.

Also fix a few instances where the OpenTelemetry group call tried to
report metrics using a tracer which didn't exist anymore, if the user
disabled analytics and then joined the same call again.
2023-04-05 13:06:55 +01:00
David Baker
7b88c4330e Merge pull request #984 from vector-im/dbkr/otel_peerconn_events
Add OpenTelemetry events for PeerConnection state changes / errors
2023-04-05 10:23:53 +01:00
David Baker
b061cbfb2f Remove the other listeners 2023-04-05 10:01:58 +01:00
David Baker
2435846f66 Latest js-sdk develop (with required PR merged) 2023-04-05 10:00:16 +01:00
David Baker
23ddd73f4f Merge remote-tracking branch 'origin/main' into dbkr/otel_peerconn_events 2023-04-05 09:35:43 +01:00
Enrico Schwendig
390442a4c3 Add webrtc metric to OTel (#974)
* stats: Add summery report

---------

Co-authored-by: David Baker <dave@matrix.org>
2023-04-05 10:25:26 +02:00
David Baker
c824ea6f9a Add OpenTelemetry events for PeerConnection state changes / errors
Creates a new class to represent individual calls and adds the listeners
there.

Requires https://github.com/matrix-org/matrix-js-sdk/pull/3251
Based on https://github.com/vector-im/element-call/pull/974
2023-04-04 18:00:45 +01:00
David Baker
28196a2e9d Merge pull request #981 from vector-im/dbkr/call_events_to_call_span
Move call events to the call span
2023-04-04 17:52:41 +01:00
David Baker
5b70def4d2 Add null check for call span 2023-04-04 17:49:49 +01:00
David Baker
2cd549cdc8 Merge pull request #982 from vector-im/dbkr/otel_flatten_include_booleans
Include booleans in flattened OpenTelemetry object
2023-04-04 17:26:20 +01:00
Robin
e0089a0aee Merge pull request #958 from robintown/forced-opt-in
Opt into analytics by default during the beta
2023-04-04 09:27:01 -04:00
Robin
61a0534984 Merge pull request #983 from robintown/rejoin-analytics
Track call rejoins
2023-04-04 09:18:40 -04:00
David Baker
29223b62ad Merge pull request #980 from vector-im/dbkr/display_name_on_call_spans
Add displayname on call spans
2023-04-04 10:26:24 +01:00
Robin Townsend
a52251befa Track call rejoins
Call rejoins will be one of the KPIs we track in PostHog to measure call quality. I've also reverted the previous behavior which logged all OpenTelemetry spans to PostHog, since we should only be sending small, anonymized bits of data there.
2023-04-03 21:05:13 -04:00
David Baker
30f75c6cd2 Don't pass null / undefined as attribute value 2023-04-03 17:41:40 +01:00
David Baker
8fa23b7da9 Include booleans in flattened OpenTelemetry object 2023-04-03 16:58:29 +01:00
David Baker
277081ee2a Move call events to the call span 2023-04-03 14:35:04 +01:00
David Baker
3a7983d2de Add displayname on call spans 2023-04-03 14:31:17 +01:00
Enrico Schwendig
3b06258e40 stats: rename enum to avoid shadow values 2023-04-03 14:07:29 +02:00
Enrico Schwendig
c53dbfde27 Merge remote-tracking branch 'origin/main' into enricoschw/real-time-media-statistics-for-full-mesh 2023-04-03 13:47:11 +02:00
Enrico Schwendig
889a31489b stats: fix typo 2023-04-03 12:37:55 +02:00
David Baker
cb0ba6d827 Add missed 'r' 2023-03-31 14:30:24 +01:00
David Baker
e18c69ec89 Use latest js-sdk develop 2023-03-31 14:29:07 +01:00
David Baker
47e0ca2eda Put cors header back to https for now
To remove that change for the diff
2023-03-31 14:27:50 +01:00
David Baker
e870188be3 Merge pull request #961 from vector-im/dbkr/otel
OpenTelemetry
2023-03-31 14:25:34 +01:00
Enrico Schwendig
dd67a45671 stats: Add summery report 2023-03-31 14:57:56 +02:00
Enrico Schwendig
707272bf19 Merge branch 'dbkr/otel' into enricoschw/real-time-media-statistics-for-full-mesh 2023-03-31 13:14:49 +02:00
David Baker
dc725f90a9 Fix confusing comment 2023-03-31 11:12:10 +01:00
David Baker
a1aca7bdf2 Fix lying comment 2023-03-31 11:10:05 +01:00
David Baker
773f2e009d Typo 2023-03-31 10:58:12 +01:00
David Baker
5e6c33b3b5 Let otel know we're joining before trying to join
Otherwise it starts getting calls being created before the group call
span exists and we get call spans not associated with the group call
span.

(What 74b218af8c should have been)
2023-03-31 10:30:01 +01:00
David Baker
72403d1aea Revert 74b218af8c
Comitted entirely the wrong thing
2023-03-31 10:26:33 +01:00
David Baker
74b218af8c Let otel know we're joining before trying to join
Otherwise it starts getting calls being created before the group call
span exists and we get call spans not associated with the group call
span.
2023-03-30 17:19:13 +01:00
David Baker
c2b78d59c6 Add more events:
* VoIP events received
 * Call errors
 * Group call errors
 * Undecryptable to-device events
2023-03-30 16:54:10 +01:00
David Baker
21458c8840 Call the same leave method everywhere
So we end the group call span whenever we leasve the call, including
if we close the page.
2023-03-30 13:03:58 +01:00
David Baker
f96ce8985d Only enable otel if we have a collector URL 2023-03-29 16:04:11 +01:00
David Baker
848e28ef92 Change allowed origin to https://* as that allows the PR branches out-of-the-box 2023-03-29 15:53:29 +01:00
David Baker
4bf1fbfd8e Gah, the sentry logger 2023-03-29 13:31:47 +01:00
David Baker
34a72679a1 Merge remote-tracking branch 'origin/main' into dbkr/otel 2023-03-29 12:30:41 +01:00
David Baker
77c6357b08 Use js-sdk from hangup refactor branch
https://github.com/matrix-org/matrix-js-sdk/pull/3234
2023-03-29 12:28:04 +01:00
David Baker
15e4c01c5d Add max old space fix to publish workflow too 2023-03-28 15:47:59 +01:00
David Baker
1307d89175 Merge pull request #975 from vector-im/dbkr/update_js_sdk_20230328
Update js-sdk
2023-03-28 15:40:30 +01:00
David Baker
d53be695f9 Work around Vite unbounded memory usage
Port fix from otel branch where it appears to be working
2023-03-28 15:33:11 +01:00
David Baker
247d15cbb5 Update js-sdk
for da03c3b529
2023-03-28 15:24:33 +01:00
Enrico Schwendig
66c3d05ae9 docu: Add webrtc metric to OTel 2023-03-28 11:51:15 +02:00
Robin Townsend
c4f029ae4f Fix lint error 2023-03-27 22:30:12 -04:00
Robin Townsend
8978f94fe4 Add temporary config 2023-03-27 09:12:32 -04:00
David Baker
40f5c53c05 Put CORS header back to http://* with comment on why browsers are annoying 2023-03-24 09:31:52 +00:00
Robin Townsend
5f41f9476b Disable the opt in analytics setting if Posthog isn't configured 2023-03-23 13:07:34 -04:00
David Baker
d1ba5dff38 Allow all origins 2023-03-23 14:37:25 +00:00
Robin Townsend
313ebe258e Add end-to-end audio observability
This reports via OpenTelemetry when particular participants are speaking, as an easy way to observe the delivery of audio in calls.
2023-03-22 14:23:26 -04:00
Robin
71b1d0c6b1 Merge pull request #970 from robintown/matrix-widget-api
Update matrix-widget-api
2023-03-22 13:58:36 -04:00
Robin Townsend
76c0277301 Update matrix-js-sdk 2023-03-22 11:41:41 -04:00
Robin Townsend
698bea93e3 Update matrix-widget-api 2023-03-22 11:33:50 -04:00
David Baker
48493a96e1 Wait until config is loaded to load otel 2023-03-22 12:41:33 +00:00
David Baker
ec88907981 Comment the max old space workaround which seems to be working (so far) 2023-03-22 12:04:15 +00:00
David Baker
9c0adfd32e Unused import 2023-03-22 12:00:34 +00:00
David Baker
f6fb65be49 Remove odd source mapping comment & unused commented code 2023-03-22 11:58:41 +00:00
David Baker
3d6ae3fbc3 Enable/disable opentelemetry based on config/user preference
Add config to set collector URL, obey the same analytics setting as
posthog. Also refactor into a class to make it easier to manage.
2023-03-22 11:55:21 +00:00
David Baker
359e055314 Make callMembershipSpan optional 2023-03-21 12:13:51 +00:00
David Baker
6696af9b3f Experiment to try & stop vite OOMing 2023-03-20 19:29:19 +00:00
David Baker
9b02d17224 Merge branch 'main' into dbkr/otel 2023-03-20 19:20:13 +00:00
David Baker
6b36604c84 Update js-sdk 2023-03-20 19:17:50 +00:00
David Baker
ef9934ce6b Commit nginx config file 2023-03-20 13:53:07 +00:00
David Baker
e7a7cf3eb8 Export events to posthog too 2023-03-20 13:30:21 +00:00
David Baker
63ede0b51a Version using events for call joins / leaves and matrix events
This is probably conceptually nicer although isn't quite as nice in
the jaeger / stalk UI.

Also this may no loger work with the posthog exporter (unsure what it
will do with events on spans).
2023-03-17 19:26:23 +00:00
David Baker
2d91b43a7d Set attributes on the root span
Setting them on the context doesn't actually make them show up in
jaeger, it's just a way to propagate the info around between
different things.
2023-03-17 19:03:43 +00:00
Robin
ff90e507a6 Merge pull request #963 from alariej/navigator_mediasession_fix
Fix for Android WebView, which does not support navigator.mediaSession
2023-03-17 13:20:05 -04:00
David Baker
f8f5d2011d Add CORS to jaeger query endpoint and make spans nested
Adds an nginx in front of the query endpoint so we can use stalk
without faffing with browser extension to bypass CORS.

Also make the spans correctly have the call membership span as parent,
which they didn't because we hadn't set the span at the point we made
the context.
2023-03-17 17:01:59 +00:00
alariej
1bf1813a77 Fix for Android WebView, which does not support navigator.mediaSession 2023-03-17 17:20:16 +01:00
David Baker
521b0a857a Send spans for state events 2023-03-16 18:08:28 +00:00
David Baker
31450219c8 More work on opentelemetry event reporting
Moastly a re-org to avoid new contexts over React component unmounts/
remounts.
2023-03-16 14:41:55 +00:00
David Baker
22d2404370 Prettier 2023-03-15 16:04:15 +00:00
David Baker
c519e13885 Version that does at least send some traces 2023-03-15 16:00:39 +00:00
David Baker
1e2cd97764 Include the arguably-obvious command line 2023-03-15 14:38:17 +00:00
David Baker
0cca5ae174 Slightly evolved but not-yet-working OpenTelemetry
More usefully, including docker config for starting a CORS enabled
OTLP collector so we don't have to use zipkin.
2023-03-15 14:35:10 +00:00
Robin Townsend
971eca59ff Opt into analytics by default during the beta 2023-03-13 19:12:47 -04:00
Robin
7aa902853a Merge pull request #956 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-03-11 00:49:06 -05:00
lunarna-gh
49b23160f3 Translated using Weblate (Polish)
Currently translated at 97.2% (139 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-03-11 01:33:43 +00:00
Timo
b68d4bf51b Update README.md (#953)
Only a tiny typo
2023-03-10 14:42:24 +01:00
Timo K
4c59638d00 otel for call start, end and mute
This is send over zipkin.
And it uses a posthog exporter to export events to posthog
using a _otel prefix
2023-03-10 10:33:54 +01:00
Robin
3e081ac936 Merge pull request #890 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-03-08 10:53:58 -05:00
Weblate
7e1d5fff14 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-03-07 16:33:45 +00:00
Jozef Gaal
ca1f502e62 Translated using Weblate (Slovak)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-03-07 16:33:44 +00:00
Open Culture Foundation
1913abc682 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-03-07 16:33:44 +00:00
Peter Chen
345891dbdf Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-03-07 16:33:44 +00:00
Jeff Huang
29082adb73 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/
2023-03-07 16:33:44 +00:00
Avery
3dc288574e Translated using Weblate (Spanish)
Currently translated at 98.6% (141 of 143 strings)

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

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-03-07 16:33:44 +00:00
Ihor Hordiichuk
6fceeec323 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-03-07 16:33:44 +00:00
Glandos
44762afd2a Translated using Weblate (French)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-03-07 16:33:44 +00:00
Linerly
fa97f51907 Translated using Weblate (Indonesian)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-03-07 16:33:44 +00:00
Vri
a15ce32ecf Translated using Weblate (German)
Currently translated at 100.0% (143 of 143 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-03-07 16:33:44 +00:00
Weblate
4a3b7bc6dd Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2023-03-07 16:33:44 +00:00
DarkCoder15
a1cb52c613 Translated using Weblate (Russian)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ru/
2023-03-07 16:33:44 +00:00
Jozef Gaal
4cfa726df1 Translated using Weblate (Slovak)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-03-07 16:33:44 +00:00
Priit Jõerüüt
e31de96679 Translated using Weblate (Estonian)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-03-07 16:33:44 +00:00
Ihor Hordiichuk
16ec643e84 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-03-07 16:33:44 +00:00
Glandos
fa62b97b27 Translated using Weblate (French)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-03-07 16:33:44 +00:00
Linerly
3e072aed3c Translated using Weblate (Indonesian)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-03-07 16:33:44 +00:00
Vri
bac795e1e8 Translated using Weblate (German)
Currently translated at 100.0% (142 of 142 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-03-07 16:33:44 +00:00
Suguru Hirahara
ba48ade3b7 Translated using Weblate (Japanese)
Currently translated at 80.8% (114 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-03-07 16:33:44 +00:00
Suguru Hirahara
0fab702d70 Translated using Weblate (Japanese)
Currently translated at 80.1% (113 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-03-07 16:33:44 +00:00
Suguru Hirahara
7f8c0153ed Translated using Weblate (Japanese)
Currently translated at 76.5% (108 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-03-07 16:33:44 +00:00
Suguru Hirahara
d35c13b903 Translated using Weblate (Japanese)
Currently translated at 75.8% (107 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-03-07 16:33:44 +00:00
Enrico Schwendig
ee4760351c add end-to-end test workflow (#885)
* e2e: add end-to-end test workflow

- The tests are executed in a Docker container. 
-  The static users are connected via `matrix-js-sdk Client`. 
-  A test user connecting to the conference via EC.
2023-03-06 09:45:42 +01:00
Enrico Schwendig
0c66b32b49 matrix-js-sdk: update to last develop branch with call group fix (#941) 2023-03-03 08:06:10 +01:00
Robin
5eb552e36a Merge pull request #935 from alariej/alariej
Add e2eEnabled parameter to Widget client
2023-03-02 16:07:04 -05:00
Enrico Schwendig
29e41c7227 Allow Element Call to be started without audio / video interface (#924)
* config: add feature in `config.json`

* groupCall: adjust connection state in feed if allowCallWithoutVideoAndAudio

* matrix-js-sdk: update version for allowCallWithoutVideoAndAudio

- I modified the SDK so that mute unmute work without media and check device permission inside the SDK
- allowCallWithoutVideoAndAudio is only checked at one point outside the SDK

* docu: add join group call without media docu in READMe

---------

Co-authored-by: Robin Townsend <robin@robin.town>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-03-02 18:48:32 +01:00
alariej
58d87db55f Add e2eEnabled parameter to Widget client 2023-03-01 14:30:25 +01:00
Timo
0423a494c4 Checkbox for analytics opt in & settings redesign (#934) 2023-03-01 13:47:36 +01:00
David Baker
2454daeef9 Merge pull request #932 from vector-im/dbkr/allow_full_alias_input
Behave sensibly if a full room alias is entered
2023-02-28 14:12:10 +00:00
David Baker
64703fd3cc Typo
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-28 14:09:52 +00:00
David Baker
53bc8eb82f Behave sensibly if a full room alias is entered
Check explicitly to see if the room name that's enetered into the box
looks like a room alias and if so, do the sensible thing.

Fixes https://github.com/vector-im/element-call/issues/852
2023-02-28 13:50:24 +00:00
Robin
cdf2d560b8 Merge pull request #911 from robintown/big-grid
New grid system
2023-02-19 09:02:57 -05:00
Robin
9dff42b437 Merge pull request #919 from robintown/ignore-coverage
Add coverage reports to .gitignore
2023-02-16 11:32:29 -05:00
Robin Townsend
1fc181dc28 Document useReactiveState 2023-02-15 16:38:49 -05:00
Robin Townsend
fbc72283d4 Add missing copyright headers 2023-02-15 16:20:58 -05:00
Robin Townsend
9a0dfad5f9 Add coverage reports to .gitignore 2023-02-15 15:13:39 -05:00
Robin Townsend
efbf319fa1 Explain why we cast the tile springs 2023-02-13 22:40:26 -05:00
Robin Townsend
ef4a62ca62 Document useMergedRefs 2023-02-13 22:38:27 -05:00
Robin Townsend
8c818b9ce1 Get 100% test coverage on grid operations 2023-02-13 22:24:04 -05:00
Robin Townsend
58ed372afa Fix type and lint errors 2023-02-13 21:57:57 -05:00
Robin Townsend
69e6ba93c1 Add a switch to toggle between the new and old grids 2023-02-13 20:36:42 -05:00
Robin Townsend
b2b2f0bb15 Merge branch 'main' into big-grid 2023-02-13 18:54:19 -05:00
Robin Townsend
8d0bf4cacc Test grid operations 2023-02-13 18:35:50 -05:00
Robin Townsend
5448744871 Document grid operations 2023-02-13 18:35:23 -05:00
David Baker
933dc4e2d3 Merge pull request #910 from vector-im/dbkr/ignore_media_action
Don't pause audio streams on media actions
2023-02-13 17:35:33 +01:00
David Baker
605dd44df0 Rename other instance of variable 2023-02-13 15:49:58 +00:00
David Baker
07a4de638f Don't pause audio streams on media actions
This adds handlers for the media actions to do nothing, otherwise
they cause the audio element for a random participant to get paused.

Fixes https://github.com/vector-im/element-call/issues/855
2023-02-13 15:20:48 +00:00
Abhi Jain
33dd2758d7 change ... to … unicode for consistency 2023-02-13 09:55:32 +05:30
Timo
eda11cfc08 Inform that the user that config keys are missing (#880) 2023-02-09 12:57:54 +01:00
Abhi Jain
d40e467b7d changes Loading room... to Loading... 2023-02-08 14:26:56 +05:30
Robin Townsend
d852e33413 Document the component 2023-02-08 00:32:08 -05:00
Robin Townsend
8d46687a54 Refactor grid state tracking 2023-02-07 23:27:49 -05:00
Robin Townsend
978b0f08e8 Move grid algorithms into a separate file 2023-02-07 22:13:50 -05:00
Robin Townsend
374c68e3c0 Fix tiles enlarging to the wrong place on mobile 2023-02-05 01:17:28 -05:00
Robin Townsend
82ac775124 Fix scrolling on mobile 2023-02-05 00:55:12 -05:00
Robin Townsend
6adcf95aaa Implement different column counts and mobile layout 2023-02-04 00:43:53 -05:00
Robin Townsend
206730ffc0 Fix infinite loop when a tile can't be enlarged 2023-02-03 16:52:42 -05:00
Robin Townsend
1e858f6ba3 Fix a typo 2023-02-03 16:27:49 -05:00
Robin Townsend
22382413dc Make drag and drop mobile-friendly 2023-02-03 15:42:47 -05:00
Robin Townsend
6cd939db0c Fix a crash when there's only 1 tile and it gets shrunk 2023-02-03 09:11:25 -05:00
Robin Townsend
42e4f6ce83 Don't allow the grid to overflow horizontally 2023-02-03 08:44:35 -05:00
David Baker
aabca7ebff Merge pull request #895 from vector-im/revert-893-dbkr/yarn_upgrade_jan23
Revert "Yarn upgrade"
2023-02-02 14:42:05 +00:00
David Baker
579b91abff Revert "Yarn upgrade" 2023-02-02 14:32:44 +00:00
David Baker
e3b4a695d6 Merge pull request #893 from vector-im/dbkr/yarn_upgrade_jan23
Yarn upgrade
2023-02-02 13:47:00 +00:00
David Baker
e1abbd5291 Yarn upgrade
Along with some type fixes to make typescript happy again. Hopefully
they are sensible.
2023-02-02 12:49:54 +00:00
Robin Townsend
4fc8598e36 Keep tile elements in a stable order 2023-02-01 11:50:52 -05:00
Robin Townsend
6784d2ba97 Remove redundant key prop 2023-02-01 11:50:25 -05:00
Robin Townsend
0915e327e1 Implement somewhat working drag & drop and improve render memoization 2023-02-01 11:32:10 -05:00
Robin Townsend
eedf8a6d1b Make tiles draggable (but not yet droppable) 2023-02-01 00:17:22 -05:00
Robin Townsend
d7db845f3b Scroll snap was a bad idea 2023-01-30 23:52:46 -05:00
Robin Townsend
82c7293308 Replace premature animation optimization with a potentially wiser one 2023-01-30 23:44:19 -05:00
Robin Townsend
0166eb67fb Make avatars scale smoothly during animations 2023-01-30 23:43:45 -05:00
Robin Townsend
e3081c1c06 Try out a snappier spring 2023-01-30 23:32:26 -05:00
Robin Townsend
f540f48461 Fix some layout bugs 2023-01-30 23:32:00 -05:00
Robin Townsend
55dece274f Fix some tile resizing bugs 2023-01-30 17:04:43 -05:00
Robin
b12e52d972 Merge pull request #866 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-01-30 14:00:59 -05:00
Suguru Hirahara
82f2fd05b5 Translated using Weblate (Japanese)
Currently translated at 74.4% (105 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-30 18:25:09 +00:00
Genbuchan
b4c6684ff5 Translated using Weblate (Japanese)
Currently translated at 74.4% (105 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-30 18:25:09 +00:00
Robin Townsend
4e73c07cb2 Try out scroll snapping 2023-01-30 09:35:40 -05:00
Robin Townsend
3805a2f20e Format with Prettier 2023-01-29 21:56:07 -05:00
Robin Townsend
4e35984900 Extract tile size change logic into a function 2023-01-29 21:54:53 -05:00
Robin Townsend
e99294c3f1 Simplify some code 2023-01-29 21:45:10 -05:00
DarkCoder15
32fb14107f Translated using Weblate (Russian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ru/
2023-01-27 15:33:29 +00:00
afr4283
14a5e53e65 Translated using Weblate (Polish)
Currently translated at 97.1% (137 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/
2023-01-27 15:33:29 +00:00
Šimon Brandner
24fea189dc Translated using Weblate (Czech)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/cs/
2023-01-27 15:33:29 +00:00
Genbuchan
0f73527ccf Translated using Weblate (Japanese)
Currently translated at 72.3% (102 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-27 15:33:29 +00:00
Suguru Hirahara
e0b94b51ab Translated using Weblate (Japanese)
Currently translated at 72.3% (102 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-27 15:33:29 +00:00
David Baker
ee0c623866 Merge pull request #884 from vector-im/dbkr/posthog_pr_previews
Add posthog to PR preview builds
2023-01-26 17:24:48 +00:00
Šimon Brandner
c321deecba Merge pull request #882 from dinosmm/patch-2 2023-01-26 17:27:40 +01:00
David Baker
b7ac131614 Add posthog to PR preview builds 2023-01-26 14:59:54 +00:00
Dino
acade92d70 Removed one more reference to Synapse
I forgot to remove one reference to Synapse previously.
2023-01-26 14:50:32 +00:00
Dino
6e275b9221 Clarified homeserver requirements
I clarified homeserver requirements (i.e. that Element Call needs a homeserver like Synapse but not necessarily Synapse), and also edited some other parts for clarity. I also updated the recommendation to not log in to an existing homeserver based on my new findings that Element Call *may* allow you to log in to an existing HS but log in is unreliable.
2023-01-26 14:24:53 +00:00
Dino
42d5db6d0f Update README.md
Updated README.md to include more detailed information about the limitations of Element Call and a recommended homeserver set up.
2023-01-26 10:35:30 +00:00
Robin Townsend
8912daa922 Make tiles resizable and fix some miscellaneous bugs 2023-01-25 23:51:36 -05:00
Robin Townsend
045103dbc9 Backfill the grid as people leave by moving tiles along paths 2023-01-25 02:30:52 -05:00
David Baker
0f2a62a59f Merge pull request #870 from vector-im/dbkr/fix_rageshake_modal_mobile
Fix the rageshake modal on mobile
2023-01-23 20:21:23 +00:00
David Baker
d2631a3e02 Fix the rageshake modal on mobile
As per comment

Unsure if this is the best fix - ideally we wouldn't go into no-controls
mode at all, but this part doesn't know whether the dialog is open so
the only thing we could really do is tweak the threshold, or possibly
guess based on width instead?
2023-01-23 17:52:02 +00:00
David Baker
41b72440a0 Merge pull request #869 from vector-im/dbkr/suppress_dup_ptt_unhold
Avoid duplicate PTT button 'unhold' events
2023-01-23 16:59:54 +00:00
David Baker
d65464e4db Avoid duplicate PTT button 'unhold' events
We called the 'unhold' function even if the button wasn't held which
probably will have been generating unmute events even when we weren't
muted.

Also use separate handlers for events so we can have specific log lines
(and also see where the event comes from when caught in the debugger).
2023-01-23 16:53:24 +00:00
Robin Townsend
59f3b05c07 Merge branch 'main' into big-grid 2023-01-23 08:57:04 -05:00
Robin
4f0a780ecf Merge pull request #863 from robintown/demo-screenshot
Add a demo screenshot to the README
2023-01-20 15:38:48 -05:00
David Baker
b8c1dd4c78 Merge pull request #865 from vector-im/dbkr/wait_until_loaded_before_registering
Don't try to register users until client is loaded
2023-01-20 18:32:14 +00:00
Robin
cf5e9ba2f9 Merge pull request #864 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-01-20 13:21:12 -05:00
David Baker
4f8bd18efd Don't try to register users until client is loaded 2023-01-20 17:59:57 +00:00
Suguru Hirahara
f56177b96a Translated using Weblate (Japanese)
Currently translated at 65.9% (93 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:20:44 +00:00
Genbuchan
85b206c270 Translated using Weblate (Japanese)
Currently translated at 65.9% (93 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:20:43 +00:00
Suguru Hirahara
bf7c45b0bc Translated using Weblate (Japanese)
Currently translated at 64.5% (91 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:20:30 +00:00
Genbuchan
906fcdf72e Translated using Weblate (Japanese)
Currently translated at 64.5% (91 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:20:29 +00:00
Suguru Hirahara
17a3e14d09 Translated using Weblate (Japanese)
Currently translated at 63.1% (89 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:20:07 +00:00
Genbuchan
26e1772c75 Translated using Weblate (Japanese)
Currently translated at 63.1% (89 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:20:07 +00:00
Suguru Hirahara
edfb8709d1 Translated using Weblate (Japanese)
Currently translated at 60.9% (86 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:19:33 +00:00
Genbuchan
7798128cbd Translated using Weblate (Japanese)
Currently translated at 60.9% (86 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:19:33 +00:00
Suguru Hirahara
5c3c15266a Translated using Weblate (Japanese)
Currently translated at 41.1% (58 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:14:22 +00:00
Genbuchan
ea7bfb5afb Translated using Weblate (Japanese)
Currently translated at 41.1% (58 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:14:22 +00:00
Suguru Hirahara
f940063e03 Translated using Weblate (Japanese)
Currently translated at 34.7% (49 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:12:49 +00:00
Genbuchan
a56d974f48 Translated using Weblate (Japanese)
Currently translated at 34.7% (49 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:12:49 +00:00
Suguru Hirahara
435f6f1ae9 Translated using Weblate (Japanese)
Currently translated at 31.2% (44 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:11:53 +00:00
Genbuchan
375db2a47b Translated using Weblate (Japanese)
Currently translated at 31.2% (44 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:11:53 +00:00
Suguru Hirahara
ed1b1c3d3e Translated using Weblate (Japanese)
Currently translated at 29.7% (42 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:11:39 +00:00
Genbuchan
ddb3637d79 Translated using Weblate (Japanese)
Currently translated at 29.7% (42 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:11:39 +00:00
Suguru Hirahara
85d5946d6a Translated using Weblate (Japanese)
Currently translated at 28.3% (40 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:11:26 +00:00
Genbuchan
3cdb413587 Translated using Weblate (Japanese)
Currently translated at 28.3% (40 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:11:25 +00:00
Suguru Hirahara
5ac1212988 Translated using Weblate (Japanese)
Currently translated at 21.2% (30 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:09:41 +00:00
Genbuchan
c9c0ed85f8 Translated using Weblate (Japanese)
Currently translated at 21.2% (30 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:09:40 +00:00
Suguru Hirahara
e4b2180bc2 Translated using Weblate (Japanese)
Currently translated at 19.1% (27 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:09:04 +00:00
Genbuchan
cab8a71ac2 Translated using Weblate (Japanese)
Currently translated at 19.1% (27 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:09:04 +00:00
Suguru Hirahara
ff5ff175fd Translated using Weblate (Japanese)
Currently translated at 17.7% (25 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:08:27 +00:00
Genbuchan
26e1530882 Translated using Weblate (Japanese)
Currently translated at 17.7% (25 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:08:27 +00:00
Suguru Hirahara
35386b5e16 Translated using Weblate (Japanese)
Currently translated at 17.0% (24 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:08:17 +00:00
Genbuchan
18fe2daea7 Translated using Weblate (Japanese)
Currently translated at 17.0% (24 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:08:17 +00:00
Suguru Hirahara
5f24bf0b9c Translated using Weblate (Japanese)
Currently translated at 14.1% (20 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:07:45 +00:00
Genbuchan
990a08f4f6 Translated using Weblate (Japanese)
Currently translated at 14.1% (20 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:07:45 +00:00
Suguru Hirahara
af565ecd77 Translated using Weblate (Japanese)
Currently translated at 7.0% (10 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:05:44 +00:00
Genbuchan
e3d72e1104 Translated using Weblate (Japanese)
Currently translated at 7.0% (10 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:05:44 +00:00
Suguru Hirahara
29ea2cfe90 Translated using Weblate (Japanese)
Currently translated at 5.6% (8 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:04:22 +00:00
Genbuchan
008ecd7409 Translated using Weblate (Japanese)
Currently translated at 5.6% (8 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:04:21 +00:00
Suguru Hirahara
3220b030fb Translated using Weblate (Japanese)
Currently translated at 4.9% (7 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:03:57 +00:00
Genbuchan
0b1689e6f7 Translated using Weblate (Japanese)
Currently translated at 4.9% (7 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:03:57 +00:00
Suguru Hirahara
74255d0554 Translated using Weblate (Japanese)
Currently translated at 4.2% (6 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:03:43 +00:00
Genbuchan
a55046148f Translated using Weblate (Japanese)
Currently translated at 4.2% (6 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:03:43 +00:00
Genbuchan
6acbf792fc Translated using Weblate (Japanese)
Currently translated at 2.8% (4 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:02:49 +00:00
Suguru Hirahara
1cbda01051 Translated using Weblate (Japanese)
Currently translated at 2.1% (3 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:02:33 +00:00
Suguru Hirahara
f61b2db80d Translated using Weblate (Japanese)
Currently translated at 2.1% (3 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:02:33 +00:00
Genbuchan
17d273135f Translated using Weblate (Japanese)
Currently translated at 2.1% (3 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/
2023-01-20 17:02:33 +00:00
Robin Townsend
9e8dadcc44 Add a demo screenshot to the README 2023-01-20 10:51:28 -05:00
David Baker
785756dc91 Merge pull request #862 from vector-im/dbkr/ptt_null_member_workarounds
Don't crash if we can't find our own member event
2023-01-20 13:20:04 +00:00
David Baker
785fa51e0c Use merged js-sdk commit 2023-01-20 13:12:03 +00:00
David Baker
47c2e9e101 Don't crash if we can't find our own member event 2023-01-20 12:10:58 +00:00
David Baker
0bb18be4ef Merge pull request #861 from vector-im/dbkr/dont_lowercase_room_ids
Fix joining rooms by ID
2023-01-20 10:57:46 +00:00
David Baker
81997624d4 Fix joining rooms by ID
We use this in embedded mode. Regressed by https://github.com/vector-im/element-call/pull/860

Lowercasing room IDs obviously makes them break, so… don't do that.
2023-01-20 10:51:52 +00:00
David Baker
c2883e52bb Merge pull request #860 from vector-im/dbkr/lowercase_room_alias
Lowercase room alias before joining
2023-01-20 09:43:28 +00:00
David Baker
7e1033f5a4 Add colon in comment
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-01-20 09:35:50 +00:00
David Baker
524f530dce Lowercase room alias before joining 2023-01-19 19:20:42 +00:00
Robin Townsend
46d1351d83 More fixes 2023-01-18 13:38:29 -05:00
Robin Townsend
2318d75bc7 prettier 2023-01-18 11:33:40 -05:00
Robin Townsend
486674c442 fixes 2023-01-18 11:32:51 -05:00
Robin Townsend
d3fba7fd5f WIP minus unfinished split grid layouts 2023-01-18 10:52:12 -05:00
David Baker
9437a00997 Merge pull request #853 from vector-im/dbkr/better_rageshake
Change rageshake to save much more regularly
2023-01-17 10:20:26 +00:00
David Baker
e1c4042d15 Add units to constant 2023-01-17 10:16:55 +00:00
David Baker
860aff4958 Change rageshake persistence to throttled flushing
Rather than every 30 seconds. This way we'll save logs for sessions
lasting less than 30 seconds which we previously didn't. Also save
on window unload just in case that doesn't catch everything.

Plus remove some more unused params.
2023-01-16 17:27:49 +00:00
David Baker
13b1dcf785 Mostly cosmetic fixes to rageshake
* Remove duplicate copyright header
 * Remove ts-ignores by just using the objects directly rather than via
   event.target
 * Use error.message rather than errorCode which TS doesn't know about
   and may or may not exist.
 * Remove some unused things like the skip rageshake function and
   the option to init rageshakes without storage.
 * Turn single function with a boolean param to make it take two entirely
   separate code paths into two functions.
2023-01-16 16:43:45 +00:00
Robin
035498a8eb Merge pull request #849 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-01-15 22:20:48 -05:00
Avery
030ca29664 Translated using Weblate (Spanish)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/es/
2023-01-14 15:33:25 +00:00
David Baker
ecdeb97502 Merge pull request #848 from vector-im/dbkr/uncecryptable_todevice
Log undecryptable to-device events
2023-01-13 21:44:39 +00:00
David Baker
6168109894 Use merged js-sdk commit 2023-01-13 21:41:21 +00:00
David Baker
a2a1a9032a Use js-sdk from branch 2023-01-13 18:33:58 +00:00
David Baker
abd909c03a Log undecryptable to-device events
Listen for the new undecryptable to-device event event and log
events for it in Posthog & Sentry, and make it visible in the
call flow diagram.
2023-01-13 18:27:22 +00:00
David Baker
be1db442d9 Merge pull request #846 from vector-im/dbkr/prevent_keyrepeat_mute_spam
Prevent mute event spam from key repeats
2023-01-13 11:59:34 +00:00
David Baker
afdce66896 Merge remote-tracking branch 'origin/main' into dbkr/prevent_keyrepeat_mute_spam 2023-01-13 11:57:02 +00:00
David Baker
1b08a5cac3 Rename file 2023-01-13 11:56:29 +00:00
David Baker
843e7690fa Merge pull request #845 from vector-im/dbkr/disable_keyboard_shortcuts_rageshake
Disable keyboard shortcuts when feedback modal is open
2023-01-13 11:55:51 +00:00
David Baker
a5977fc992 Rename to useCallViewKeyboardShortcuts 2023-01-13 11:52:40 +00:00
David Baker
6277359d30 Merge pull request #844 from vector-im/dbkr/fix_cache
Fix caching headers on Docker image
2023-01-13 10:39:50 +00:00
David Baker
73682b67ba Merge pull request #843 from vector-im/dbkr/dev_mode_idb_no_worker
Use IndexedDB storage in dev mode, just without the worker
2023-01-13 10:14:01 +00:00
David Baker
f2193302c1 Prevent mute event spam from key repeats 2023-01-12 18:26:21 +00:00
David Baker
9ba4ce429f Disable keyboard shortcuts when feedback modal is open 2023-01-12 17:31:19 +00:00
David Baker
d9b0e08ea2 Fix caching headers on Docker image 2023-01-12 16:20:37 +00:00
David Baker
5f26534496 Use IndexedDB storage in dev mode, just without the worker
As per comment, we can't use workers in Vite dev mode. We previously
fell back to the memory store but this ends up with it working significantly
differently in dev mode to production, eg. dev mode would always start
by doing an initial sync, so old to-device messages would arrive again.

There's no need to fall all the way back to the memory store though,
we can use the IndexedDB store without the worker.
2023-01-12 15:17:46 +00:00
David Baker
30688715cd Revert f20fc78bd7 2023-01-12 13:20:11 +00:00
David Baker
f20fc78bd7 Use branch of js-sdk with Olm debugging
Pulls in changes from https://github.com/matrix-org/matrix-js-sdk/pull/3055

Not intended to stay long-term.
2023-01-12 11:28:15 +00:00
Robin
741233909d Merge pull request #829 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2023-01-09 13:35:43 -05:00
Robin
4e0f4a8dc7 Merge pull request #835 from robintown/unmuted-when-speaking
Work around mute state updates being slow
2023-01-09 13:35:05 -05:00
Šimon Brandner
0d151452ba Merge pull request #833 from vector-im/SimonBrandner/feat/hide-audio 2023-01-09 19:28:04 +01:00
Robin Townsend
4fd76f9599 Work around mute state updates being slow
Since the app already determines when someone is speaking, we can use that information to make it less obvious when to-device messages are being slow to deliver mute state updates.
2023-01-09 11:10:59 -05:00
Robin
d123793deb Merge pull request #832 from robintown/update-js-sdk
Update matrix-js-sdk
2023-01-09 10:52:19 -05:00
Robin Townsend
449c1c9d79 Try updating Olm to fix type errors 2023-01-09 10:49:01 -05:00
Robin Townsend
de5b58792e Update matrix-js-sdk 2023-01-09 10:32:05 -05:00
Robin Townsend
7769074410 Merge branch 'main' into update-js-sdk 2023-01-09 10:31:39 -05:00
Šimon Brandner
881054e265 Hide local volume controls for tiles with no audio
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-01-07 10:09:20 +01:00
Robin
767f9cdc4a Merge pull request #831 from robintown/no-video-mute
Leave audio elements unmuted regardless of mute state
2023-01-06 12:08:13 -05:00
Robin Townsend
946f564f84 Update matrix-js-sdk 2023-01-06 10:39:29 -05:00
Robin Townsend
468e389324 Leave audio elements unmuted regardless of mute state 2023-01-06 10:26:10 -05:00
Jozef Gaal
62e98f6c47 Translated using Weblate (Slovak)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/
2023-01-06 09:33:22 +00:00
Priit Jõerüüt
de31c099e3 Translated using Weblate (Estonian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/et/
2023-01-06 09:33:22 +00:00
Ihor Hordiichuk
49cae76387 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/
2023-01-06 09:33:22 +00:00
Glandos
d45ea78ddb Translated using Weblate (French)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2023-01-06 09:33:22 +00:00
Linerly
dcbc3ed865 Translated using Weblate (Indonesian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2023-01-06 09:33:22 +00:00
Vri
ff19135d4e Translated using Weblate (German)
Currently translated at 100.0% (141 of 141 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2023-01-06 09:33:22 +00:00
Robin
de7343d16a Merge pull request #821 from robintown/save-lockfile
Save lockfile
2023-01-05 10:35:52 -05:00
Robin Townsend
c09fec5f88 Save lockfile 2023-01-04 08:25:26 -05:00
135 changed files with 7817 additions and 1828 deletions

24
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Run E2E tests
on:
workflow_run:
workflows: ["deploy"]
types:
- completed
branches-ignore:
- "main"
jobs:
e2e:
name: E2E tests runs on Element Call
runs-on: ubuntu-latest
steps:
- name: Check out test private repo
uses: actions/checkout@v3
with:
repository: vector-im/static-call-participant
ref: refs/heads/main
path: static-call-participant
token: ${{ secrets.GH_E2E_TEST_TOKEN }}
- name: Build E2E Image
run: "cd static-call-participant && docker build --no-cache --tag matrixdotorg/chrome-node-static-call-participant:latest ."
- name: Run E2E tests in container
run: "docker run --rm -v '${{ github.workspace }}/static-call-participant/callemshost-users.txt:/opt/app/callemshost-users.txt' matrixdotorg/chrome-node-static-call-participant:latest ./e2e.sh"

88
.github/workflows/netlify-livekit.yaml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Netlify LiveKit Experiment
on:
workflow_run:
workflows: ["Build"]
types:
- completed
branches:
- "livekit-experiment"
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
deployments: write
# Important: the 'branches' filter above will match the 'livekit-experiment' branch on forks,
# so we need to check the head repo too in order to not run on PRs from forks
# We check the branch name again too just for completeness
# (Is there a nicer way to see if a PR is from a fork?)
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == 'vector-im/element-call' && github.event.workflow_run.head_branch == 'livekit-experiment'
steps:
- name: Create Deployment
uses: bobheadxi/deployments@v1
id: deployment
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: livekit-experiment-branch-cd
ref: ${{ github.event.workflow_run.head_sha }}
- name: "Download artifact"
uses: actions/github-script@v3.1.0
with:
script: |
const artifacts = await github.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "build"
})[0];
const download = await github.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
const fs = require('fs');
fs.writeFileSync('${{github.workspace}}/build.zip', Buffer.from(download.data));
- name: Extract Artifacts
run: unzip -d dist build.zip && rm build.zip
- name: Add redirects file
# We fetch from github directly as we don't bother checking out the repo
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/livekit-experiment/config/netlify_redirects > dist/_redirects
- name: Add config file
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/livekit-experiment/config/element_io_preview.json > dist/config.json
- name: Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@v1.2.3
with:
publish-dir: dist
deploy-message: "Deploy from GitHub Actions"
production-branch: livekit-experiment
production-deploy: true
# These don't work because we're in workflow_run
enable-pull-request-comment: false
enable-commit-comment: false
github-deployment-environment: livekit-experiment
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: e3b9fa82-c040-4db6-b4bf-42b524d57423
timeout-minutes: 1
- name: Update deployment status
uses: bobheadxi/deployments@v1
if: always()
with:
step: finish
override: false
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
env: ${{ steps.deployment.outputs.env }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env_url: ${{ steps.netlify.outputs.deploy-url }}

View File

@@ -1,6 +1,8 @@
name: Run jest tests
on:
pull_request: {}
push:
branches: [livekit]
jobs:
jest:
name: Run jest tests
@@ -16,3 +18,7 @@ jobs:
run: "yarn install"
- name: Jest
run: "yarn run test"
- name: Upload to codecov
uses: codecov/codecov-action@v3
with:
flags: unittests

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ dist-ssr
*.local
.idea/
public/config.json
/coverage

View File

@@ -5,6 +5,8 @@
Full mesh group calls powered by [Matrix](https://matrix.org), implementing [MatrixRTC](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md).
![A demo of Element Call with six people](demo.jpg)
To try it out, visit our hosted version at [call.element.io](https://call.element.io). You can also find the latest development version continuously deployed to [element-call.netlify.app](https://element-call.netlify.app).
## Host it yourself
@@ -40,6 +42,28 @@ server {
}
```
By default, the app expects you to have a Matrix homeserver (such as [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html)) installed locally and running on port 8008. If you wish to use a homeserver on a different URL or one that is hosted on a different server, you can add a config file as above, and include the homeserver URL that you'd like to use.
Element Call requires a homeserver with registration enabled without any 3pid or token requirements, if you want it to be used by unregistered users. Furthermore, it is not recommended to use it with an existing homeserver where user accounts have joined normal rooms, as it may not be able to handle those yet and it may behave unreliably.
Therefore, to use a self-hosted homeserver, this is recommended to be a new server where any user account created has not joined any normal rooms anywhere in the Matrix federated network. The homeserver used can be setup to disable federation, so as to prevent spam registrations (if you keep registrations open) and to ensure Element Call continues to work in case any user decides to log in to their Element Call account using the standard Element app and joins normal rooms that Element Call cannot handle.
### Features
#### Allow joining group calls without a camera and a microphone
You can allow joining a group call without video and audio enabling this feature in your `config.json`:
```json
{
...
"features": {
"feature_group_calls_without_video_and_audio": true
}
}
```
## Development
Element Call is built against [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/2553). To get started, clone, install, and link the package:
@@ -60,8 +84,6 @@ yarn
yarn link matrix-js-sdk
```
By default, the app expects you to have [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008. If you wish to use another homeserver, you can add a config file as above.
You're now ready to launch the development server:
```

View File

@@ -5,6 +5,10 @@
"server_name": "call.ems.host"
}
},
"posthog": {
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
"api_host": "https://posthog-element-call.element.io"
},
"rageshake": {
"submit_url": "https://element.io/bugreports/submit"
}

View File

@@ -2,9 +2,24 @@ server {
listen 8080;
server_name localhost;
root /app;
location / {
root /app;
# disable cache entriely by default (apart from Etag which is accurate enough)
add_header Cache-Control 'private no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
# also turn off last-modified since they are just the timestamps of the file in the docker image
# and may or may not bear any resemblance to when the resource changed
add_header Last-Modified "";
try_files $uri /$uri /index.html;
}
# assets can be cached because they have hashed filenames
location /assets {
expires 1w;
add_header Cache-Control "public, no-transform";
}
}

18
config/otel_dev/README.md Normal file
View File

@@ -0,0 +1,18 @@
# OpenTelemetry Collector for development
This directory contains a docker compose file that starts a jaeger all-in-one instance
with an in-memory database, along with a standalone OpenTelemetry collector that forwards
traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be
configured to send CORS headers so can't be used from a browser. This sets the config on
the collector to send CORS headers.
This also adds an nginx to add CORS headers to the jaeger query endpoint, such that it can
be used from webapps like stalk (https://deniz.co/stalk/). The CORS enabled endpoint is
exposed on port 16687. To use stalk, you should simply be able to navigate to it and add
http://127.0.0.1:16687/api as a data source.
(Yes, we could enable the OTLP collector in jaeger all-in-one and passed this through
the nginx to enable CORS too, rather than running a separate collector. There's no reason
it's done this way other than that I'd already set up the separate collector.)
Running `docker compose up` in this directory should be all you need.

View File

@@ -0,0 +1,41 @@
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
cors:
allowed_origins:
# This can't be '*' because opentelemetry-js uses sendBeacon which always operates
# in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*'
#- "https://pr976--element-call.netlify.app"
- "http://*"
allowed_headers:
- "*"
processors:
batch:
timeout: 1s
resource:
attributes:
- key: test.key
value: "test-value"
action: insert
exporters:
logging:
loglevel: info
jaeger:
endpoint: jaeger-all-in-one:14250
tls:
insecure: true
extensions:
health_check:
pprof:
endpoint: :1888
zpages:
endpoint: :55679
service:
extensions: [pprof, zpages, health_check]
pipelines:
traces:
receivers: [otlp]
processors: [batch, resource]
exporters: [logging, jaeger]

View File

@@ -0,0 +1,29 @@
version: "2"
services:
# Jaeger
jaeger-all-in-one:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
- "14268"
- "14250"
# Collector
collector-gateway:
image: otel/opentelemetry-collector:latest
volumes:
- ./collector-gateway.yaml:/etc/collector-gateway.yaml
command: ["--config=/etc/collector-gateway.yaml"]
ports:
- "1888:1888" # pprof extension
- "13133:13133" # health_check extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "55670:55679" # zpages extension
depends_on:
- jaeger-all-in-one
nginx:
image: nginxinc/nginx-unprivileged:latest
volumes:
- ./nginx_otel.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "16687:8080"

View File

@@ -0,0 +1,16 @@
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://jaeger-all-in-one:16686/;
add_header Access-Control-Allow-Origin *;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin *;
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}

BIN
demo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
"serve": "vite preview",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
@@ -18,7 +18,14 @@
},
"dependencies": {
"@juggle/resize-observer": "^3.3.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.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/sdk-trace-web": "^1.9.1",
"@react-aria/button": "^3.3.4",
"@react-aria/dialog": "^3.1.4",
"@react-aria/focus": "^3.5.0",
@@ -45,8 +52,9 @@
"i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#2c8eece5ca5333c6e6a14e8ed53f359ed0e9e9bf",
"matrix-widget-api": "^1.0.0",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f884c78579c336a03bc20ff8f4e92c46582822b6",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
"pako": "^2.0.4",
@@ -62,6 +70,7 @@
"react-use-clipboard": "^1.0.7",
"react-use-measure": "^2.1.1",
"sdp-transform": "^2.14.1",
"tinyqueue": "^2.0.3",
"unique-names-generator": "^4.6.0"
},
"devDependencies": {
@@ -109,6 +118,11 @@
"\\.(css|less|svg)+$": "identity-obj-proxy",
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
}
},
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"
]
}
}

View File

@@ -23,7 +23,6 @@
"Create account": "Създай акаунт",
"Debug log": "Debug логове",
"Debug log request": "Заявка за debug логове",
"Description (optional)": "Описание (незадължително)",
"Details": "Детайли",
"Developer": "Разработчик",
"Display name": "Име/псевдоним",
@@ -34,7 +33,6 @@
"Full screen": "Цял екран",
"Go": "Напред",
"Grid layout menu": "Меню \"решетков изглед\"",
"Having trouble? Help us fix it.": "Имате проблем? Помогнете да го поправим.",
"Home": "Начало",
"Include debug logs": "Включи debug логове",
"Incompatible versions": "Несъвместими версии",
@@ -46,7 +44,6 @@
"Join call now": "Влез в разговора сега",
"Join existing call?": "Присъединяване към съществуващ разговор?",
"Leave": "Напусни",
"Loading room…": "Напускане на стаята…",
"Loading…": "Зареждане…",
"Local volume": "Локална сила на звука",
"Logging in…": "Влизане…",
@@ -56,7 +53,6 @@
"Microphone permissions needed to join the call.": "Необходими са разрешения за микрофона за да можете да се присъедините в разговора.",
"Microphone {{n}}": "Микрофон {{n}}",
"More": "Още",
"More menu": "Мено \"още\"",
"Mute microphone": "Заглуши микрофона",
"No": "Не",
"Not now, return to home screen": "Не сега, върни се на началния екран",
@@ -77,8 +73,6 @@
"Release to stop": "Отпуснете за да спрете",
"Remove": "Премахни",
"Return to home screen": "Връщане на началния екран",
"Save": "Запази",
"Saving…": "Запазване…",
"Select an option": "Изберете опция",
"Send debug logs": "Изпратете debug логове",
"Sending…": "Изпращане…",
@@ -93,7 +87,6 @@
"Spotlight": "Прожектор",
"Stop sharing screen": "Спри споделянето на екрана",
"Submit feedback": "Изпрати обратна връзка",
"Submitting feedback…": "Изпращане на обратна връзка…",
"Take me Home": "Отиди в Начало",
"Talk over speaker": "Говорете заедно с говорителя",
"Talking…": "Говорене…",
@@ -104,7 +97,6 @@
"Turn off camera": "Изключи камерата",
"Turn on camera": "Включи камерата",
"Unmute microphone": "Включи микрофона",
"User ID": "Потребителски идентификатор",
"User menu": "Потребителско меню",
"Username": "Потребителско име",
"Version: {{version}}": "Версия: {{version}}",
@@ -121,7 +113,6 @@
"Your recent calls": "Скорошните ви разговори",
"{{count}} people connected|one": "{{count}} човек се свърза",
"{{count}} people connected|other": "{{count}} човека се звързаха",
"{{displayName}}, your call is now ended": "{{displayName}}, разговорът ви приключи",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} презентира",
"{{name}} is talking…": "{{name}} говори…",

View File

@@ -26,14 +26,12 @@
"Version: {{version}}": "Verze: {{version}}",
"Username": "Uživatelské jméno",
"User menu": "Uživatelské menu",
"User ID": "ID uživatele",
"Unmute microphone": "Zapnout mikrofon",
"Turn on camera": "Zapnout kameru",
"Turn off camera": "Vypnout kameru",
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
"Thanks! We'll get right on it.": "Děkujeme! Hned se na to vrhneme.",
"Take me Home": "Domovská obrazovka",
"Submitting feedback…": "Odesílání zpětné vazby…",
"Submit feedback": "Dát feedback",
"Stop sharing screen": "Zastavit sdílení obrazovek",
"Speaker {{n}}": "Reproduktor {{n}}",
@@ -48,8 +46,6 @@
"Sending debug logs…": "Posílání ladícího záznamu…",
"Send debug logs": "Poslat ladící záznam",
"Select an option": "Vyberte možnost",
"Saving…": "Ukládání…",
"Save": "Uložit",
"Return to home screen": "Vrátit se na domácí obrazovku",
"Remove": "Odstranit",
"Registering…": "Registrování…",
@@ -66,12 +62,11 @@
"Microphone permissions needed to join the call.": "Přístup k mikrofonu je nutný pro připojení se k hovoru.",
"Microphone {{n}}": "Mikrofon {{n}}",
"Microphone": "Mikrofon",
"Login to your account": "Přihlásit se ke svůmu účtu",
"Login to your account": "Přihlásit se ke svému účtu",
"Login": "Přihlášení",
"Logging in…": "Přihlašování se…",
"Local volume": "Lokální hlasitost",
"Loading…": "Načítání…",
"Loading room…": "Načítání místnosti…",
"Leave": "Opustit hovor",
"Join call now": "Připojit se k hovoru",
"Join call": "Připojit se k hovoru",
@@ -83,7 +78,6 @@
"{{name}} is talking…": "{{name}} mluví…",
"{{name}} is presenting": "{{name}} prezentuje",
"{{name}} (Connecting...)": "{{name}} (Připojení...)",
"{{displayName}}, your call is now ended": "{{displayName}}, váš hovor je nyní ukončen",
"{{count}} people connected|other": "{{count}} lidí připojeno",
"{{count}} people connected|one": "{{count}} lidí připojeno",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Tato možnost způsobí, že zvuk účastníků hovoru se bude tvářit jako by přicházel z místa, kde jsou umístěni na obrazovce.(Experimentální možnost: může způsobit nestabilitu audia.)",
@@ -91,13 +85,10 @@
"Walkie-talkie call name": "Jméno vysílačkového hovoru",
"Walkie-talkie call": "Vysílačkový hovor",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Vysílačkový hovor",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Zapnout jedno-klávesové zkratky, např. 'm' pro vypnutí/zapnutí mikrofonu.",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Toto bude odesílat anonymizovaná data (jako délku a počet účastníků hovoru) týmu Element Call, aby nám pomohly zlepšovat aplikaci podle toho, jak je používaná.",
"Talking…": "Mluvení…",
"Talk over speaker": "Mluvit přes mluvčího",
"Spotlight": "Soustředěný mód",
"Single-key keyboard shortcuts": "Jedno-klávesová klávesnice",
"Release to stop": "Pusťte pro ukončení",
"Release spacebar key to stop": "Pusťte mezerník pro ukončení",
"Recaptcha not loaded": "Recaptcha se nenačetla",
@@ -106,11 +97,9 @@
"Press and hold spacebar to talk over {{name}}": "Zmáčkněte a držte mezerník, abyste mluvili přes {{name}}",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Ostatní uživatelé se pokoušejí připojit k tomuto hovoru s nekompatibilních verzí. Tito uživatelé by se měli ujistit, že stránku načetli znovu:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "Nejste registrovaní? <2>Vytvořit účet</2>",
"More menu": "Další možnosti",
"Join existing call?": "Připojit se k existujícimu hovoru?",
"Include debug logs": "Zahrnout ladící záznamy",
"Home": "Domov",
"Having trouble? Help us fix it.": "Máte problémy? Pomozte nám je spravit.",
"Grid layout menu": "Menu rozložení",
"Go": "Pokračovat",
"Full screen": "Zvětšit na celou obrazovku",
@@ -122,7 +111,6 @@
"Display name": "Zobrazované jméno",
"Developer": "Vývojář",
"Details": "Detaily",
"Description (optional)": "Popis (nepovinný)",
"Debug log request": "Žádost o protokoly ladění",
"Debug log": "Protokoly ladění",
"Create account": "Vytvořit účet",
@@ -131,10 +119,14 @@
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions</2>",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.",
"Allow analytics": "Povolit analytiku",
"Advanced": "Pokročilé",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?</0><1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory </1>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Připojit se</0><1>Or</1><2>Zkopírovat odkaz a připojit se později</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Už máte účet?</0><1><0>Přihlásit se</0> Or <2>Jako host</2></1>",
"{{name}} (Waiting for video...)": "{{name}} (Čekání na video...)"
"{{name}} (Waiting for video...)": "{{name}} (Čekání na video...)",
"This feature is only supported on Firefox.": "Tato funkce je podporována jen ve Firefoxu.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Odeslání ladících záznamů nám pomůže diagnostikovat problém.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Oops, něco se pokazilo.</0>",
"Use the upcoming grid system": "Používat nový systém pro zobrazení videí",
"Expose developer settings in the settings window.": "Zobrazit vývojářské nastavení.",
"Developer Settings": "Vývojářské nastavení"
}

View File

@@ -23,7 +23,6 @@
"Create account": "Konto erstellen",
"Debug log": "Debug-Protokoll",
"Debug log request": "Debug-Log Anfrage",
"Description (optional)": "Beschreibung (optional)",
"Details": "Details",
"Developer": "Entwickler",
"Display name": "Anzeigename",
@@ -33,7 +32,6 @@
"Full screen": "Vollbild",
"Go": "Los gehts",
"Grid layout menu": "Grid-Layout-Menü",
"Having trouble? Help us fix it.": "Du hast ein Problem? Hilf uns, es zu beheben.",
"Home": "Startseite",
"Include debug logs": "Debug-Protokolle einschließen",
"Incompatible versions": "Inkompatible Versionen",
@@ -45,7 +43,6 @@
"Join call now": "Anruf beitreten",
"Join existing call?": "An bestehendem Anruf teilnehmen?",
"Leave": "Verlassen",
"Loading room…": "Lade Raum …",
"Loading…": "Lade …",
"Local volume": "Lokale Lautstärke",
"Logging in…": "Anmelden …",
@@ -55,7 +52,6 @@
"Microphone permissions needed to join the call.": "Mikrofon-Berechtigung ist erforderlich, um dem Anruf beizutreten.",
"Microphone {{n}}": "Mikrofon {{n}}",
"More": "Mehr",
"More menu": "Weiteres Menü",
"Mute microphone": "Mikrofon stummschalten",
"No": "Nein",
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
@@ -76,8 +72,6 @@
"Release to stop": "Loslassen zum Stoppen",
"Remove": "Entfernen",
"Return to home screen": "Zurück zum Startbildschirm",
"Save": "Speichern",
"Saving…": "Speichere …",
"Select an option": "Wähle eine Option",
"Send debug logs": "Debug-Logs senden",
"Sending…": "Senden …",
@@ -92,7 +86,6 @@
"Spotlight": "Rampenlicht",
"Stop sharing screen": "Beenden der Bildschirmfreigabe",
"Submit feedback": "Rückmeldung geben",
"Submitting feedback…": "Sende Rückmeldung …",
"Take me Home": "Zurück zur Startseite",
"Talk over speaker": "Aktiven Sprecher verdrängen und sprechen",
"Talking…": "Sprechen …",
@@ -103,7 +96,6 @@
"Turn off camera": "Kamera ausschalten",
"Turn on camera": "Kamera einschalten",
"Unmute microphone": "Mikrofon aktivieren",
"User ID": "Benutzer-ID",
"User menu": "Benutzermenü",
"Username": "Benutzername",
"Version: {{version}}": "Version: {{version}}",
@@ -119,7 +111,6 @@
"Your recent calls": "Deine letzten Anrufe",
"{{count}} people connected|one": "{{count}} Person verbunden",
"{{count}} people connected|other": "{{count}} Personen verbunden",
"{{displayName}}, your call is now ended": "{{displayName}}, dein Anruf wurde beendet",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} präsentiert",
"{{name}} is talking…": "{{name}} spricht …",
@@ -129,13 +120,27 @@
"Sending debug logs…": "Sende Debug-Protokolle …",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Anruf beitreten</0><1>Oder</1><2>Anruflink kopieren und später beitreten</2>",
"{{name}} (Connecting...)": "{{name}} (verbindet sich …)",
"Allow analytics": "Analysedaten senden",
"Advanced": "Erweitert",
"Copy": "Kopieren",
"Element Call Home": "Element Call-Startseite",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Dies wird anonymisierte Daten (wie z. B. die Dauer eines Anrufs und die Zahl der Teilnehmenden) dem Element Call-Team senden, um uns bei der Optimierung der Anwendung basierend auf dem Nutzungsverhalten zu helfen.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Ob Tastenkürzel mit nur einer Taste aktiviert sein sollen, z. B. „m“ um das Mikrofon stumm/aktiv zu schalten.",
"Single-key keyboard shortcuts": "Ein-Tasten-Tastenkürzel",
"{{name}} (Waiting for video...)": "{{name}} (Warte auf Video …)",
"This feature is only supported on Firefox.": "Diese Funktion wird nur in Firefox unterstützt."
"This feature is only supported on Firefox.": "Diese Funktion wird nur in Firefox unterstützt.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Hoppla, etwas ist schiefgelaufen.</0>",
"Use the upcoming grid system": "Nutze das kommende Rastersystem",
"Expose developer settings in the settings window.": "Zeige die Entwicklereinstellungen im Einstellungsfenster.",
"Developer Settings": "Entwicklereinstellungen",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Mit der Teilnahme an der Beta akzeptierst du die Sammlung von anonymen Daten, die wir zur Verbesserung des Produkts verwenden. Weitere Informationen zu den von uns erhobenen Daten findest du in unserer <2>Datenschutzerklärung</2> und unseren <5>Cookie-Richtlinien</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.",
"Feedback": "Rückmeldung",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.",
"Your feedback": "Deine Rückmeldung",
"Thanks, we received your feedback!": "Danke, wir haben deine Rückmeldung erhalten!",
"Submitting…": "Sende …",
"Submit": "Absenden",
"{{count}} stars|other": "{{count}} Sterne",
"{{displayName}}, your call has ended.": "{{displayName}}, dein Anruf wurde beendet.",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Wir würden uns freuen, deine Rückmeldung zu hören, um deine Erfahrung verbessern zu können.</0>",
"How did it go?": "Wie ist es gelaufen?",
"{{count}} stars|one": "{{count}} Stern",
"<0>Thanks for your feedback!</0>": "<0>Danke für deine Rückmeldung!</0>"
}

View File

@@ -0,0 +1,95 @@
{
"You can't talk at the same time": "Δεν μπορείς να μιλάς ταυτόχρονα",
"Version: {{version}}": "Έκδοση: {{version}}",
"User menu": "Μενού χρήστη",
"Submit feedback": "Υποβάλετε σχόλια",
"Stop sharing screen": "Διακοπή κοινής χρήσης οθόνης",
"Sign in": "Σύνδεση",
"Show call inspector": "Εμφάνιση του επιθεωρητή κλήσης",
"Share screen": "Κοινή χρήση οθόνης",
"Sending…": "Αποστολή…",
"Select an option": "Επιλέξτε μια επιλογή",
"Remove": "Αφαίρεση",
"Registering…": "Εγγραφή…",
"Press and hold to talk": "Πατήστε παρατεταμένα για να μιλήσετε",
"Not registered yet? <2>Create an account</2>": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό</2>",
"Login to your account": "Συνδεθείτε στο λογαριασμό σας",
"Logging in…": "Σύνδεση…",
"Invite people": "Προσκαλέστε άτομα",
"Invite": "Πρόσκληση",
"Inspector": "Επιθεωρητής",
"Incompatible versions!": "Μη συμβατές εκδόσεις!",
"Incompatible versions": "Μη συμβατές εκδόσεις",
"Display name": "Εμφανιζόμενο όνομα",
"Developer Settings": "Ρυθμίσεις προγραμματιστή",
"Debug log request": "Αίτημα αρχείου καταγραφής",
"Call link copied": "Ο σύνδεσμος κλήσης αντιγράφηκε",
"Avatar": "Avatar",
"Accept microphone permissions to join the call.": "Αποδεχτείτε τα δικαιώματα μικροφώνου για να συμμετάσχετε στην κλήση.",
"Accept camera/microphone permissions to join the call.": "Αποδεχτείτε τα δικαιώματα κάμερας/μικροφώνου για να συμμετάσχετε στην κλήση.",
"<0>Oops, something's gone wrong.</0>": "<0>Ωχ, κάτι πήγε στραβά.</0>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Δημιουργήστε λογαριασμό</0> Ή <2>Συμμετέχετε ως επισκέπτης</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Έχετε ήδη λογαριασμό;</0><1><0>Συνδεθείτε</0> Ή <2>Συμμετέχετε ως επισκέπτης</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Κλήση walkie-talkie",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"Your recent calls": "Οι πρόσφατες κλήσεις σας",
"Yes, join call": "Ναι, συμμετοχή στην κλήση",
"WebRTC is not supported or is being blocked in this browser.": "Το WebRTC δεν υποστηρίζεται ή έχει αποκλειστεί σε αυτό το πρόγραμμα περιήγησης.",
"Walkie-talkie call name": "Όνομα κλήσης walkie-talkie",
"Walkie-talkie call": "Κλήση walkie-talkie",
"Waiting for other participants…": "Αναμονή για άλλους συμμετέχοντες…",
"Waiting for network": "Αναμονή για δίκτυο",
"Video call name": "Όνομα βίντεο κλήσης",
"Video call": "Βίντεο κλήση",
"Video": "Βίντεο",
"Username": "Όνομα χρήστη",
"Turn on camera": "Ενεργοποιήστε την κάμερα",
"Turn off camera": "Απενεργοποιήστε την κάμερα",
"This feature is only supported on Firefox.": "Αυτή η δυνατότητα υποστηρίζεται μόνο στον Firefox.",
"This call already exists, would you like to join?": "Αυτή η κλήση υπάρχει ήδη, θα θέλατε να συμμετάσχετε;",
"Speaker": "Ηχείο",
"Spatial audio": "Χωρικός ήχος",
"Sign out": "Αποσύνδεση",
"Settings": "Ρυθμίσεις",
"Return to home screen": "Επιστροφή στην αρχική οθόνη",
"Register": "Εγγραφή",
"Profile": "Προφίλ",
"Press and hold spacebar to talk": "Για να μιλήσετε κρατήστε πατημένο το πλήκτρο διαστήματος",
"Passwords must match": "Οι κωδικοί πρέπει να ταιριάζουν",
"Password": "Κωδικός",
"Not now, return to home screen": "Όχι τώρα, επιστροφή στην αρχική οθόνη",
"No": "Όχι",
"Mute microphone": "Σίγαση μικροφώνου",
"More": "Περισσότερα",
"Microphone permissions needed to join the call.": "Απαιτούνται δικαιώματα μικροφώνου για συμμετοχή στην κλήση.",
"Microphone {{n}}": "Μικρόφωνο {{n}}",
"Microphone": "Μικρόφωνο",
"Login": "Σύνδεση",
"Loading…": "Φόρτωση…",
"Leave": "Αποχώρηση",
"Join existing call?": "Συμμετοχή στην υπάρχουσα κλήση;",
"Join call now": "Συμμετοχή στην κλήση τώρα",
"Join call": "Συμμετοχή στην κλήση",
"Go": "Μετάβαση",
"Full screen": "Πλήρη οθόνη",
"Exit full screen": "Έξοδος από πλήρη οθόνη",
"Details": "Λεπτομέρειες",
"Create account": "Δημιουργία λογαριασμού",
"Copy and share this call link": "Αντιγράψτε και μοιραστείτε αυτόν τον σύνδεσμο κλήσης",
"Copy": "Αντιγραφή",
"Copied!": "Αντιγράφηκε!",
"Connection lost": "Η σύνδεση χάθηκε",
"Confirm password": "Επιβεβαίωση κωδικού",
"Close": "Κλείσιμο",
"Change layout": "Αλλαγή διάταξης",
"Camera/microphone permissions needed to join the call.": "Απαιτούνται δικαιώματα κάμερας/μικροφώνου για να συμμετάσχετε στην κλήση.",
"Camera {{n}}": "Κάμερα {{n}}",
"Camera": "Κάμερα",
"Audio": "Ήχος",
"{{name}} is talking…": "{{name}} ομιλεί…",
"{{name}} is presenting": "{{name}} παρουσιάζει",
"{{name}} (Waiting for video...)": "{{name}} (Αναμονή για βίντεο...)",
"{{name}} (Connecting...)": "{{name}} (Συνδέεται...)",
"{{count}} people connected|other": "{{count}} άτομα συνδεδεμένα",
"{{count}} people connected|one": "{{count}} άτομο συνδεδεμένο"
}

View File

@@ -1,28 +1,32 @@
{
"{{count}} people connected|one": "{{count}} person connected",
"{{count}} people connected|other": "{{count}} people connected",
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
"{{count}} stars|one": "{{count}} star",
"{{count}} stars|other": "{{count}} stars",
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
"{{name}} (Connecting...)": "{{name}} (Connecting...)",
"{{name}} (Waiting for video...)": "{{name}} (Waiting for video...)",
"{{name}} is presenting": "{{name}} is presenting",
"{{name}} is talking…": "{{name}} is talking…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie 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></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>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>",
"<0>Oops, something's gone wrong.</0>": "<0>Oops, something's gone wrong.</0>",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Submitting debug logs will help us track down the problem.</0>",
"<0>Thanks for your feedback!</0>": "<0>Thanks for your feedback!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>We'd love to hear your feedback so we can improve your experience.</0>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
"Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.",
"Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.",
"Advanced": "Advanced",
"Allow analytics": "Allow analytics",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.",
"Audio": "Audio",
"Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "By clicking \"Go\", you agree to our <2>Terms and conditions</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.",
"Call link copied": "Call link copied",
"Call type menu": "Call type menu",
"Camera": "Camera",
@@ -32,26 +36,30 @@
"Close": "Close",
"Confirm password": "Confirm password",
"Connection lost": "Connection lost",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Copied!": "Copied!",
"Copy": "Copy",
"Copy and share this call link": "Copy and share this call link",
"Create account": "Create account",
"Debug log": "Debug log",
"Debug log request": "Debug log request",
"Description (optional)": "Description (optional)",
"Details": "Details",
"Developer": "Developer",
"Developer Settings": "Developer Settings",
"Display name": "Display name",
"Download debug logs": "Download debug logs",
"Element Call Home": "Element Call Home",
"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",
"Having trouble? Help us fix it.": "Having trouble? Help us fix it.",
"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!",
@@ -62,7 +70,6 @@
"Join call now": "Join call now",
"Join existing call?": "Join existing call?",
"Leave": "Leave",
"Loading room…": "Loading room…",
"Loading…": "Loading…",
"Local volume": "Local volume",
"Logging in…": "Logging in…",
@@ -72,7 +79,6 @@
"Microphone {{n}}": "Microphone {{n}}",
"Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.",
"More": "More",
"More menu": "More menu",
"Mute microphone": "Mute microphone",
"No": "No",
"Not now, return to home screen": "Not now, return to home screen",
@@ -93,8 +99,6 @@
"Release to stop": "Release to stop",
"Remove": "Remove",
"Return to home screen": "Return to home screen",
"Save": "Save",
"Saving…": "Saving…",
"Select an option": "Select an option",
"Send debug logs": "Send debug logs",
"Sending debug logs…": "Sending debug logs…",
@@ -104,27 +108,27 @@
"Show call inspector": "Show call inspector",
"Sign in": "Sign in",
"Sign out": "Sign out",
"Single-key keyboard shortcuts": "Single-key keyboard shortcuts",
"Spatial audio": "Spatial audio",
"Speaker": "Speaker",
"Speaker {{n}}": "Speaker {{n}}",
"Spotlight": "Spotlight",
"Stop sharing screen": "Stop sharing screen",
"Submit": "Submit",
"Submit feedback": "Submit feedback",
"Submitting feedback…": "Submitting feedback…",
"Submitting…": "Submitting…",
"Take me Home": "Take me Home",
"Talk over speaker": "Talk over speaker",
"Talking…": "Talking…",
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.",
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
"This feature is only supported on Firefox.": "This feature is only supported on Firefox.",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.",
"Turn off camera": "Turn off camera",
"Turn on camera": "Turn on camera",
"Unmute microphone": "Unmute microphone",
"User ID": "User ID",
"Use the upcoming grid system": "Use the upcoming grid system",
"User menu": "User menu",
"Username": "Username",
"Version: {{version}}": "Version: {{version}}",
@@ -136,8 +140,8 @@
"Walkie-talkie call": "Walkie-talkie call",
"Walkie-talkie call name": "Walkie-talkie call name",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.",
"Yes, join call": "Yes, join call",
"You can't talk at the same time": "You can't talk at the same time",
"Your feedback": "Your feedback",
"Your recent calls": "Your recent calls"
}

View File

@@ -23,7 +23,6 @@
"Version: {{version}}": "Versión: {{version}}",
"Username": "Nombre de usuario",
"User menu": "Menú de usuario",
"User ID": "ID de usuario",
"Unmute microphone": "Desilenciar el micrófono",
"Turn on camera": "Encender la cámara",
"Turn off camera": "Apagar la cámara",
@@ -33,7 +32,6 @@
"Talking…": "Hablando…",
"Talk over speaker": "Hablar por encima",
"Take me Home": "Volver al inicio",
"Submitting feedback…": "Enviando comentarios…",
"Submit feedback": "Enviar comentarios",
"Stop sharing screen": "Dejar de compartir pantalla",
"Spotlight": "Foco",
@@ -49,8 +47,6 @@
"Sending debug logs…": "Enviando registros de depuración…",
"Send debug logs": "Enviar registros de depuración",
"Select an option": "Selecciona una opción",
"Saving…": "Guardando…",
"Save": "Guardar",
"Return to home screen": "Volver a la pantalla de inicio",
"Remove": "Eliminar",
"Release to stop": "Suelta para parar",
@@ -68,7 +64,6 @@
"Not now, return to home screen": "Ahora no, volver a la pantalla de inicio",
"No": "No",
"Mute microphone": "Silenciar micrófono",
"More menu": "Menú Más",
"More": "Más",
"Microphone permissions needed to join the call.": "Se necesitan permisos del micrófono para unirse a la llamada.",
"Microphone {{n}}": "Micrófono {{n}}",
@@ -77,7 +72,6 @@
"Logging in…": "Iniciando sesión…",
"Local volume": "Volumen local",
"Loading…": "Cargando…",
"Loading room…": "Cargando sala…",
"Leave": "Abandonar",
"Join existing call?": "¿Unirse a llamada existente?",
"Join call now": "Unirse a la llamada ahora",
@@ -89,7 +83,6 @@
"Incompatible versions": "Versiones incompatibles",
"Include debug logs": "Incluir registros de depuración",
"Home": "Inicio",
"Having trouble? Help us fix it.": "¿Tienes problemas? Ayúdanos a resolverlos.",
"Grid layout menu": "Menú de distribución de cuadrícula",
"Go": "Comenzar",
"Full screen": "Pantalla completa",
@@ -100,7 +93,6 @@
"Display name": "Nombre a mostrar",
"Developer": "Desarrollador",
"Details": "Detalles",
"Description (optional)": "Descripción (opcional)",
"Debug log request": "Petición de registros de depuración",
"Debug log": "Registro de depuración",
"Create account": "Crear cuenta",
@@ -126,15 +118,17 @@
"{{name}} is talking…": "{{name}} está hablando…",
"{{name}} is presenting": "{{name}} está presentando",
"{{name}} (Connecting...)": "{{name}} (Conectando...)",
"{{displayName}}, your call is now ended": "{{displayName}}, tu llamada ha finalizado",
"{{count}} people connected|other": "{{count}} personas conectadas",
"{{count}} people connected|one": "{{count}} persona conectada",
"Allow analytics": "Permitir analíticas",
"Advanced": "Avanzado",
"Element Call Home": "Inicio de Element Call",
"Copy": "Copiar",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Esto enviará datos anónimos (como la duración de la llamada y el número de participantes) al equipo de Element Call para ayudarnos a optimizar la aplicación dependiendo de cómo se use.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Habilita los atajos de teclado de una sola tecla, por ejemplo 'm' para silenciar/desilenciar el micrófono.",
"Single-key keyboard shortcuts": "Atajos de teclado de una sola tecla",
"{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)"
"{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)",
"This feature is only supported on Firefox.": "Esta característica solo está disponible en Firefox.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Subir los registros de depuración nos ayudará a encontrar el problema.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Ups, algo ha salido mal.</0>",
"Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
"Developer Settings": "Ajustes de desarrollador",
"Use the upcoming grid system": "Utilizar el próximo sistema de cuadrícula",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Al participar en esta beta, consientes a la recogida de datos anónimos, los cuales usaremos para mejorar el producto. Puedes encontrar más información sobre que datos recogemos en nuestra <2>Política de privacidad</2> y en nuestra <5>Política sobre Cookies</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta."
}

View File

@@ -10,7 +10,6 @@
"{{name}} is talking…": "{{nimi}} räägib…",
"{{name}} is presenting": "{{nimi}} esitab",
"{{name}} (Connecting...)": "{{nimi}} (ühendamisel...)",
"{{displayName}}, your call is now ended": "{{displayName}}, sinu kõne on nüüd lõppenud",
"{{count}} people connected|other": "{{count}} osalejat liitunud",
"{{count}} people connected|one": "{{count}} osaleja liitunud",
"Invite people": "Kutsu inimesi",
@@ -20,7 +19,6 @@
"Incompatible versions": "Ühildumatud versioonid",
"Include debug logs": "Lisa veatuvastuslogid",
"Home": "Avavaatesse",
"Having trouble? Help us fix it.": "Kas on probleeme? Aita meil asja parandada.",
"Grid layout menu": "Ruudustikvaate menüü",
"Go": "Jätka",
"Full screen": "Täisekraan",
@@ -31,7 +29,6 @@
"Display name": "Kuvatav nimi",
"Developer": "Arendaja",
"Details": "Täpsemalt",
"Description (optional)": "Kirjeldus (valikuline)",
"Debug log request": "Veaotsingulogi päring",
"Debug log": "Veaotsingulogi",
"Create account": "Loo konto",
@@ -60,7 +57,6 @@
"Mute microphone": "Summuta mikrofon",
"Your recent calls": "Hiljutised kõned",
"You can't talk at the same time": "Üheaegselt ei saa rääkida",
"More menu": "Rohkem valikuid",
"More": "Rohkem",
"Microphone permissions needed to join the call.": "Kõnega liitumiseks on vaja lubada mikrofoni kasutamine.",
"Microphone {{n}}": "Mikrofon {{n}}",
@@ -70,15 +66,12 @@
"Logging in…": "Sisselogimine …",
"Local volume": "Kohalik helitugevus",
"Loading…": "Laadimine …",
"Loading room…": "Ruumi laadimine …",
"Leave": "Lahku",
"Join existing call?": "Liitu juba käimasoleva kõnega?",
"Join call now": "Kõnega liitumine",
"Join call": "Kõnega liitumine",
"User ID": "Kasutajatunnus",
"Turn on camera": "Lülita kaamera sisse",
"Turn off camera": "Lülita kaamera välja",
"Submitting feedback…": "Tagasiside saatmine…",
"Take me Home": "Mine avalehele",
"Submit feedback": "Jaga tagasisidet",
"Stop sharing screen": "Lõpeta ekraani jagamine",
@@ -95,8 +88,6 @@
"Sending debug logs…": "Veaotsingulogide saatmine…",
"Send debug logs": "Saada veaotsingulogid",
"Select an option": "Vali oma eelistus",
"Saving…": "Salvestamine…",
"Save": "Salvesta",
"Return to home screen": "Tagasi avalehele",
"Remove": "Eemalda",
"Release to stop": "Peatamiseks vabasta klahv",
@@ -129,13 +120,27 @@
"WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Muudab kõneleja heli nii, nagu tuleks see sealt, kus on tema pilt ekraanil. (See on katseline funktsionaalsus ja võib mõjutada heli stabiilsust.)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika</2> ja <6>teenusetingimused</6>.<9></9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega</12>",
"Allow analytics": "Luba analüütika",
"Advanced": "Lisaseadistused",
"Element Call Home": "Element Call Home",
"Copy": "Kopeeri",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Me saadame kõne anonüümsed andmed (nagu kõne kestus ja osalejate arv) meie arendustiimile ja see võimaldab levinud kasutusmustrite alusel arendust optimeerida.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Kas kasutame üheklahvilisi kiirklahve, näiteks „m“ mikrofoni sisse/välja lülitamiseks.",
"Single-key keyboard shortcuts": "Üheklahvilised kiirklahvid",
"{{name}} (Waiting for video...)": "{{name}} (Ootame videovoo algust...)",
"This feature is only supported on Firefox.": "See funktsionaalsus on toetatud vaid Firefoxis."
"This feature is only supported on Firefox.": "See funktsionaalsus on toetatud vaid Firefoxis.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Ohoo, midagi on nüüd katki.</0>",
"Use the upcoming grid system": "Kasuta tulevast ruudustiku-põhist paigutust",
"Expose developer settings in the settings window.": "Näita seadistuste aknas arendajale vajalikke seadeid.",
"Developer Settings": "Arendaja seadistused",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Nõustudes selle beetaversiooni kasutamisega sa nõustud ka toote arendamiseks kasutatavate anonüümsete andmete kogumisega. Täpsemat teavet kogutavate andmete kohta leiad meie <2>Privaatsuspoliitikast</2> ja meie <5>Küpsiste kasutamise reeglitest</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.",
"Your feedback": "Sinu tagasiside",
"Thanks, we received your feedback!": "Tänud, me oleme sinu tagasiside kätte saanud!",
"Submitting…": "Saadan…",
"Submit": "Saada",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.",
"Feedback": "Tagasiside",
"{{count}} stars|one": "{{count}} tärn",
"{{count}} stars|other": "{{count}} tärni",
"How did it go?": "Kuidas sujus?",
"{{displayName}}, your call has ended.": "{{displayName}}, sinu kõne on lõppenud.",
"<0>Thanks for your feedback!</0>": "<0>Täname Sind tagasiside eest!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Meie rakenduse paremaks muutmiseks me hea meelega ootame Sinu arvamusi.</0>"
}

View File

@@ -3,7 +3,6 @@
"Video call": "تماس تصویری",
"Video": "ویدیو",
"Username": "نام کاربری",
"User ID": "آی دی کاربر",
"Turn on camera": "روشن کردن دوربین",
"Turn off camera": "خاموش کردن دوربین",
"Take me Home": "مرا به خانه ببر",
@@ -11,7 +10,6 @@
"Sign out": "خروج",
"Sign in": "ورود",
"Settings": "تنظیمات",
"Save": "ذخیره",
"Profile": "پروفایل",
"Password": "رمز عبور",
"No": "خیر",
@@ -21,7 +19,6 @@
"Login to your account": "به حساب کاربری خود وارد شوید",
"Login": "ورود",
"Loading…": "بارگزاری…",
"Loading room…": "بارگزاری اتاق…",
"Leave": "خروج",
"Join existing call?": "پیوست به تماس؟",
"Join call now": "الان به تماس بپیوند",
@@ -37,7 +34,6 @@
"Display name": "نام نمایشی",
"Developer": "توسعه دهنده",
"Details": "جزئیات",
"Description (optional)": "توضیحات (اختیاری)",
"Debug log request": "درخواست لاگ عیب‌یابی",
"Debug log": "لاگ عیب‌یابی",
"Create account": "ساخت حساب کاربری",
@@ -66,7 +62,6 @@
"{{roomName}} - Walkie-talkie call": "{{roomName}} - تماس واکی-تاکی",
"{{name}} is talking…": "{{name}} در حال صحبت است…",
"{{name}} is presenting": "{{name}} حاضر است",
"{{displayName}}, your call is now ended": "{{displayName}} تماس شما پایان یافت",
"{{count}} people connected|other": "{{count}} نفر متصل هستند",
"{{count}} people connected|one": "{{count}} فرد متصل هستند",
"Local volume": "حجم داخلی",
@@ -81,7 +76,6 @@
"Sending debug logs…": "در حال ارسال باگ‌های عیب‌یابی…",
"Send debug logs": "ارسال لاگ‌های عیب‌یابی",
"Select an option": "یک گزینه را انتخاب کنید",
"Saving…": "در حال ذخیره…",
"Return to home screen": "برگشت به صفحه اصلی",
"Remove": "حذف",
"Release to stop": "برای توقف رها کنید",
@@ -98,12 +92,10 @@
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "کاربران دیگر تلاش می‌کنند با ورژن‌های ناسازگار به مکالمه بپیوندند. این کاربران باید از بروزرسانی مرورگرشان اطمینان داشته باشند:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "هنوز ثبت‌نام نکرده‌اید؟ <2>ساخت حساب کاربری</2>",
"Not now, return to home screen": "الان نه، به صفحه اصلی برگردید",
"More menu": "تنظیمات بیشتر",
"Microphone permissions needed to join the call.": "برای پیوستن به مکالمه دسترسی به میکروفون نیاز است.",
"Microphone {{n}}": "میکروفون {{n}}",
"Logging in…": "ورود…",
"Include debug logs": "شامل لاگ‌های عیب‌یابی",
"Having trouble? Help us fix it.": "با مشکلی رو به رو شدید؟ به ما کمک کنید رفعش کنیم.",
"Grid layout menu": "منوی طرح‌بندی شبکه‌ای",
"Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.",
"You can't talk at the same time": "نمی توانید هم‌زمان صحبت کنید",
@@ -123,17 +115,11 @@
"Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.",
"Talking…": "در حال صحبت کردن…",
"Talk over speaker": "روی بلندگو صحبت کنید",
"Submitting feedback…": "در حال ارسال بازخورد…",
"Submit feedback": "بازخورد ارائه دهید",
"Stop sharing screen": "توقف اشتراک‌گذاری صفحه نمایش",
"Spatial audio": "صدای جهت‌دار",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "این که میان‌برهای صفحه‌کلید تک‌کلیده مثل m برای خموشی و ناخموشی میکروفون به کار بیفتند یا نه.",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "داده‌های ناشناس شده (از اطَلاعاتی مثل طول تماس و شمارهٔ طرف‌ها) را به گروه تماس المنت فرستاده تا در بهینه‌سازی برنامه بر پایهٔ چگونگی استفاده‌اش یاریمان کنند.",
"Single-key keyboard shortcuts": "میان‌برهای صفحه‌کلید تک‌کلیده",
"Element Call Home": "خانهٔ تماس المنت",
"Copy": "رونوشت",
"Allow analytics": "نمایش تجزیه‌ها",
"Advanced": "پیش رفته",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>اکنون به تماس پیوسته</0><1>یا</1><2>پیوند تماس را رونوشت کرده و بعداً بپیوندید</2>",
"{{name}} (Waiting for video...)": "{{name}} (منتظر تصویر…)",
"{{name}} (Connecting...)": "{{name}} (وصل شدن…)"

View File

@@ -22,7 +22,6 @@
"Create account": "Créer un compte",
"Debug log": "Journal de débogage",
"Debug log request": "Demande dun journal de débogage",
"Description (optional)": "Description (facultatif)",
"Details": "Informations",
"Developer": "Développeur",
"Display name": "Nom daffichage",
@@ -32,7 +31,6 @@
"Full screen": "Plein écran",
"Go": "Commencer",
"Grid layout menu": "Menu en grille",
"Having trouble? Help us fix it.": "Un problème ? Aidez nous à le résoudre.",
"Home": "Accueil",
"Include debug logs": "Inclure les journaux de débogage",
"Incompatible versions": "Versions incompatibles",
@@ -43,7 +41,6 @@
"Join call now": "Rejoindre lappel maintenant",
"Join existing call?": "Rejoindre un appel existant ?",
"Leave": "Partir",
"Loading room…": "Chargement du salon…",
"Loading…": "Chargement…",
"Local volume": "Volume local",
"Logging in…": "Connexion…",
@@ -53,7 +50,6 @@
"Microphone permissions needed to join the call.": "Accès au microphone requis pour rejoindre lappel.",
"Microphone {{n}}": "Microphone {{n}}",
"More": "Plus",
"More menu": "Menu plus",
"Mute microphone": "Couper le micro",
"No": "Non",
"Not now, return to home screen": "Pas maintenant, retourner à laccueil",
@@ -74,8 +70,6 @@
"Release to stop": "Relâcher pour arrêter",
"Remove": "Supprimer",
"Return to home screen": "Retour à laccueil",
"Save": "Enregistrer",
"Saving…": "Enregistrement…",
"Select an option": "Sélectionnez une option",
"Send debug logs": "Envoyer les journaux de débogage",
"Sending…": "Envoi…",
@@ -88,7 +82,6 @@
"Spotlight": "Premier plan",
"Stop sharing screen": "Arrêter le partage décran",
"Submit feedback": "Envoyer des retours",
"Submitting feedback…": "Envoi des retours…",
"Take me Home": "Retouner à laccueil",
"Talk over speaker": "Parler par dessus lintervenant",
"Thanks! We'll get right on it.": "Merci ! Nous allons nous y attaquer.",
@@ -98,7 +91,6 @@
"{{roomName}} - Walkie-talkie call": "{{roomName}} — Appel talkie-walkie",
"{{name}} is talking…": "{{name}} est en train de parler…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{displayName}}, your call is now ended": "{{displayName}}, votre appel est désormais terminé",
"{{count}} people connected|other": "{{count}} personnes connectées",
"{{count}} people connected|one": "{{count}} personne connectée",
"Your recent calls": "Appels récents",
@@ -115,7 +107,6 @@
"Version: {{version}}": "Version : {{version}}",
"Username": "Nom dutilisateur",
"User menu": "Menu utilisateur",
"User ID": "Identifiant utilisateur",
"Unmute microphone": "Allumer le micro",
"Turn on camera": "Allumer la caméra",
"Turn off camera": "Couper la caméra",
@@ -129,13 +120,27 @@
"Sending debug logs…": "Envoi des journaux de débogage…",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Rejoindre lappel maintenant</0><1>Ou</1><2>Copier le lien de lappel et rejoindre plus tard</2>",
"{{name}} (Connecting...)": "{{name}} (Connexion…)",
"Allow analytics": "Autoriser les statistiques",
"Advanced": "Avancé",
"Element Call Home": "Accueil Element Call",
"Copy": "Copier",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Cela enverra des données anonymisées (telles que la durée dun appel et le nombre de participants) à léquipe de Element Call pour aider à optimiser lapplication en fonction de lutilisation.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Bascule sur les raccourcis clavier à touche unique, par exemple « m » pour désactiver / activer le micro.",
"Single-key keyboard shortcuts": "Raccourcis clavier en une touche",
"{{name}} (Waiting for video...)": "{{name}} (En attente de vidéo…)",
"This feature is only supported on Firefox.": "Cette fonctionnalité est prise en charge dans Firefox uniquement."
"This feature is only supported on Firefox.": "Cette fonctionnalité est prise en charge dans Firefox uniquement.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Oups, quelque chose sest mal passé.</0>",
"Use the upcoming grid system": "Utiliser le futur système de grille",
"Expose developer settings in the settings window.": "Affiche les paramètres développeurs dans la fenêtre des paramètres.",
"Developer Settings": "Paramètres développeurs",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "En participant à cette beta, vous consentez à la collecte de données anonymes, qui seront utilisées pour améliorer le produit. Vous trouverez plus dinformations sur les données collectées dans notre <2>Politique de vie privée</2> et notre <5>Politique de cookies</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de lappel.",
"Your feedback": "Votre commentaire",
"Thanks, we received your feedback!": "Merci, nous avons reçu vos commentaires !",
"Submitting…": "Envoi…",
"Submit": "Envoyer",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, veuillez nous envoyer une courte description ci-dessous.",
"Feedback": "Commentaires",
"{{count}} stars|other": "{{count}} favoris",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Nous aimerions avoir vos commentaires afin que nous puissions améliorer votre expérience.</0>",
"{{count}} stars|one": "{{count}} favori",
"{{displayName}}, your call has ended.": "{{displayName}}, votre appel est terminé.",
"<0>Thanks for your feedback!</0>": "<0>Merci pour votre commentaire !</0>",
"How did it go?": "Comment cela sest-il passé ?"
}

View File

@@ -23,7 +23,6 @@
"Create account": "Buat akun",
"Debug log": "Catatan pengawakutuan",
"Debug log request": "Permintaan catatan pengawakutuan",
"Description (optional)": "Deskripsi (opsional)",
"Details": "Detail",
"Developer": "Pengembang",
"Display name": "Nama tampilan",
@@ -34,7 +33,6 @@
"Full screen": "Layar penuh",
"Go": "Bergabung",
"Grid layout menu": "Menu tata letak kisi",
"Having trouble? Help us fix it.": "Mengalami masalah? Bantu kami memperbaikinya.",
"Home": "Beranda",
"Include debug logs": "Termasuk catatan pengawakutuan",
"Incompatible versions": "Versi tidak kompatibel",
@@ -46,7 +44,6 @@
"Join call now": "Bergabung ke panggilan sekarang",
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
"Leave": "Keluar",
"Loading room…": "Memuat ruangan…",
"Loading…": "Memuat…",
"Local volume": "Volume lokal",
"Logging in…": "Memasuki…",
@@ -56,7 +53,6 @@
"Microphone permissions needed to join the call.": "Izin mikrofon dibutuhkan untuk bergabung ke panggilan ini.",
"Microphone {{n}}": "Mikrofon {{n}}",
"More": "Lainnya",
"More menu": "Menu lainnya",
"Mute microphone": "Bisukan mikrofon",
"No": "Tidak",
"Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda",
@@ -77,8 +73,6 @@
"Release to stop": "Lepaskan untuk berhenti",
"Remove": "Hapus",
"Return to home screen": "Kembali ke layar beranda",
"Save": "Simpan",
"Saving…": "Menyimpan…",
"Select an option": "Pilih sebuah opsi",
"Send debug logs": "Kirim catatan pengawakutuan",
"Sending…": "Mengirimkan…",
@@ -93,7 +87,6 @@
"Spotlight": "Sorotan",
"Stop sharing screen": "Berhenti membagikan layar",
"Submit feedback": "Kirim masukan",
"Submitting feedback…": "Mengirimkan masukan…",
"Take me Home": "Bawa saya ke Beranda",
"Talk over speaker": "Bicara pada pembicara",
"Talking…": "Berbicara…",
@@ -104,7 +97,6 @@
"Turn off camera": "Matikan kamera",
"Turn on camera": "Nyalakan kamera",
"Unmute microphone": "Suarakan mikrofon",
"User ID": "ID pengguna",
"User menu": "Menu pengguna",
"Username": "Nama pengguna",
"Version: {{version}}": "Versi: {{version}}",
@@ -121,7 +113,6 @@
"Your recent calls": "Panggilan Anda terkini",
"{{count}} people connected|one": "{{count}} orang terhubung",
"{{count}} people connected|other": "{{count}} orang terhubung",
"{{displayName}}, your call is now ended": "{{displayName}}, panggilan Anda sekarang telah berakhir",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} sedang mempresentasi",
"{{name}} is talking…": "{{name}} sedang berbicara…",
@@ -129,13 +120,26 @@
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
"{{name}} (Connecting...)": "{{name}} (Menghubungkan...)",
"Allow analytics": "Perbolehkan analitik",
"Advanced": "Tingkat lanjut",
"Element Call Home": "Beranda Element Call",
"Copy": "Salin",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Ini akan mengirimkan data anonim (seperti durasi dan jumlah peserta panggilan) ke tim Element Call untuk membantu kami mengoptimalkan aplikasi berdasarkan bagaimana penggunaannya.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Apakah pintasan papan ketik seharusnya diaktifkan, mis. 'm' untuk membisukan/menyuarakan mikrofon.",
"Single-key keyboard shortcuts": "Pintasan papan ketik satu tombol",
"{{name}} (Waiting for video...)": "{{name}} (Menunggu video...)",
"This feature is only supported on Firefox.": "Fitur ini hanya didukung di Firefox."
"This feature is only supported on Firefox.": "Fitur ini hanya didukung di Firefox.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Aduh, ada yang salah.</0>",
"Use the upcoming grid system": "Gunakan sistem kisi yang akan segera datang",
"Expose developer settings in the settings window.": "Ekspos pengaturan pengembang dalam jendela pengaturan.",
"Developer Settings": "Pengaturan Pengembang",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Dengan bergabung dalam beta ini, Anda mengizinkan kami untuk mengumpulkan data anonim, yang kami gunakan untuk meningkatkan produk ini. Anda dapat mempelajari lebih lanjut tentang data apa yang kami lacak dalam <2>Kebijakan Privasi</2> dan <5>Kebijakan Kuki</5> kami.",
"<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>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.",
"Feedback": "Masukan",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
"Submit": "Kirim",
"Submitting…": "Mengirim",
"Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!",
"Your feedback": "Masukan Anda",
"{{displayName}}, your call has ended.": "{{displayName}}, panggilan Anda telah berakhir.",
"<0>Thanks for your feedback!</0>": "<0>Terima kasih atas masukan Anda!</0>",
"How did it go?": "Bagaimana rasanya?",
"{{count}} stars|one": "{{count}} bintang",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Kami ingin mendengar masukan Anda supaya kami bisa meningkatkan pengalaman Anda.</0>"
}

View File

@@ -1 +1,106 @@
{}
{
"{{name}} (Connecting...)": "{{name}}(接続しています…)",
"{{count}} people connected|other": "{{count}}人が接続済",
"{{count}} people connected|one": "{{count}}人が接続済",
"{{name}} (Waiting for video...)": "{{name}}(ビデオを待機しています…)",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>既にアカウントをお持ちですか?</0><1><0>ログイン</0>または<2>ゲストとしてアクセス</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - トランシーバー通話",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>アカウントを作成</0>または<2>ゲストとしてアクセス</2>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>今すぐ通話に参加</0><1>または</1><2>通話リンクをコピーし、後で参加</2>",
"Accept camera/microphone permissions to join the call.": "通話に参加するには、カメラ・マイクの許可が必要です。",
"<0>Oops, something's gone wrong.</0>": "<0>何かがうまく行きませんでした。</0>",
"Camera/microphone permissions needed to join the call.": "通話に参加する場合、カメラ・マイクの許可が必要です。",
"Camera": "カメラ",
"Call link copied": "通話リンクをコピーしました",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "「今すぐ通話に参加」をクリックすると、<2>利用規約</2>に同意したとみなされます",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "「続行」をクリックすると、 <2>利用規約</2>に同意したとみなされます",
"Avatar": "アバター",
"Accept microphone permissions to join the call.": "通話に参加するには、マイクの許可が必要です。",
"Audio": "音声",
"Connection lost": "接続が切断されました",
"Confirm password": "パスワードを確認",
"Close": "閉じる",
"Change layout": "レイアウトを変更",
"Copied!": "コピーしました!",
"Copy and share this call link": "通話リンクをコピーし共有",
"Copy": "コピー",
"Debug log": "デバッグログ",
"Create account": "アカウントを作成",
"Go": "続行",
"Fetching group call timed out.": "グループ通話の取得がタイムアウトしました。",
"Element Call Home": "Element Call ホーム",
"Download debug logs": "デバッグログをダウンロード",
"Display name": "表示名",
"Developer": "開発者",
"Details": "詳細",
"Full screen": "全画面表示",
"Exit full screen": "全画面表示を終了",
"Include debug logs": "デバッグログを含める",
"Home": "ホーム",
"Incompatible versions!": "互換性のないバージョンです!",
"Incompatible versions": "互換性のないバージョン",
"Join existing call?": "既存の通話に参加しますか?",
"Join call now": "今すぐ通話に参加",
"Join call": "通話に参加",
"Invite": "招待",
"Invite people": "連絡先を招待",
"Not registered yet? <2>Create an account</2>": "アカウントがありませんか? <2>アカウントを作成</2>",
"Mute microphone": "マイクをミュート",
"Microphone permissions needed to join the call.": "通話の参加にはマイクの許可が必要です。",
"Microphone": "マイク",
"Login": "ログイン",
"Logging in…": "ログインしています…",
"Loading…": "読み込んでいます…",
"Leave": "退出",
"Version: {{version}}": "バージョン:{{version}}",
"Username": "ユーザー名",
"User menu": "ユーザーメニュー",
"Unmute microphone": "マイクのミュートを解除",
"Turn on camera": "カメラをつける",
"Turn off camera": "カメラを切る",
"Submit feedback": "フィードバックを送信",
"Stop sharing screen": "画面共有を停止",
"Spotlight": "スポットライト",
"Send debug logs": "デバッグログを送信",
"Sign out": "サインアウト",
"Sign in": "サインイン",
"Share screen": "画面共有",
"Settings": "設定",
"Sending…": "送信しています…",
"Sending debug logs…": "デバッグログを送信しています…",
"Return to home screen": "ホーム画面に戻る",
"Registering…": "登録しています…",
"Register": "登録",
"Profile": "プロフィール",
"Press and hold spacebar to talk": "スペースを長押しで会話",
"Passwords must match": "パスワードが一致する必要があります",
"Password": "パスワード",
"Speaker": "スピーカー",
"Video call name": "ビデオ通話の名称",
"Video call": "ビデオ通話",
"Video": "ビデオ",
"Waiting for other participants…": "他の参加者を待機しています…",
"Waiting for network": "ネットワークを待機しています",
"Walkie-talkie call name": "トランシーバー通話の名称",
"Walkie-talkie call": "トランシーバー通話",
"Camera {{n}}": "カメラ {{n}}",
"{{name}} is talking…": "{{name}}が話しています…",
"Yes, join call": "はい、通話に参加",
"Spatial audio": "空間オーディオ",
"Select an option": "オプションを選択",
"Debug log request": "デバッグログを要求",
"Your recent calls": "最近の通話",
"You can't talk at the same time": "同時に会話することはできません",
"WebRTC is not supported or is being blocked in this browser.": "お使いのブラウザでWebRTCがサポートされていないか、またはブロックされています。",
"Login to your account": "アカウントにログイン",
"Freedom": "自由",
"Talking…": "話しています…",
"Remove": "削除",
"No": "いいえ",
"This feature is only supported on Firefox.": "この機能はFirefoxでのみサポートされています。",
"This call already exists, would you like to join?": "この通話は既に存在します。参加しますか?",
"Take me Home": "ホームに戻る",
"Press and hold to talk": "押し続けて会話",
"{{name}} is presenting": "{{name}}が画面を共有しています",
"{{names}}, {{name}}": "{{names}}、{{name}}"
}

View File

@@ -3,7 +3,6 @@
"<0>Create an account</0> Or <2>Access as a guest</2>": "",
"{{count}} people connected|one": "{{count}}명 연결됨",
"{{count}} people connected|other": "{{count}}명 연결됨",
"{{displayName}}, your call is now ended": "{{displayName}}님, 전화가 종료되었습니다",
"{{names}}, {{name}}": "{{names}}님, {{name}}님",
"{{name}} is presenting": "{{name}}님이 발표 중",
"{{name}} is talking…": "{{name}}님이 말하는 중…",

View File

@@ -1,9 +1,8 @@
{
"More menu": "Menu \"więcej\"",
"Login": "Zaloguj się",
"Go": "Kontynuuj",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Warunki</2>",
"{{count}} people connected|other": "{{count}} ludzi połączono",
"Go": "Przejdź",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Zasady i warunki</2>",
"{{count}} people connected|other": "{{count}} osób połączonych",
"Your recent calls": "Twoje ostatnie połączenia",
"You can't talk at the same time": "Nie możesz mówić w tym samym czasie",
"Yes, join call": "Tak, dołącz do połączenia",
@@ -18,7 +17,6 @@
"Version: {{version}}": "Wersja: {{version}}",
"Username": "Nazwa użytkownika",
"User menu": "Menu użytkownika",
"User ID": "ID użytkownika",
"Unmute microphone": "Wyłącz wyciszenie mikrofonu",
"Turn on camera": "Włącz kamerę",
"Turn off camera": "Wyłącz kamerę",
@@ -26,8 +24,7 @@
"This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
"Thanks! We'll get right on it.": "Dziękujemy! Zaraz się tym zajmiemy.",
"Talking…": "Mówienie…",
"Take me Home": "Zabierz mnie do ekranu startowego",
"Submitting feedback…": "Przesyłanie opinii…",
"Take me Home": "Zabierz mnie do strony głównej",
"Submit feedback": "Prześlij opinię",
"Stop sharing screen": "Zatrzymaj udostępnianie ekranu",
"Spotlight": "Centrum uwagi",
@@ -43,12 +40,10 @@
"Sending debug logs…": "Wysyłanie dzienników debugowania…",
"Send debug logs": "Wyślij dzienniki debugowania",
"Select an option": "Wybierz opcję",
"Saving…": "Zapisywanie…",
"Save": "Zapisz",
"Return to home screen": "Powróć do ekranu domowego",
"Return to home screen": "Powróć do strony głównej",
"Remove": "Usuń",
"Release to stop": "Puść przycisk, aby przestać",
"Release spacebar key to stop": "Puść spację, aby przestać",
"Release to stop": "Puść przycisk, aby zatrzymać",
"Release spacebar key to stop": "Puść spację, aby zatrzymać",
"Registering…": "Rejestrowanie…",
"Register": "Zarejestruj",
"Recaptcha not loaded": "Recaptcha nie została załadowana",
@@ -58,7 +53,7 @@
"Press and hold to talk": "Przytrzymaj, aby mówić",
"Press and hold spacebar to talk over {{name}}": "Przytrzymaj spację, aby mówić wraz z {{name}}",
"Press and hold spacebar to talk": "Przytrzymaj spację, aby mówić",
"Passwords must match": "Hasła muszą być identyczne",
"Passwords must match": "Hasła muszą pasować",
"Password": "Hasło",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Inni użytkownicy próbują dołączyć do tego połączenia przy użyciu niekompatybilnych wersji. Powinni oni upewnić się, że odświeżyli stronę w swoich przeglądarkach:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "Nie masz konta? <2>Utwórz je</2>",
@@ -71,9 +66,8 @@
"Microphone": "Mikrofon",
"Login to your account": "Zaloguj się do swojego konta",
"Logging in…": "Logowanie…",
"Local volume": "Lokalna głośność",
"Local volume": "Głośność lokalna",
"Loading…": "Ładowanie…",
"Loading room…": "Ładowanie pokoju…",
"Leave": "Opuść",
"Join existing call?": "Dołączyć do istniejącego połączenia?",
"Join call now": "Dołącz do połączenia teraz",
@@ -85,44 +79,68 @@
"Incompatible versions": "Niekompatybilne wersje",
"Include debug logs": "Dołącz dzienniki debugowania",
"Home": "Strona domowa",
"Having trouble? Help us fix it.": "Masz problem? Pomóż nam go naprawić.",
"Grid layout menu": "Menu układu siatki",
"Full screen": "Pełen ekran",
"Full screen": "Pełny ekran",
"Freedom": "Wolność",
"Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.",
"Exit full screen": "Zamknij pełny ekran",
"Exit full screen": "Opuść pełny ekran",
"Download debug logs": "Pobierz dzienniki debugowania",
"Display name": "Wyświetlana nazwa",
"Developer": "Deweloper",
"Display name": "Nazwa wyświetlana",
"Developer": "Programista",
"Details": "Szczegóły",
"Description (optional)": "Opis (opcjonalny)",
"Debug log request": "Prośba o dzienniki debugowania",
"Debug log": "Dzienniki debugowania",
"Create account": "Utwórz konto",
"Copy and share this call link": "Skopiuj i podziel się linkiem do połączenia",
"Copy and share this call link": "Skopiuj i udostępnij link do rozmowy",
"Copied!": "Skopiowano!",
"Connection lost": "Połączenie utracone",
"Confirm password": "Potwierdź hasło",
"Close": "Zamknij",
"Change layout": "Zmień układ",
"Camera/microphone permissions needed to join the call.": "Aby dołączyć do tego połączenia, potrzebne są uprawnienia do kamery/mikrofonu.",
"Camera/microphone permissions needed to join the call.": "Wymagane są uprawnienia do kamery/mikrofonu, aby dołączyć do rozmowy.",
"Camera {{n}}": "Kamera {{n}}",
"Camera": "Kamera",
"Call type menu": "Menu rodzaju połączenia",
"Call type menu": "Menu typu połączenia",
"Call link copied": "Skopiowano link do połączenia",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Warunki</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Zasady i warunki</2>",
"Avatar": "Awatar",
"Audio": "Dźwięk",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Inny użytkownik w tym połączeniu napotkał problem. Aby lepiej zdiagnozować tę usterkę, chcielibyśmy zebrać dzienniki debugowania.",
"Accept microphone permissions to join the call.": "Przyznaj uprawnienia do mikrofonu aby dołączyć do połączenia.",
"Accept camera/microphone permissions to join the call.": "Przyznaj uprawnienia do kamery/mikrofonu aby dołączyć do połączenia.",
"Accept microphone permissions to join the call.": "Akceptuj uprawnienia mikrofonu, aby dołączyć do połączenia.",
"Accept camera/microphone permissions to join the call.": "Akceptuj uprawnienia kamery/mikrofonu, aby dołączyć do połączenia.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> Albo <2>Dołącz jako gość</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> Albo <2>Dołącz jako gość</2></1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> lub <2>Dołącz jako gość</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> lub <2>Dołącz jako gość</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - połączenie walkie-talkie",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{name}} mówi…",
"{{name}} is presenting": "{{name}} prezentuje",
"{{displayName}}, your call is now ended": "{{displayName}}, twoje połączenie zostało zakończone",
"{{count}} people connected|one": "{{count}} osoba połączona"
"{{count}} people connected|one": "{{count}} osoba połączona",
"This feature is only supported on Firefox.": "Ta funkcjonalność jest dostępna tylko w Firefox.",
"Copy": "Kopiuj",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Ojej, coś poszło nie tak.</0>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Dołącz do rozmowy już teraz</0><1>lub</1><2>Skopiuj link do rozmowy i dołącz później</2>",
"{{name}} (Waiting for video...)": "{{name}} (Oczekiwanie na wideo...)",
"{{name}} (Connecting...)": "{{name}} (Łączenie...)",
"Expose developer settings in the settings window.": "Wyświetl opcje programisty w oknie ustawień.",
"Element Call Home": "Strona główna Element Call",
"Developer Settings": "Opcje programisty",
"Talk over speaker": "Rozmowa przez głośnik",
"Use the upcoming grid system": "Użyj nadchodzącego systemu siatek",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Ta strona jest chroniona przez ReCAPTCHA, więc obowiązują na niej <2>Polityka prywatności</2> i <6>Warunki świadczenia usług</6> Google.<9></9>Klikając \"Zarejestruj się\", zgadzasz się na nasze <12>Warunki świadczenia usług</12>",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Uczestnicząc w tej becie, upoważniasz nas do zbierania anonimowych danych, które wykorzystamy do ulepszenia produktu. Dowiedz się więcej na temat danych, które zbieramy w naszej <2>Polityce prywatności</2> i <5>Polityce ciasteczek</5>.",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.",
"Thanks, we received your feedback!": "Dziękujemy, otrzymaliśmy Twoją opinię!",
"Feedback": "Opinia użytkownika",
"Submitting…": "Wysyłanie…",
"Submit": "Wyślij",
"Your feedback": "Twoje opinie",
"{{count}} stars|other": "{{count}} gwiazdki",
"{{count}} stars|one": "{{count}} gwiazdka",
"{{displayName}}, your call has ended.": "{{displayName}}, Twoje połączenie zostało zakończone.",
"<0>Thanks for your feedback!</0>": "<0>Dziękujemy za Twoją opinię!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Z przyjemnością wysłuchamy Twojej opinii, aby poprawić Twoje doświadczenia.</0>",
"How did it go?": "Jak poszło?"
}

View File

@@ -1,6 +1,5 @@
{
"Register": "Зарегистрироваться",
"Saving…": "Сохранение…",
"Registering…": "Регистрация…",
"Logging in…": "Вход…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
@@ -10,7 +9,6 @@
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
"Talking…": "Говорите…",
"Submitting feedback…": "Отправка отзыва…",
"Submit feedback": "Отправить отзыв",
"Sending debug logs…": "Отправка журнала отладки…",
"Select an option": "Выберите вариант",
@@ -38,7 +36,6 @@
"Version: {{version}}": "Версия: {{version}}",
"Username": "Имя пользователя",
"User menu": "Меню пользователя",
"User ID": "ID пользователя",
"Unmute microphone": "Включить микрофон",
"Turn on camera": "Включить камеру",
"Turn off camera": "Отключить камеру",
@@ -57,7 +54,6 @@
"Sending…": "Отправка…",
"Local volume": "Местная громкость",
"Call type menu": "Меню \"Тип звонка\"",
"More menu": "Полное меню",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Звонок-рация",
"Include debug logs": "Приложить журнал отладки",
"Download debug logs": "Скачать журнал отладки",
@@ -65,7 +61,6 @@
"Debug log": "Журнал отладки",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.",
"Send debug logs": "Отправить журнал отладки",
"Save": "Сохранить",
"Return to home screen": "Вернуться в Начало",
"Remove": "Удалить",
"Recaptcha not loaded": "Невозможно начать проверку",
@@ -86,7 +81,6 @@
"Login to your account": "Войдите в свой аккаунт",
"Login": "Вход",
"Loading…": "Загрузка…",
"Loading room…": "Загрузка комнаты…",
"Leave": "Покинуть",
"Join existing call?": "Присоединиться к существующему звонку?",
"Join call now": "Присоединиться сейчас",
@@ -97,7 +91,6 @@
"Incompatible versions!": "Несовместимые версии!",
"Incompatible versions": "Несовместимые версии",
"Home": "Начало",
"Having trouble? Help us fix it.": "Есть проблема? Помогите нам её устранить.",
"Go": "Далее",
"Full screen": "Полноэкранный режим",
"Freedom": "Свобода",
@@ -106,7 +99,6 @@
"Display name": "Видимое имя",
"Developer": "Разработчику",
"Details": "Подробности",
"Description (optional)": "Описание (необязательно)",
"Create account": "Создать аккаунт",
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
"Copied!": "Скопировано!",
@@ -124,14 +116,19 @@
"Accept camera/microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к камере/микрофону.",
"{{name}} is talking…": "{{name}} говорит…",
"{{name}} is presenting": "{{name}} показывает",
"{{displayName}}, your call is now ended": "{{displayName}}, ваш звонок завершён",
"{{count}} people connected|other": "{{count}} подключилось",
"{{count}} people connected|one": "{{count}} подключился",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Это даст разрешение на отправку анонимизированных данных (таких, как продолжительность звонка и количество участников) команде Element Call, чтобы помочь нам оптимизировать работу приложения на основании того как оно используется.",
"Element Call Home": "Главная Element Call",
"Copy": "Копировать",
"Allow analytics": "Разрешить аналитику",
"Advanced": "Расширенные",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Присоединиться сейчас</0><1>или<1><2>Скопировать ссылку на звонок и присоединиться позже</2>",
"{{name}} (Connecting...)": "{{name}} (Соединение...)"
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Присоединиться сейчас к звонку</0><1>или<1><2>Скопировать ссылку на звонок и присоединиться позже</2>",
"{{name}} (Connecting...)": "{{name}} (Соединение...)",
"This feature is only supported on Firefox.": "Эта возможность доступна только в Firefox.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Отправка журналов поможет нам найти и устранить проблему.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Упс, что-то пошло не так.</0>",
"{{name}} (Waiting for video...)": "{{name}} (Ожидание видео...)",
"Use the upcoming grid system": "Использовать сеточный показ",
"Expose developer settings in the settings window.": "Раскрыть настройки разработчика в окне настроек.",
"Developer Settings": "Настройки Разработчика",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Участвуя в этой бета-версии, вы соглашаетесь на сбор анонимных данных, которые мы используем для улучшения продукта. Более подробную информацию о том, какие данные мы отслеживаем, вы можете найти в нашей <2> Политике конфиденциальности</2> и нашей <5> Политике использования файлов cookie</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора."
}

View File

@@ -5,27 +5,21 @@
"Fetching group call timed out.": "Vypršal čas načítania skupinového volania.",
"Element Call Home": "Domov Element Call",
"You can't talk at the same time": "Nemôžete hovoriť naraz",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Či chcete povoliť jednotlačidlové klávesové skratky, napr. \"m\" na stlmenie/zapnutie mikrofónu.",
"Waiting for other participants…": "Čaká sa na ďalších účastníkov…",
"Waiting for network": "Čakanie na sieť",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Týmto spôsobom sa budú posielať anonymizované údaje (napríklad trvanie hovoru a počet účastníkov) tímu Element Call, aby nám pomohli optimalizovať aplikáciu na základe toho, ako sa používa.",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Zvuk reproduktora tak bude vyzerať, akoby vychádzal z miesta, kde je na obrazovke umiestnená jeho ikona. (Experimentálna funkcia: môže to mať vplyv na stabilitu zvuku.)",
"Thanks! We'll get right on it.": "Vďaka! Hneď sa do toho pustíme.",
"Talking…": "Rozprávanie…",
"Talk over speaker": "Hovor cez reproduktor",
"Take me Home": "Zober ma domov",
"Submitting feedback…": "Odosielanie spätnej väzby…",
"Submit feedback": "Odoslať spätnú väzbu",
"Stop sharing screen": "Zastaviť zdieľanie obrazovky",
"Single-key keyboard shortcuts": "Jednotlačidlové klávesové skratky",
"Show call inspector": "Zobraziť inšpektora hovorov",
"Share screen": "Zdieľať obrazovku",
"Sending…": "Odosielanie…",
"Sending debug logs…": "Odosielanie záznamov o ladení…",
"Send debug logs": "Odoslať záznamy o ladení",
"Select an option": "Vyberte možnosť",
"Saving…": "Ukladanie…",
"Save": "Uložiť",
"Return to home screen": "Návrat na domovskú obrazovku",
"Remove": "Odstrániť",
"Release spacebar key to stop": "Pustite medzerník pre ukončenie",
@@ -46,7 +40,6 @@
"Not now, return to home screen": "Teraz nie, vrátiť sa na domovskú obrazovku",
"No": "Nie",
"Mute microphone": "Stlmiť mikrofón",
"More menu": "Ponuka viac",
"More": "Viac",
"Microphone permissions needed to join the call.": "Povolenie mikrofónu je potrebné na pripojenie k hovoru.",
"Microphone {{n}}": "Mikrofón {{n}}",
@@ -55,7 +48,6 @@
"Login": "Prihlásiť sa",
"Logging in…": "Prihlasovanie…",
"Loading…": "Načítanie…",
"Loading room…": "Načítanie miestnosti…",
"Leave": "Opustiť",
"Join existing call?": "Pripojiť sa k existujúcemu hovoru?",
"Join call now": "Pripojiť sa k hovoru teraz",
@@ -66,7 +58,6 @@
"Incompatible versions!": "Nekompatibilné verzie!",
"Incompatible versions": "Nekompatibilné verzie",
"Home": "Domov",
"Having trouble? Help us fix it.": "Máte problém? Pomôžte nám ho opraviť.",
"Grid layout menu": "Ponuka rozloženia mriežky",
"Go": "Prejsť",
"Full screen": "Zobrazenie na celú obrazovku",
@@ -84,7 +75,6 @@
"Version: {{version}}": "Verzia: {{version}}",
"Username": "Meno používateľa",
"User menu": "Používateľské menu",
"User ID": "ID používateľa",
"Unmute microphone": "Zrušiť stlmenie mikrofónu",
"Turn on camera": "Zapnúť kameru",
"Turn off camera": "Vypnúť kameru",
@@ -99,7 +89,6 @@
"Display name": "Zobrazované meno",
"Developer": "Vývojár",
"Details": "Podrobnosti",
"Description (optional)": "Popis (voliteľné)",
"Debug log request": "Žiadosť o záznam ladenia",
"Debug log": "Záznam o ladení",
"Create account": "Vytvoriť účet",
@@ -120,8 +109,6 @@
"Avatar": "Obrázok",
"Audio": "Audio",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.",
"Allow analytics": "Povoliť analytiku",
"Advanced": "Pokročilé",
"Accept camera/microphone permissions to join the call.": "Prijmite povolenia kamery/mikrofónu, aby ste sa mohli pripojiť k hovoru.",
"Accept microphone permissions to join the call.": "Prijmite povolenia mikrofónu, aby ste sa mohli pripojiť k hovoru.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Prečo neskončiť nastavením hesla, aby ste si zachovali svoj účet? </0><1>Budete si môcť ponechať svoje meno a nastaviť obrázok, ktorý sa bude používať pri budúcich hovoroch</1>",
@@ -134,8 +121,26 @@
"{{name}} is presenting": "{{name}} prezentuje",
"{{name}} (Waiting for video...)": "{{name}} (Čaká sa na video...)",
"{{name}} (Connecting...)": "{{name}} (Pripájanie...)",
"{{displayName}}, your call is now ended": "{{displayName}}, váš hovor je teraz ukončený",
"{{count}} people connected|other": "{{count}} osôb pripojených",
"{{count}} people connected|one": "{{count}} osoba pripojená",
"This feature is only supported on Firefox.": "Táto funkcia je podporovaná len v prehliadači Firefox."
"This feature is only supported on Firefox.": "Táto funkcia je podporovaná len v prehliadači Firefox.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Hups, niečo sa pokazilo.</0>",
"Use the upcoming grid system": "Použiť pripravovaný systém mriežky",
"Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
"Developer Settings": "Nastavenia pre vývojárov",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Účasťou v tejto beta verzii súhlasíte so zhromažďovaním anonymných údajov, ktoré použijeme na zlepšenie produktu. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich <2>Zásadách ochrany osobných údajov</2> a <5>Zásadách používania súborov cookie</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.",
"Your feedback": "Vaša spätná väzba",
"Thanks, we received your feedback!": "Ďakujeme, dostali sme vašu spätnú väzbu!",
"Submitting…": "Odosielanie…",
"Submit": "Odoslať",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.",
"Feedback": "Spätná väzba",
"{{count}} stars|one": "{{count}} hviezdička",
"How did it go?": "Ako to išlo?",
"{{count}} stars|other": "{{count}} hviezdičiek",
"{{displayName}}, your call has ended.": "{{displayName}}, váš hovor skončil.",
"<0>Thanks for your feedback!</0>": "<0> Ďakujeme za vašu spätnú väzbu!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0> Radi si vypočujeme vašu spätnú väzbu, aby sme mohli zlepšiť vaše skúsenosti.</0>"
}

View File

@@ -21,7 +21,6 @@
"Create account": "Hesap aç",
"Debug log": "Hata ayıklama kütüğü",
"Debug log request": "Hata ayıklama kütük istemi",
"Description (optional)": "Tanım (isteğe bağlı)",
"Details": "Ayrıntı",
"Developer": "Geliştirici",
"Display name": "Ekran adı",
@@ -32,7 +31,6 @@
"Full screen": "Tam ekran",
"Go": "Git",
"Grid layout menu": "Izgara plan menü",
"Having trouble? Help us fix it.": "Sorun mu var? Çözmemize yardım edin.",
"Home": "Ev",
"Include debug logs": "Hata ayıklama kütüğünü dahil et",
"Incompatible versions": "Uyumsuz sürümler",
@@ -43,7 +41,6 @@
"Join call now": "Aramaya katıl",
"Join existing call?": "Mevcut aramaya katıl?",
"Leave": ık",
"Loading room…": "Oda yükleniyor…",
"Loading…": "Yükleniyor…",
"Local volume": "Yerel ses seviyesi",
"Logging in…": "Giriliyor…",
@@ -53,7 +50,6 @@
"Microphone permissions needed to join the call.": "Aramaya katılmak için mikrofon erişim izni gerek.",
"Microphone {{n}}": "{{n}}. mikrofon",
"More": "Daha",
"More menu": "Daha fazla",
"Mute microphone": "Mikrofonu kapat",
"No": "Hayır",
"Not now, return to home screen": "Şimdi değil, ev ekranına dön",
@@ -71,8 +67,6 @@
"Release to stop": "Kesmek için bırakın",
"Remove": ıkar",
"Return to home screen": "Ev ekranına geri dön",
"Save": "Kaydet",
"Saving…": "Kaydediliyor…",
"Select an option": "Bir seçenek seç",
"Send debug logs": "Hata ayıklama kütüğünü gönder",
"Sending…": "Gönderiliyor…",
@@ -84,14 +78,12 @@
"Spatial audio": "Uzamsal ses",
"Stop sharing screen": "Ekran paylaşmayı terk et",
"Submit feedback": "Geri bildirim ver",
"Submitting feedback…": "Geri bildirimler gönderiliyor…",
"Take me Home": "Ev ekranına gir",
"Talking…": "Konuşuyor…",
"Thanks! We'll get right on it.": "Sağol! Bununla ilgileneceğiz.",
"This call already exists, would you like to join?": "Bu arama zaten var, katılmak ister misiniz?",
"{{count}} people connected|one": "{{count}} kişi bağlı",
"{{count}} people connected|other": "{{count}} kişi bağlı",
"{{displayName}}, your call is now ended": "Aramanız bitti, {{displayName]}!",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} sunuyor",
"{{name}} is talking…": "{{name}} konuşuyor…",

View File

@@ -14,7 +14,6 @@
"Version: {{version}}": "Версія: {{version}}",
"Username": "Ім'я користувача",
"User menu": "Меню користувача",
"User ID": "ID користувача",
"Unmute microphone": "Увімкнути мікрофон",
"Turn on camera": "Увімкнути камеру",
"Turn off camera": "Вимкнути камеру",
@@ -25,7 +24,6 @@
"Talking…": "Говоріть…",
"Talk over speaker": "Говорити через динамік",
"Take me Home": "Перейти до Домівки",
"Submitting feedback…": "Надсилання відгуку…",
"Submit feedback": "Надіслати відгук",
"Stop sharing screen": "Припинити показ екрана",
"Spotlight": "У центрі уваги",
@@ -38,11 +36,9 @@
"Share screen": "Поділитися екраном",
"Settings": "Налаштування",
"Sending…": "Надсилання…",
"Sending debug logs…": "Надсилання журналу зневадження…",
"Send debug logs": "Надіслати журнал зневадження",
"Sending debug logs…": "Надсилання журналу налагодження…",
"Send debug logs": "Надіслати журнал налагодження",
"Select an option": "Вибрати опцію",
"Saving…": "Збереження…",
"Save": "Зберегти",
"Return to home screen": "Повернутися на екран домівки",
"Remove": "Вилучити",
"Release to stop": "Відпустіть, щоб закінчити",
@@ -63,7 +59,6 @@
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
"No": "Ні",
"Mute microphone": "Заглушити мікрофон",
"More menu": "Усе меню",
"More": "Докладніше",
"Microphone permissions needed to join the call.": "Для участі у виклику необхідний дозвіл на користування мікрофоном.",
"Microphone {{n}}": "Мікрофон {{n}}",
@@ -72,7 +67,6 @@
"Login": "Увійти",
"Logging in…": "Вхід…",
"Local volume": "Локальна гучність",
"Loading room…": "Завантаження кімнати…",
"Leave": "Вийти",
"Join existing call?": "Приєднатися до наявного виклику?",
"Join call now": "Приєднатися до виклику зараз",
@@ -82,22 +76,20 @@
"Inspector": "Інспектор",
"Incompatible versions!": "Несумісні версії!",
"Incompatible versions": "Несумісні версії",
"Include debug logs": "Долучити журнали зневадження",
"Include debug logs": "Долучити журнали налагодження",
"Home": "Домівка",
"Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.",
"Grid layout menu": "Меню у вигляді сітки",
"Go": "Далі",
"Full screen": "Повноекранний режим",
"Freedom": "Свобода",
"Fetching group call timed out.": "Вичерпано час очікування групового виклику.",
"Exit full screen": "Вийти з повноекранного режиму",
"Download debug logs": "Завантажити журнали зневадження",
"Download debug logs": "Завантажити журнали налагодження",
"Display name": "Показуване ім'я",
"Developer": "Розробнику",
"Details": "Подробиці",
"Description (optional)": "Опис (необов'язково)",
"Debug log request": "Запит журналу зневадження",
"Debug log": "Журнал зневадження",
"Debug log request": "Запит журналу налагодження",
"Debug log": "Журнал налагодження",
"Create account": "Створити обліковий запис",
"Copy and share this call link": "Скопіювати та поділитися цим посиланням на виклик",
"Copied!": "Скопійовано!",
@@ -114,7 +106,7 @@
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Далі», ви погодитеся з нашими <2>Умовами та положеннями</2>",
"Avatar": "Аватар",
"Audio": "Звук",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал зневадження.",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал налагодження.",
"Accept microphone permissions to join the call.": "Надайте дозволи на використання мікрофонів для приєднання до виклику.",
"Accept camera/microphone permissions to join the call.": "Надайте дозвіл на використання камери/мікрофона для приєднання до виклику.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?</0><1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів</1>",
@@ -124,18 +116,31 @@
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{name}} балакає…",
"{{name}} is presenting": "{{name}} показує",
"{{displayName}}, your call is now ended": "{{displayName}}, ваш виклик завершено",
"{{count}} people connected|other": "{{count}} під'єдналися",
"{{count}} people connected|one": "{{count}} під'єднується",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
"{{name}} (Connecting...)": "{{name}} (З'єднання...)",
"Allow analytics": "Дозволити аналітику",
"Advanced": "Розширені",
"Element Call Home": "Домівка Element Call",
"Copy": "Копіювати",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Це дозволить надсилати анонімні дані (такі як тривалість виклику та кількість учасників) команді Element Call, щоб допомогти нам оптимізувати роботу застосунку на основі того, як він використовується.",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Чи вмикати/вимикати мікрофон однією клавішею, наприклад, «m» для ввімкнення/вимкнення мікрофона.",
"Single-key keyboard shortcuts": "Одноклавішні комбінації клавіш",
"{{name}} (Waiting for video...)": "{{name}} (Очікування на відео...)",
"This feature is only supported on Firefox.": "Ця функція підтримується лише в браузері Firefox."
"This feature is only supported on Firefox.": "Ця функція підтримується лише в браузері Firefox.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Надсилання журналів налагодження допоможе нам виявити проблему.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Йой, щось пішло не за планом.</0>",
"Use the upcoming grid system": "Використовувати майбутню сіткову систему",
"Expose developer settings in the settings window.": "Відкрийте налаштування розробника у вікні налаштувань.",
"Developer Settings": "Налаштування розробника",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Користуючись дочасним доступом, ви даєте згоду на збір анонімних даних, які ми використовуємо для вдосконалення продукту. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашій <2>Політиці Приватності</2> і нашій <5>Політиці про куки</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.",
"Your feedback": "Ваш відгук",
"Thanks, we received your feedback!": "Дякуємо, ми отримали ваш відгук!",
"Submitting…": "Надсилання…",
"Submit": "Надіслати",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
"Feedback": "Відгук",
"<0>Thanks for your feedback!</0>": "<0>Дякуємо за ваш відгук!</0>",
"{{count}} stars|one": "{{count}} зірка",
"{{count}} stars|other": "{{count}} зірок",
"{{displayName}}, your call has ended.": "{{displayName}}, ваш виклик завершено.",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.</0>",
"How did it go?": "Вам усе сподобалось?"
}

105
public/locales/vi/app.json Normal file
View File

@@ -0,0 +1,105 @@
{
"Login": "Đăng nhập",
"{{count}} people connected|other": "{{count}} người đã kết nối",
"{{name}} (Waiting for video...)": "{{name}} (Đang đợi truyền hình...)",
"Join call": "Tham gia cuộc gọi",
"Mute microphone": "Tắt micrô",
"Password": "Mật khẩu",
"Settings": "Cài đặt",
"Sending…": "Đang gửi…",
"Sign in": "Đăng nhập",
"Submit": "Gửi",
"Video call name": "Tên cuộc gọi truyền hình",
"Video call": "Gọi truyền hình",
"Video": "Truyền hình",
"Username": "Tên người dùng",
"Yes, join call": "Vâng, tham gia cuộc gọi",
"Your feedback": "Phản hồi của bạn",
"{{count}} people connected|one": "{{count}} người đã kết nối",
"{{name}} (Connecting...)": "{{name}} (Đang kết nối...)",
"Your recent calls": "Cuộc gọi gần đây",
"You can't talk at the same time": "Bạn không thể nói cùng thời điểm",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC không được hỗ trợ hay bị chặn trong trình duyệt này.",
"Waiting for network": "Đang đợi kết nối mạng",
"Waiting for other participants…": "Đang đợi những người khác…",
"Version: {{version}}": "Phiên bản: {{version}}",
"Turn on camera": "Bật máy quay",
"Turn off camera": "Tắt máy quay",
"Submit feedback": "Gửi phản hồi",
"Stop sharing screen": "Ngừng chia sẻ màn hình",
"Speaker": "Loa",
"Sign out": "Đăng xuất",
"Share screen": "Chia sẻ màn hình",
"No": "Không",
"Invite people": "Mời mọi người",
"Join call now": "Tham gia cuộc gọi",
"Create account": "Tạo tài khoản",
"{{name}} is presenting": "{{name}} đang thuyết trình",
"{{name}} is talking…": "{{name}} đang nói…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Cuộc gọi thoại",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Tạo tài khoản</0> Hay <2>Tham gia dưới tên khác</2>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Tham gia cuộc gọi</0><1>hay</1><2>Sao chép liên kết cuộc gọi và tham gia sau</2>",
"Accept camera/microphone permissions to join the call.": "Chấp nhận quyền sử dụng máy quay/micrô để tham gia cuộc gọi.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề.</0>",
"Avatar": "Ảnh đại diện",
"Audio": "Âm thanh",
"Camera/microphone permissions needed to join the call.": "Cần quyền máy quay/micrô để tham gia cuộc gọi.",
"Camera": "Máy quay",
"Camera {{n}}": "Máy quay {{n}}",
"Call link copied": "Đã sao chép liên kết cuộc gọi",
"Copied!": "Đã sao chép!",
"Connection lost": "Mất kết nối",
"Confirm password": "Xác nhận mật khẩu",
"Close": "Đóng",
"Change layout": "Thay đổi bố cục",
"Debug log": "Nhật ký gỡ lỗi",
"Copy": "Sao chép",
"Copy and share this call link": "Sao chép và chia sẻ liên kết cuộc gọi này",
"Display name": "Tên hiển thị",
"Developer Settings": "Cài đặt phát triển",
"Developer": "Nhà phát triển",
"Details": "Chi tiết",
"Download debug logs": "Tải xuống nhật ký gỡ lỗi",
"Feedback": "Phản hồi",
"Full screen": "Toàn màn hình",
"Incompatible versions!": "Phiên bản không tương thích!",
"Incompatible versions": "Phiên bản không tương thích",
"Include debug logs": "Kèm theo nhật ký gỡ lỗi",
"Invite": "Mời",
"Join existing call?": "Tham gia cuộc gọi?",
"Leave": "Rời",
"Loading…": "Đang tải…",
"Logging in…": "Đang đăng nhập…",
"Login to your account": "Đăng nhập vào tài khoản của bạn",
"Microphone": "Micrô",
"Microphone {{n}}": "Micrô {{n}}",
"Not registered yet? <2>Create an account</2>": "Chưa đăng ký? <2>Tạo tài khoản</2>",
"Passwords must match": "Mật khẩu phải khớp",
"Press and hold spacebar to talk": "Nhấn và giữ phím space để nói",
"Press and hold to talk": "Nhấn và giữ để nói",
"Press and hold to talk over {{name}}": "Nhấn và giữ để nói qua {{name}}",
"Register": "Đăng ký",
"Release spacebar key to stop": "Nhả phím space để ngừng",
"Spotlight": "Tiêu điểm",
"Submitting…": "Đang gửi…",
"Thanks, we received your feedback!": "Cảm ơn, chúng tôi đã nhận được phản hồi!",
"Talking…": "Đang nói…",
"Walkie-talkie call": "Cuộc gọi thoại",
"Walkie-talkie call name": "Tên cuộc gọi thoại",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Đã có tài khoản?</0><1><0>Đăng nhập</0> Hay <2>Tham gia dưới tên Khách</2></1>",
"Exit full screen": "Rời chế độ toàn màn hình",
"Profile": "Hồ sơ",
"Registering…": "Đang đăng ký…",
"Unmute microphone": "Bật micrô",
"This call already exists, would you like to join?": "Cuộc gọi đã tồn tại, bạn có muốn tham gia không?",
"This feature is only supported on Firefox.": "Tính năng này chỉ được hỗ trợ trên Firefox.",
"Speaker {{n}}": "Loa {{n}}",
"Recaptcha not loaded": "Chưa tải được Recaptcha",
"Debug log request": "Yêu cầu nhật ký gỡ lỗi",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Khi nhấn vào \"Tham gia cuộc gọi\", bạn đồng ý với <2>Điều khoản và điều kiện</2> của chúng tôi",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Một người dùng khác trong cuộc gọi đang gặp vấn đề. Để có thể chẩn đoán tốt hơn chúng tôi muốn thu thập nhật ký gỡ lỗi.",
"Accept microphone permissions to join the call.": "Chấp nhận quyền sử dụng micrô để tham gia cuộc gọi.",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Tại sao lại không hoàn thiện bằng cách đặt mật khẩu để giữ tài khoản của bạn?</0><1>Bạn sẽ có thể giữ tên và đặt ảnh đại diện cho những cuộc gọi tiếp theo.</1>",
"Press and hold spacebar to talk over {{name}}": "Nhấn và giữ phím space để nói trên {{name}}"
}

View File

@@ -2,7 +2,6 @@
"Your recent calls": "最近通话",
"You can't talk at the same time": "你不能在同一时间发言",
"Yes, join call": "是,加入通话",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "是否启用单键键盘快捷键,例如,'m'可使麦克风静音/取消静音。",
"WebRTC is not supported or is being blocked in this browser.": "此浏览器不支持WebRTC或WebRTC被浏览器阻止。",
"Walkie-talkie call name": "对讲机通话名称",
"Walkie-talkie call": "对讲机通话",
@@ -14,11 +13,9 @@
"Version: {{version}}": "版本:{{version}}",
"Username": "用户名",
"User menu": "用户菜单",
"User ID": "用户ID",
"Unmute microphone": "取消麦克风静音",
"Turn on camera": "开启摄像头",
"Turn off camera": "关闭摄像头",
"This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "这将向Element Call团队发送匿名数据如通话的持续时间和参与者的数量以帮助我们根据使用方式优化应用程序。",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "这将使发言人的音频看起来像是来自他们在屏幕上的位置。(实验性功能:这可能影响音频的稳定性)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "本网站受reCaptcha保护并适用Google<2>隐私政策</2>和<6>服务条款</6>。<9></9>点击\"注册\"则表明您同意我们的<12>条款和条件</12>",
"This call already exists, would you like to join?": "该通话已存在,你想加入吗?",
@@ -26,20 +23,16 @@
"Talking…": "正在发言……",
"Talk over speaker": "通过扬声器发言",
"Take me Home": "返回主页",
"Submitting feedback…": "正在提交反馈……",
"Submit feedback": "提交反馈",
"Stop sharing screen": "停止屏幕共享",
"Spotlight": "聚焦模式",
"Speaker {{n}}": "发言人 {{n}}",
"Speaker": "发言人",
"Spatial audio": "空间音频",
"Single-key keyboard shortcuts": "单键键盘快捷方式",
"Sign out": "注销登录",
"Sign in": "登录",
"Audio": "音频",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "这个通话中的另一个用户出现了问题。为了更好地诊断这些问题,我们想收集调试日志。",
"Allow analytics": "允许进行分析",
"Advanced": "偏好",
"Accept microphone permissions to join the call.": "授予麦克风权限以加入通话。",
"Accept camera/microphone permissions to join the call.": "授予摄像头/麦克风权限以加入通话。",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>为什么不设置一个密码来保留你的账户?</0><1>你将可以保留你的名字并设置一个头像,以便在未来的通话中使用。</1>",
@@ -52,7 +45,6 @@
"{{name}} is presenting": "{{name}}正在展示",
"{{name}} (Waiting for video...)": "{{name}}(等待视频……)",
"{{name}} (Connecting...)": "{{name}} (正在连接……)",
"{{displayName}}, your call is now ended": "{{displayName}},您的通话已结束",
"{{count}} people connected|other": "{{count}}人已连接",
"{{count}} people connected|one": "{{count}}人已连接",
"Inspector": "检查器",
@@ -63,8 +55,6 @@
"Sending debug logs…": "正在发送调试日志……",
"Send debug logs": "发送调试日志",
"Select an option": "选择一个选项",
"Saving…": "正在保存……",
"Save": "保存",
"Return to home screen": "返回主页",
"Remove": "移除",
"Release to stop": "松开后停止",
@@ -85,7 +75,6 @@
"Not now, return to home screen": "暂不,先返回主页",
"No": "否",
"Mute microphone": "麦克风静音",
"More menu": "更多",
"More": "更多",
"Microphone permissions needed to join the call.": "加入通话需要麦克风权限。",
"Microphone {{n}}": "麦克风 {{n}}",
@@ -95,7 +84,6 @@
"Logging in…": "登录中……",
"Local volume": "本地音量",
"Loading…": "加载中……",
"Loading room…": "加载房间中……",
"Leave": "离开",
"Join existing call?": "加入现有的通话?",
"Join call now": "现在加入通话",
@@ -106,7 +94,6 @@
"Incompatible versions": "不兼容版本",
"Include debug logs": "包含调试日志",
"Home": "主页",
"Having trouble? Help us fix it.": "遇到麻烦?帮助我们解决问题。",
"Grid layout menu": "网格布局菜单",
"Go": "开始",
"Full screen": "全屏",
@@ -118,7 +105,6 @@
"Display name": "显示名称",
"Developer": "开发者",
"Details": "详情",
"Description (optional)": "描述(可选)",
"Debug log request": "调试日志请求",
"Debug log": "调试日志",
"Create account": "创建账户",
@@ -136,5 +122,7 @@
"Call type menu": "通话类型菜单",
"Call link copied": "链接已复制",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "点击“现在加入”则表示同意我们的<2>条款与条件<2>",
"Avatar": "头像"
"Avatar": "头像",
"<0>Oops, something's gone wrong.</0>": "<0>哎哟,出问题了。</0>",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": ""
}

View File

@@ -1,14 +1,146 @@
{
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>加入通話</0><1>或</1><2>複製通話連結並稍候加入</2>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>建立帳號</0> 或 <2>訪客模式</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>已經有帳號了?</0><1><0>登入</0> Or <2>訪客模式登入</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - 無線電通話",
"{{names}}, {{name}}": "{{names}}{{name}}",
"{{name}} is talking…": "{{name}} 正在交談中…",
"{{name}} is presenting": "{{name}} 正在報告",
"{{name}} (Waiting for video...)": "{{name}} (等視訊畫面 ...)",
"{{name}} (Connecting...)": "{{name}} (連線中 ...)",
"{{displayName}}, your call is now ended": "{{displayName}},您的通話現在已結束",
"{{count}} people connected|other": "{{count}} 人已連",
"{{count}} people connected|one": "{{count}} 人已連線"
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>現在加入通話</0><1>或</1><2>複製通話連結,稍後再加入</2>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>建立帳號</0> 或<2>訪客身份登入</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>已經有帳號?</0><1><0>登入</0> <2>訪客身份登入</2></1>",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - 對講機式通話",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{name}} 正在發言…",
"{{name}} is presenting": "{{name}} 已上線",
"{{name}} (Waiting for video...)": "{{name}} (等視訊...)",
"{{name}} (Connecting...)": "{{name}} (連結中...)",
"{{count}} people connected|other": "{{count}} 人已連結",
"{{count}} people connected|one": "{{count}} 人已連",
"Use the upcoming grid system": "使用即將推出的網格系統",
"Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。",
"Developer Settings": "開發者設定",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>送出除錯紀錄,可幫助我們修正問題。</0>",
"<0>Oops, something's gone wrong.</0>": "<0>喔喔,有些地方怪怪的。</0>",
"Your recent calls": "您最近的通話",
"You can't talk at the same time": "您無法在同一時間發言",
"Yes, join call": "是,加入對話",
"WebRTC is not supported or is being blocked in this browser.": "此瀏覽器未支援 WebRTC 或 WebRTC 被瀏覽器封鎖。",
"Walkie-talkie call name": "對講機式通話名稱",
"Walkie-talkie call": "即時通話",
"Waiting for other participants…": "等待其他參加者…",
"Waiting for network": "等待網路連線",
"Video call name": "視訊通話姓名",
"Video call": "視訊通話",
"Video": "視訊",
"Version: {{version}}": "版本: {{version}}",
"Username": "使用者名稱",
"User menu": "使用者選單",
"Unmute microphone": "取消麥克風靜音",
"Turn on camera": "開啟相機",
"Turn off camera": "關閉相機",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "這會使得發言者的聲音聽起來,像從他們在畫面中的位置傳來(實驗性功能:這可能會影響語音的穩定性。)",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "此網站使用Google 驗證碼技術保護,適用<2>隱私條款</2> 與<6>條款與細則</6> 。<9></9>按下「註冊」,表示您同意我們的<12>條款與細則</12>",
"This feature is only supported on Firefox.": "只有 Firefox 支援此功能。",
"This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?",
"Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。",
"Talking…": "對話中…",
"Talk over speaker": "以擴音對話",
"Take me Home": "帶我回主畫面",
"Submit feedback": "遞交回覆",
"Stop sharing screen": "停止分享螢幕畫面",
"Spotlight": "聚焦",
"Speaker {{n}}": "發言者{{n}}",
"Speaker": "發言者",
"Spatial audio": "空間音效",
"Sign out": "登出",
"Sign in": "登入",
"Show call inspector": "顯示通話稽查員",
"Share screen": "分享畫面",
"Settings": "設定",
"Sending…": "傳送中…",
"Sending debug logs…": "傳送除錯記錄檔中…",
"Send debug logs": "傳送除錯紀錄",
"Select an option": "選擇一個選項",
"Return to home screen": "回到首頁",
"Remove": "移除",
"Release to stop": "放開以停止",
"Release spacebar key to stop": "放開空白鍵以停止",
"Registering…": "註冊中…",
"Register": "註冊",
"Recaptcha not loaded": "驗證碼未載入",
"Recaptcha dismissed": "略過驗證碼",
"Profile": "個人檔案",
"Press and hold to talk over {{name}}": "與{{name}}對話時,請按住按鍵",
"Press and hold to talk": "請按住按鍵來發言",
"Press and hold spacebar to talk over {{name}}": "與{{name}}對話時,請按住空白鍵",
"Press and hold spacebar to talk": "說話時請按住空白鍵",
"Passwords must match": "密碼必須相符",
"Password": "密碼",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "有使用者試著加入通話,但他們的軟體版本不相容。這些使用者需要確認已將瀏覽器更新到最新版本:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "還沒註冊嗎?<2>建立帳號</2>",
"Not now, return to home screen": "現在不行,回到首頁",
"No": "否",
"Mute microphone": "麥克風靜音",
"More": "更多",
"Microphone permissions needed to join the call.": "加入通話前需要取得麥克風的權限。",
"Microphone {{n}}": "麥克風 {{n}}",
"Microphone": "麥克風",
"Login to your account": "登入您的帳號",
"Login": "登入",
"Logging in…": "登入中…",
"Local volume": "您的音量",
"Loading…": "載入中…",
"Leave": "離開",
"Join existing call?": "加入已開始的通話嗎?",
"Join call now": "現在加入通話",
"Join call": "加入通話",
"Invite people": "邀請夥伴",
"Invite": "邀請",
"Inspector": "稽查員",
"Incompatible versions!": "不相容版本!",
"Incompatible versions": "不相容版本",
"Include debug logs": "包含除錯紀錄",
"Home": "首頁",
"Grid layout menu": "格框式清單",
"Go": "前往",
"Full screen": "全螢幕",
"Freedom": "自由",
"Fetching group call timed out.": "加入群組對話已逾時。",
"Exit full screen": "退出全螢幕",
"Element Call Home": "Element Call 首頁",
"Download debug logs": "下載偵錯報告",
"Display name": "顯示名稱",
"Developer": "開發者",
"Details": "詳細說明",
"Debug log request": "請求偵錯報告",
"Debug log": "除錯紀錄",
"Create account": "建立帳號",
"Copy and share this call link": "複製並分享通話連結",
"Copy": "複製",
"Copied!": "已複製!",
"Connection lost": "連線中斷",
"Confirm password": "確認密碼",
"Close": "關閉",
"Change layout": "變更排列",
"Camera/microphone permissions needed to join the call.": "加入通話需要取得相機/麥克風的權限。",
"Camera {{n}}": "相機 {{n}}",
"Camera": "相機",
"Call type menu": "通話類型選單",
"Call link copied": "已複製通話連結",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "當您按下「加入通話」,您也同時同意了我們的條款與細則",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "當您按下「前往」,你也同意了我們的條款與細則",
"Avatar": "大頭照",
"Audio": "語音",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。",
"Accept microphone permissions to join the call.": "請授權使用您的麥克風以加入通話。",
"Accept camera/microphone permissions to join the call.": "請授權使用您的相機/麥克風以加入對話。",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>何不設定密碼以保留此帳號?</0><1>您可以保留暱稱並設定頭像,以便未來通話時使用</1>",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "參與此測試版即表示您同意蒐集匿名資料,我們使用這些資料來改進產品。您可以在我們的<2>隱私政策</2>與我們的 <5>Cookie 政策</5> 中找到關於我們追蹤哪些資料的更多資訊。",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。",
"Your feedback": "您的回饋",
"Thanks, we received your feedback!": "感謝,我們已經收到您的回饋了!",
"Submitting…": "正在遞交……",
"Submit": "遞交",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。",
"Feedback": "回饋",
"{{count}} stars|other": "{{count}} 個星星",
"<0>Thanks for your feedback!</0>": "<0>感謝您的回饋!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>我們想要聽到您的回饋,如此我們才能改善您的體驗。</0>",
"{{count}} stars|one": "{{count}} 個星星",
"{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。",
"How did it go?": "進展如何?"
}

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
# This script can be used to reformat the release notes generated by
# GitHub releases into a format slightly more appropriate for our
# project (ie. we don't really need to mention the author of every PR)
#
# eg. in: * OpenTelemetry by @dbkr in https://github.com/vector-im/element-call/pull/961
# out: * OpenTelemetry (https://github.com/vector-im/element-call/pull/961)
import sys
import re
for line in sys.stdin:
matches = re.match(r'^\* (.*) by (\S+) in (\S+)$', line.strip())
print("* %s (%s)" % (matches[1], matches[3]))

View File

@@ -1,5 +1,5 @@
/*
Copyright 2021 New Vector Ltd
Copyright 2021 - 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.
@@ -29,7 +29,9 @@ import { usePageFocusStyle } from "./usePageFocusStyle";
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
import { InspectorContextProvider } from "./room/GroupCallInspector";
import { CrashView, LoadingView } from "./FullScreenView";
import { DisconnectedBanner } from "./DisconnectedBanner";
import { Initializer } from "./initializer";
import { MediaHandlerProvider } from "./settings/useMediaHandler";
const SentryRoute = Sentry.withSentryRouting(Route);
@@ -55,32 +57,35 @@ export default function App({ history }: AppProps) {
{loaded ? (
<Suspense fallback={null}>
<ClientProvider>
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
</SentryRoute>
</Switch>
</OverlayProvider>
</Sentry.ErrorBoundary>
</InspectorContextProvider>
<MediaHandlerProvider>
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<DisconnectedBanner />
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
</SentryRoute>
</Switch>
</OverlayProvider>
</Sentry.ErrorBoundary>
</InspectorContextProvider>
</MediaHandlerProvider>
</ClientProvider>
</Suspense>
) : (

View File

@@ -25,9 +25,10 @@ import React, {
useRef,
} from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { ErrorView } from "./FullScreenView";
import {
@@ -36,7 +37,10 @@ import {
fallbackICEServerAllowed,
} from "./matrix-utils";
import { widget } from "./widget";
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
import {
PosthogAnalytics,
RegistrationType,
} from "./analytics/PosthogAnalytics";
import { translatedError } from "./TranslatedError";
import { useEventTarget } from "./useEvents";
import { Config } from "./config/Config";
@@ -67,6 +71,8 @@ const loadSession = (): Session => {
const saveSession = (session: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
const clearSession = () => localStorage.removeItem("matrix-auth-store");
const isDisconnected = (syncState, syncData) =>
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";
interface ClientState {
loading: boolean;
@@ -78,6 +84,7 @@ interface ClientState {
logout: () => void;
setClient: (client: MatrixClient, session: Session) => void;
error?: Error;
disconnected: boolean;
}
const ClientContext = createContext<ClientState>(null);
@@ -95,7 +102,15 @@ export const ClientProvider: FC<Props> = ({ children }) => {
const history = useHistory();
const initializing = useRef(false);
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
{
loading,
isAuthenticated,
isPasswordlessUser,
client,
userName,
error,
disconnected,
},
setState,
] = useState<ClientProviderState>({
loading: true,
@@ -104,8 +119,18 @@ export const ClientProvider: FC<Props> = ({ children }) => {
client: undefined,
userName: null,
error: undefined,
disconnected: false,
});
const onSync = (state: SyncState, _old: SyncState, data: ISyncStateData) => {
setState((currentState) => {
const disconnected = isDisconnected(state, data);
return disconnected === currentState.disconnected
? currentState
: { ...currentState, disconnected };
});
};
useEffect(() => {
// In case the component is mounted, unmounted, and remounted quickly (as
// React does in strict mode), we need to make sure not to doubly initialize
@@ -180,9 +205,10 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}
}
};
let clientWithListener: MatrixClient;
init()
.then(({ client, isPasswordlessUser }) => {
clientWithListener = client;
setState({
client,
loading: false,
@@ -190,7 +216,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isPasswordlessUser,
userName: client?.getUserIdLocalpart(),
error: undefined,
disconnected: isDisconnected(
client?.getSyncState,
client?.getSyncStateData
),
});
clientWithListener?.on(ClientEvent.Sync, onSync);
})
.catch((err) => {
logger.error(err);
@@ -201,9 +232,13 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isPasswordlessUser: false,
userName: null,
error: undefined,
disconnected: false,
});
})
.finally(() => (initializing.current = false));
return () => {
clientWithListener?.removeListener(ClientEvent.Sync, onSync);
};
}, []);
const changePassword = useCallback(
@@ -232,6 +267,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
error: undefined,
disconnected: false,
});
},
[client]
@@ -253,6 +289,10 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isPasswordlessUser: session.passwordlessUser,
userName: newClient.getUserIdLocalpart(),
error: undefined,
disconnected: isDisconnected(
newClient.getSyncState(),
newClient.getSyncStateData()
),
});
} else {
clearSession();
@@ -264,6 +304,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isPasswordlessUser: false,
userName: null,
error: undefined,
disconnected: false,
});
}
},
@@ -281,6 +322,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isPasswordlessUser: true,
userName: "",
error: undefined,
disconnected: false,
});
history.push("/");
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
@@ -323,6 +365,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
userName,
setClient,
error: undefined,
disconnected,
}),
[
loading,
@@ -333,12 +376,16 @@ export const ClientProvider: FC<Props> = ({ children }) => {
logout,
userName,
setClient,
disconnected,
]
);
useEffect(() => {
window.matrixclient = client;
window.isPasswordlessUser = isPasswordlessUser;
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
}, [client, isPasswordlessUser]);
if (error) {

View File

@@ -0,0 +1,27 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.banner {
position: absolute;
padding: 29px;
background-color: var(--quaternary-content);
vertical-align: middle;
font-size: var(--font-size-body);
text-align: center;
z-index: 1;
top: 76px;
width: calc(100% - 58px);
}

View File

@@ -0,0 +1,47 @@
/*
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 classNames from "classnames";
import React, { HTMLAttributes, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css";
import { useClient } from "./ClientContext";
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> {
children?: ReactNode;
className?: string;
}
export function DisconnectedBanner({
children,
className,
...rest
}: DisconnectedBannerProps) {
const { t } = useTranslation();
const { disconnected } = useClient();
return (
<>
{disconnected && (
<div className={classNames(styles.banner, className)} {...rest}>
{children}
{t("Connectivity to the server has been lost.")}
</div>
)}
</>
);
}

View File

@@ -134,7 +134,9 @@ export function RoomHeaderInfo({ roomName, avatarUrl }: RoomHeaderInfo) {
/>
<VideoIcon width={16} height={16} />
</div>
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
<Subtitle data-testid="roomHeader_roomName" fontWeight="semiBold">
{roomName}
</Subtitle>
</>
);
}

View File

@@ -40,7 +40,7 @@ limitations under the License.
.modalHeader {
display: flex;
justify-content: space-between;
padding: 34px 34px 0 34px;
padding: 34px 32px 0 32px;
}
.modalHeader h3 {
@@ -72,7 +72,7 @@ limitations under the License.
.modalHeader {
display: flex;
justify-content: space-between;
padding: 24px 24px 0 24px;
padding: 32px 20px 0 20px;
}
.modal.mobileFullScreen {

View File

@@ -92,6 +92,7 @@ export function Modal({
{...closeButtonProps}
ref={closeButtonRef}
className={styles.closeButton}
data-testid="modal_close"
title={t("Close")}
>
<CloseIcon />

View File

@@ -79,6 +79,15 @@ export interface UrlParams {
* The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web.
*/
analyticsID: string | null;
/**
* Whether the app is allowed to use fallback STUN servers for ICE in case the
* user's homeserver doesn't provide any.
*/
allowIceFallback: boolean;
/**
* Whether the app is allowed screen share only mode
*/
allowVoipWithNoMedia: boolean;
}
/**
@@ -135,6 +144,8 @@ export const getUrlParams = (
fonts: getAllParams("font"),
fontScale: Number.isNaN(fontScale) ? null : fontScale,
analyticsID: getParam("analyticsID"),
allowIceFallback: hasParam("allowIceFallback"),
allowVoipWithNoMedia: hasParam("allowVoipWithNoMedia"),
};
};

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.
@@ -25,6 +25,7 @@ import { Menu } from "./Menu";
import { TooltipTrigger } from "./Tooltip";
import { Avatar, Size } from "./Avatar";
import { ReactComponent as UserIcon } from "./icons/User.svg";
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
import { Body } from "./typography/Typography";
@@ -58,6 +59,12 @@ export function UserMenu({
key: "user",
icon: UserIcon,
label: displayName,
dataTestid: "usermenu_user",
});
arr.push({
key: "settings",
icon: SettingsIcon,
label: t("Settings"),
});
if (isPasswordlessUser && !preventNavigation) {
@@ -65,6 +72,7 @@ export function UserMenu({
key: "login",
label: t("Sign in"),
icon: LoginIcon,
dataTestid: "usermenu_login",
});
}
@@ -73,6 +81,7 @@ export function UserMenu({
key: "logout",
label: t("Sign out"),
icon: LogoutIcon,
dataTestid: "usermenu_logout",
});
}
}
@@ -93,7 +102,11 @@ export function UserMenu({
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={tooltip} placement="bottom left">
<Button variant="icon" className={styles.userButton}>
<Button
variant="icon"
className={styles.userButton}
data-testid="usermenu_open"
>
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
<Avatar
size={Size.SM}
@@ -108,9 +121,14 @@ export function UserMenu({
</TooltipTrigger>
{(props) => (
<Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label }) => (
{items.map(({ key, icon: Icon, label, dataTestid }) => (
<Item key={key} textValue={label}>
<Icon width={24} height={24} className={styles.menuIcon} />
<Icon
width={24}
height={24}
className={styles.menuIcon}
data-testid={dataTestid}
/>
<Body overflowEllipsis>{label}</Body>
</Item>
))}

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,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useClient } from "./ClientContext";
import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { ProfileModal } from "./profile/ProfileModal";
import { SettingsModal } from "./settings/SettingsModal";
import { UserMenu } from "./UserMenu";
interface Props {
@@ -35,10 +35,17 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
const onAction = useCallback(
(value: string) => {
async (value: string) => {
switch (value) {
case "user":
setDefaultSettingsTab("profile");
modalState.open();
break;
case "settings":
setDefaultSettingsTab("audio");
modalState.open();
break;
case "logout":
@@ -64,7 +71,13 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
displayName || (userName ? userName.replace("@", "") : undefined)
}
/>
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
{modalState.isOpen && (
<SettingsModal
client={client}
defaultTab={defaultSettingsTab}
{...modalProps}
/>
)}
</>
);
}

View File

@@ -0,0 +1,14 @@
import React, { FC } from "react";
import { Trans } from "react-i18next";
import { Link } from "../typography/Typography";
export const AnalyticsNotice: FC = () => (
<Trans>
By participating in this beta, you consent to the collection of anonymous
data, which we use to improve the product. You can find more information
about which data we track in our{" "}
<Link href="https://element.io/privacy">Privacy Policy</Link> and our{" "}
<Link href="https://element.io/cookie-policy">Cookie Policy</Link>.
</Trans>
);

View File

@@ -19,8 +19,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient } from "matrix-js-sdk";
import { Buffer } from "buffer";
import { widget } from "./widget";
import { getSetting, setSetting, settingsBus } from "./settings/useSetting";
import { widget } from "../widget";
import { getSetting, setSetting, settingsBus } from "../settings/useSetting";
import {
CallEndedTracker,
CallStartedTracker,
@@ -28,9 +28,11 @@ import {
SignupTracker,
MuteCameraTracker,
MuteMicrophoneTracker,
UndecryptableToDeviceEventTracker,
QualitySurveyEventTracker,
} from "./PosthogEvents";
import { Config } from "./config/Config";
import { getUrlParams } from "./UrlParams";
import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams";
/* Posthog analytics tracking.
*
@@ -68,6 +70,7 @@ export enum RegistrationType {
interface PlatformProperties {
appVersion: string;
matrixBackend: "embedded" | "jssdk";
callBackend: "livekit" | "full-mesh";
}
interface PosthogSettings {
@@ -93,7 +96,7 @@ export class PosthogAnalytics {
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics";
// set true during the constructor if posthog config is present, otherwise false
private static internalInstance = null;
private static internalInstance: PosthogAnalytics | null = null;
private identificationPromise: Promise<void>;
private readonly enabled: boolean = false;
@@ -101,6 +104,10 @@ export class PosthogAnalytics {
private platformSuperProperties = {};
private registrationType: RegistrationType = RegistrationType.Guest;
public static hasInstance(): boolean {
return Boolean(this.internalInstance);
}
public static get instance(): PosthogAnalytics {
if (!this.internalInstance) {
this.internalInstance = new PosthogAnalytics(posthog);
@@ -136,6 +143,9 @@ export class PosthogAnalytics {
});
this.enabled = true;
} else {
logger.info(
"Posthog is not enabled because there is no api key or no host given in the config"
);
this.enabled = false;
}
this.startListeningToSettingsChanges();
@@ -182,6 +192,7 @@ export class PosthogAnalytics {
return {
appVersion,
matrixBackend: widget ? "embedded" : "jssdk",
callBackend: "full-mesh",
};
}
@@ -223,10 +234,8 @@ export class PosthogAnalytics {
.join("");
}
public async identifyUser(analyticsIdGenerator: () => string) {
// There might be a better way to get the client here.
if (this.anonymity == Anonymity.Pseudonymous) {
private async identifyUser(analyticsIdGenerator: () => string) {
if (this.anonymity == Anonymity.Pseudonymous && this.enabled) {
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
// different devices to send the same ID.
let analyticsID = await this.getAnalyticsId();
@@ -317,7 +326,12 @@ export class PosthogAnalytics {
this.setAnonymity(Anonymity.Disabled);
}
public updateSuperProperties() {
public onLoginStatusChanged(): void {
const optInAnalytics = getSetting("opt-in-analytics", false);
this.updateAnonymityAndIdentifyUser(optInAnalytics);
}
private updateSuperProperties() {
// Update super properties in posthog with our platform (app version, platform).
// These properties will be subsequently passed in every event.
//
@@ -337,7 +351,7 @@ export class PosthogAnalytics {
return this.eventSignup.getSignupEndTime() > new Date(0);
}
public async updateAnonymityAndIdentifyUser(
private async updateAnonymityAndIdentifyUser(
pseudonymousOptIn: boolean
): Promise<void> {
// Update this.anonymity based on the user's analytics opt-in settings
@@ -346,6 +360,10 @@ export class PosthogAnalytics {
: Anonymity.Disabled;
this.setAnonymity(anonymity);
// We may not yet have a Matrix client at this point, if not, bail. This should get
// triggered again by onLoginStatusChanged once we do have a client.
if (!window.matrixclient) return;
if (anonymity === Anonymity.Pseudonymous) {
this.setRegistrationType(
window.matrixclient.isGuest() || window.isPasswordlessUser
@@ -383,7 +401,7 @@ export class PosthogAnalytics {
this.capture(eventName, properties, options);
}
public startListeningToSettingsChanges(): void {
private startListeningToSettingsChanges(): void {
// Listen to account data changes from sync so we can observe changes to relevant flags and update.
// This is called -
// * On page load, when the account data is first received by sync
@@ -415,4 +433,6 @@ export class PosthogAnalytics {
public eventLogin = new LoginTracker();
public eventMuteMicrophone = new MuteMicrophoneTracker();
public eventMuteCamera = new MuteCameraTracker();
public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker();
public eventQualitySurvey = new QualitySurveyEventTracker();
}

View File

@@ -149,3 +149,35 @@ export class MuteCameraTracker {
});
}
}
interface UndecryptableToDeviceEvent {
eventName: "UndecryptableToDeviceEvent";
callId: string;
}
export class UndecryptableToDeviceEventTracker {
track(callId: string) {
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
eventName: "UndecryptableToDeviceEvent",
callId,
});
}
}
interface QualitySurveyEvent {
eventName: "QualitySurvey";
callId: string;
feedbackText: string;
stars: number;
}
export class QualitySurveyEventTracker {
track(callId: string, feedbackText: string, stars: number) {
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
eventName: "QualitySurvey",
callId,
feedbackText,
stars,
});
}
}

View File

@@ -0,0 +1,163 @@
/*
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 {
SpanProcessor,
ReadableSpan,
Span,
} from "@opentelemetry/sdk-trace-base";
import { hrTimeToMilliseconds } from "@opentelemetry/core";
import { logger } from "matrix-js-sdk/src/logger";
import { PosthogAnalytics } from "./PosthogAnalytics";
interface PrevCall {
callId: string;
hangupTs: number;
}
/**
* The maximum time between hanging up and joining the same call that we would
* consider a 'rejoin' on the user's part.
*/
const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
/**
* Span processor that extracts certain metrics from spans to send to PostHog
*/
export class PosthogSpanProcessor implements SpanProcessor {
async forceFlush(): Promise<void> {}
onStart(span: Span): void {
// Hack: Yield to allow attributes to be set before processing
Promise.resolve().then(() => {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipStart(span);
return;
case "matrix.groupCallMembership.summaryReport":
this.onSummaryReportStart(span);
return;
}
});
}
onEnd(span: ReadableSpan): void {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span);
return;
}
}
private get prevCall(): PrevCall | null {
// This is stored in localStorage so we can remember the previous call
// across app restarts
const data = localStorage.getItem("matrix-prev-call");
if (data === null) return null;
try {
return JSON.parse(data);
} catch (e) {
logger.warn("Invalid prev call data", data);
return null;
}
}
private set prevCall(data: PrevCall | null) {
localStorage.setItem("matrix-prev-call", JSON.stringify(data));
}
private onGroupCallMembershipStart(span: ReadableSpan): void {
const prevCall = this.prevCall;
const newCallId = span.attributes["matrix.confId"] as string;
// If the user joined the same call within a short time frame, log this as a
// rejoin. This is interesting as a call quality metric, since rejoins may
// indicate that users had to intervene to make the product work.
if (prevCall !== null && newCallId === prevCall.callId) {
const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs;
if (duration <= maxRejoinMs) {
PosthogAnalytics.instance.trackEvent({
eventName: "Rejoin",
callId: prevCall.callId,
rejoinDuration: duration,
});
}
}
}
private onGroupCallMembershipEnd(span: ReadableSpan): void {
this.prevCall = {
callId: span.attributes["matrix.confId"] as string,
hangupTs: hrTimeToMilliseconds(span.endTime),
};
}
private onSummaryReportStart(span: ReadableSpan): void {
// Searching for an event like this:
// matrix.stats.summary
// matrix.stats.summary.percentageReceivedAudioMedia: 0.75
// matrix.stats.summary.percentageReceivedMedia: 1
// matrix.stats.summary.percentageReceivedVideoMedia: 0.75
// matrix.stats.summary.maxJitter: 100
// matrix.stats.summary.maxPacketLoss: 20
const event = span.events.find((e) => e.name === "matrix.stats.summary");
if (event !== undefined) {
const attributes = event.attributes;
if (attributes) {
const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`;
const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`;
const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`;
const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`;
const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`;
const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`;
const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`;
const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`;
const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`;
const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`;
const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`;
PosthogAnalytics.instance.trackEvent(
{
eventName: "MediaReceived",
callId: span.attributes["matrix.confId"] as string,
mediaReceived: mediaReceived,
audioReceived: audioReceived,
videoReceived: videoReceived,
maxJitter: maxJitter,
maxPacketLoss: maxPacketLoss,
peerConnections: peerConnections,
percentageConcealedAudio: percentageConcealedAudio,
opponentUsersInCall: opponentUsersInCall,
opponentDevicesInCall: opponentDevicesInCall,
diffDevicesToPeerConnections: diffDevicesToPeerConnections,
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
},
// Send instantly because the window might be closing
{ send_instantly: true }
);
}
}
}
/**
* Shutdown the processor.
*/
shutdown(): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -0,0 +1,114 @@
import { Attributes } from "@opentelemetry/api";
import { hrTimeToMicroseconds } from "@opentelemetry/core";
import {
SpanProcessor,
ReadableSpan,
Span,
} from "@opentelemetry/sdk-trace-base";
const dumpAttributes = (attr: Attributes) =>
Object.entries(attr).map(([key, value]) => ({
key,
type: typeof value,
value,
}));
/**
* Exports spans on demand to the Jaeger JSON format, which can be attached to
* rageshakes and loaded into analysis tools like Jaeger and Stalk.
*/
export class RageshakeSpanProcessor implements SpanProcessor {
private readonly spans: ReadableSpan[] = [];
async forceFlush(): Promise<void> {}
onStart(span: Span): void {
this.spans.push(span);
}
onEnd(): void {}
/**
* Dumps the spans collected so far as Jaeger-compatible JSON.
*/
public dump(): string {
const now = Date.now() * 1000; // Jaeger works in microseconds
const traces = new Map<string, ReadableSpan[]>();
// Organize spans by their trace IDs
for (const span of this.spans) {
const traceId = span.spanContext().traceId;
let trace = traces.get(traceId);
if (trace === undefined) {
trace = [];
traces.set(traceId, trace);
}
trace.push(span);
}
const processId = "p1";
const processes = {
[processId]: {
serviceName: "element-call",
tags: [],
},
warnings: null,
};
return JSON.stringify({
// Honestly not sure what some of these fields mean, I just know that
// they're present in Jaeger JSON exports
total: 0,
limit: 0,
offset: 0,
errors: null,
data: [...traces.entries()].map(([traceId, spans]) => ({
traceID: traceId,
warnings: null,
processes,
spans: spans.map((span) => {
const ctx = span.spanContext();
const startTime = hrTimeToMicroseconds(span.startTime);
// If the span has not yet ended, pretend that it ends now
const duration =
span.duration[0] === -1
? now - startTime
: hrTimeToMicroseconds(span.duration);
return {
traceID: traceId,
spanID: ctx.spanId,
operationName: span.name,
processID: processId,
warnings: null,
startTime,
duration,
references:
span.parentSpanId === undefined
? []
: [
{
refType: "CHILD_OF",
traceID: traceId,
spanID: span.parentSpanId,
},
],
tags: dumpAttributes(span.attributes),
logs: span.events.map((event) => ({
timestamp: hrTimeToMicroseconds(event.time),
// The name of the event is in the "event" field, aparently.
fields: [
...dumpAttributes(event.attributes ?? {}),
{ key: "event", type: "string", value: event.name },
],
})),
};
}),
})),
});
}
async shutdown(): Promise<void> {}
}

38
src/array-utils.ts Normal file
View File

@@ -0,0 +1,38 @@
/*
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.
*/
/**
* Gets the index of the last element in the array to satsify the given
* predicate.
*/
// TODO: remove this once TypeScript recognizes the existence of
// Array.prototype.findLastIndex
export function findLastIndex<T>(
array: T[],
predicate: (item: T) => boolean
): number | null {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i;
}
return null;
}
/**
* Counts the number of elements in an array that satsify the given predicate.
*/
export const count = <T>(array: T[], predicate: (item: T) => boolean): number =>
array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0);

View File

@@ -25,7 +25,7 @@ import { Button } from "../button";
import styles from "./LoginPage.module.css";
import { useInteractiveLogin } from "./useInteractiveLogin";
import { usePageTitle } from "../usePageTitle";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { Config } from "../config/Config";
export const LoginPage: FC = () => {
@@ -88,6 +88,7 @@ export const LoginPage: FC = () => {
autoCapitalize="none"
prefix="@"
suffix={`:${Config.defaultServerName()}`}
data-testid="login_username"
/>
</FieldRow>
<FieldRow>
@@ -96,6 +97,7 @@ export const LoginPage: FC = () => {
ref={passwordRef}
placeholder={t("Password")}
label={t("Password")}
data-testid="login_password"
/>
</FieldRow>
{error && (
@@ -104,7 +106,11 @@ export const LoginPage: FC = () => {
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={loading}>
<Button
type="submit"
disabled={loading}
data-testid="login_login"
>
{loading ? t("Logging in…") : t("Login")}
</Button>
</FieldRow>

View File

@@ -38,7 +38,7 @@ import { LoadingView } from "../FullScreenView";
import { useRecaptcha } from "./useRecaptcha";
import { Caption, Link } from "../typography/Typography";
import { usePageTitle } from "../usePageTitle";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { Config } from "../config/Config";
export const RegisterPage: FC = () => {
@@ -166,6 +166,7 @@ export const RegisterPage: FC = () => {
autoCapitalize="none"
prefix="@"
suffix={`:${Config.defaultServerName()}`}
data-testid="register_username"
/>
</FieldRow>
<FieldRow>
@@ -179,6 +180,7 @@ export const RegisterPage: FC = () => {
value={password}
placeholder={t("Password")}
label={t("Password")}
data-testid="register_password"
/>
</FieldRow>
<FieldRow>
@@ -193,6 +195,7 @@ export const RegisterPage: FC = () => {
placeholder={t("Confirm password")}
label={t("Confirm password")}
ref={confirmPasswordRef}
data-testid="register_confirm_password"
/>
</FieldRow>
<Caption>
@@ -217,7 +220,11 @@ export const RegisterPage: FC = () => {
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={registering}>
<Button
type="submit"
disabled={registering}
data-testid="register_register"
>
{registering ? t("Registering…") : t("Register")}
</Button>
</FieldRow>

View File

@@ -39,10 +39,10 @@ limitations under the License.
.secondaryHangup,
.button,
.copyButton {
padding: 7px 15px;
padding: 8px 20px;
border-radius: 8px;
font-size: var(--font-size-body);
font-weight: 700;
font-weight: 600;
}
.button {

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.
@@ -238,7 +238,7 @@ export function SettingsButton({
return (
<TooltipTrigger tooltip={tooltip}>
<Button variant="toolbar" {...rest}>
<SettingsIcon />
<SettingsIcon width={20} height={20} />
</Button>
</TooltipTrigger>
);
@@ -246,9 +246,11 @@ export function SettingsButton({
export function InviteButton({
className,
variant = "toolbar",
...rest
}: {
className?: string;
variant?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
@@ -257,7 +259,7 @@ export function InviteButton({
return (
<TooltipTrigger tooltip={tooltip}>
<Button variant="toolbar" {...rest}>
<Button variant={variant} {...rest}>
<AddUserIcon />
</Button>
</TooltipTrigger>

View File

@@ -36,6 +36,14 @@ export interface ConfigOptions {
submit_url: string;
};
/**
* Sets the URL to send opentelemetry data to. If unset, opentelemetry will
* be disabled.
*/
opentelemetry?: {
collector_url: string;
};
// Describes the default homeserver to use. The same format as Element Web
// (without identity servers as we don't use them).
default_server_config?: {
@@ -44,6 +52,14 @@ export interface ConfigOptions {
server_name: string;
};
};
/**
* Allow to join a group calls without audio and video.
* TEMPORARY: Is a feature that's not proved and experimental
*/
features?: {
feature_group_calls_without_video_and_audio: boolean;
};
}
// Overrides members from ConfigOptions that are always provided by the

View File

@@ -15,14 +15,14 @@ limitations under the License.
*/
import classNames from "classnames";
import React, { FormEventHandler, forwardRef } from "react";
import React, { FormEventHandler, forwardRef, ReactNode } from "react";
import styles from "./Form.module.css";
interface FormProps {
className: string;
onSubmit: FormEventHandler<HTMLFormElement>;
children: JSX.Element[];
children: ReactNode[];
}
export const Form = forwardRef<HTMLFormElement, FormProps>(

View File

@@ -43,7 +43,9 @@ export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
<p>{t("This call already exists, would you like to join?")}</p>
<FieldRow rightAlign className={styles.buttons}>
<Button onPress={onClose}>{t("No")}</Button>
<Button onPress={onJoin}>{t("Yes, join call")}</Button>
<Button onPress={onJoin} data-testid="home_joinExistingRoom">
{t("Yes, join call")}
</Button>
</FieldRow>
</ModalContent>
</Modal>

View File

@@ -37,3 +37,7 @@ limitations under the License.
.recentCallsTitle {
margin-bottom: 32px;
}
.notice {
color: var(--secondary-content);
}

View File

@@ -24,7 +24,11 @@ import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
import {
createRoom,
roomAliasLocalpartFromRoomName,
sanitiseRoomNameInput,
} from "../matrix-utils";
import { useGroupCallRooms } from "./useGroupCallRooms";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import commonStyles from "./common.module.css";
@@ -35,9 +39,11 @@ import { CallList } from "./CallList";
import { UserMenuContainer } from "../UserMenuContainer";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Title } from "../typography/Typography";
import { Caption, Title } from "../typography/Typography";
import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import { useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
interface Props {
client: MatrixClient;
@@ -48,6 +54,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
const [callType, setCallType] = useState(CallType.Video);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics();
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
@@ -57,7 +64,10 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
const roomNameData = data.get("callName");
const roomName = typeof roomNameData === "string" ? roomNameData : "";
const roomName =
typeof roomNameData === "string"
? sanitiseRoomNameInput(roomNameData)
: "";
const ptt = callType === CallType.Radio;
async function submit() {
@@ -123,6 +133,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
type="text"
required
autoComplete="off"
data-testid="home_callName"
/>
<Button
@@ -130,10 +141,16 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
size="lg"
className={styles.button}
disabled={loading}
data-testid="home_go"
>
{loading ? t("Loading…") : t("Go")}
</Button>
</FieldRow>
{optInAnalytics === null && (
<Caption className={styles.notice}>
<AnalyticsNotice />
</Caption>
)}
{error && (
<FieldRow className={styles.fieldRow}>
<ErrorMessage error={error} />

View File

@@ -45,3 +45,7 @@ limitations under the License.
display: none;
}
}
.notice {
color: var(--secondary-content);
}

View File

@@ -24,7 +24,11 @@ import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { UserMenuContainer } from "../UserMenuContainer";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button";
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
import {
createRoom,
roomAliasLocalpartFromRoomName,
sanitiseRoomNameInput,
} from "../matrix-utils";
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
@@ -35,12 +39,15 @@ import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
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";
export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
const [callType, setCallType] = useState(CallType.Video);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics();
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
@@ -54,7 +61,7 @@ export const UnauthenticatedView: FC = () => {
(e) => {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
const roomName = data.get("callName") as string;
const roomName = sanitiseRoomNameInput(data.get("callName") as string);
const displayName = data.get("displayName") as string;
const ptt = callType === CallType.Radio;
@@ -135,6 +142,7 @@ export const UnauthenticatedView: FC = () => {
type="text"
required
autoComplete="off"
data-testid="home_callName"
/>
</FieldRow>
<FieldRow>
@@ -145,10 +153,16 @@ export const UnauthenticatedView: FC = () => {
placeholder={t("Display name")}
type="text"
required
data-testid="home_displayName"
autoComplete="off"
/>
</FieldRow>
<Caption>
{optInAnalytics === null && (
<Caption className={styles.notice}>
<AnalyticsNotice />
</Caption>
)}
<Caption className={styles.notice}>
<Trans>
By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
@@ -159,7 +173,12 @@ export const UnauthenticatedView: FC = () => {
<ErrorMessage error={error} />
</FieldRow>
)}
<Button type="submit" size="lg" disabled={loading}>
<Button
type="submit"
size="lg"
disabled={loading}
data-testid="home_go"
>
{loading ? t("Loading…") : t("Go")}
</Button>
<div id={recaptchaId} />
@@ -167,14 +186,14 @@ export const UnauthenticatedView: FC = () => {
</main>
<footer className={styles.footer}>
<Body className={styles.mobileLoginLink}>
<Link color="primary" to="/login">
<Link color="primary" to="/login" data-testid="home_login">
{t("Login to your account")}
</Link>
</Body>
<Body>
<Trans>
Not registered yet?{" "}
<Link color="primary" to="/register">
<Link color="primary" to="/register" data-testid="home_register">
Create an account
</Link>
</Trans>

View File

@@ -0,0 +1,3 @@
<svg width="28" height="26" viewBox="0 0 28 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 21.0267L22.24 26.0001L20.0533 16.6267L27.3333 10.3201L17.7466 9.50675L14 0.666748L10.2533 9.50675L0.666626 10.3201L7.94663 16.6267L5.75996 26.0001L14 21.0267Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1,4 @@
<svg width="28" height="26" viewBox="0 0 28 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M14 7.50675L15.2933 10.5601L15.92 12.0401L17.52 12.1734L20.8133 12.4534L18.3066 14.6267L17.0933 15.6801L17.4533 17.2534L18.2 20.4667L15.3733 18.7601L14 17.9067L12.6266 18.7334L9.79996 20.4401L10.5466 17.2267L10.9066 15.6534L9.69329 14.6001L7.18663 12.4267L10.48 12.1467L12.08 12.0134L12.7066 10.5334L14 7.50675M14 0.666748L10.2533 9.50675L0.666626 10.3201L7.94663 16.6267L5.75996 26.0001L14 21.0267L22.24 26.0001L20.0533 16.6267L27.3333 10.3201L17.7466 9.50675L14 0.666748Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>

Before

Width:  |  Height:  |  Size: 892 B

After

Width:  |  Height:  |  Size: 922 B

View File

@@ -180,10 +180,16 @@ h2 {
/* Subtitle */
h3 {
font-weight: 400;
font-weight: 600;
font-size: var(--font-size-subtitle);
}
/* Body Semi Bold */
h4 {
font-weight: 600;
font-size: var(--font-size-body);
}
h1,
h2,
h3 {

View File

@@ -23,6 +23,7 @@ import * as Sentry from "@sentry/react";
import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
import { ElementCallOpenTelemetry } from "./otel/otel";
enum LoadState {
None,
@@ -35,6 +36,7 @@ class DependencyLoadStates {
// olm: LoadState = LoadState.None;
config: LoadState = LoadState.None;
sentry: LoadState = LoadState.None;
openTelemetry: LoadState = LoadState.None;
allDepsAreLoaded() {
return !Object.values(this).some((s) => s !== LoadState.Loaded);
@@ -43,6 +45,12 @@ class DependencyLoadStates {
export class Initializer {
private static internalInstance: Initializer;
private isInitialized = false;
public static isInitialized(): boolean {
return Initializer.internalInstance?.isInitialized;
}
public static initBeforeReact() {
// this maybe also needs to return a promise in the future,
// if we have to do async inits before showing the loading screen
@@ -209,10 +217,20 @@ export class Initializer {
this.loadStates.sentry = LoadState.Loaded;
}
// OpenTelemetry (also only after config loaded)
if (
this.loadStates.openTelemetry === LoadState.None &&
this.loadStates.config === LoadState.Loaded
) {
ElementCallOpenTelemetry.globalInit();
this.loadStates.openTelemetry = LoadState.Loaded;
}
if (this.loadStates.allDepsAreLoaded()) {
// resolve if there is no dependency that is not loaded
resolve();
this.isInitialized = true;
}
}
private initPromise: Promise<void>;
private initPromise: Promise<void> | null;
}

View File

@@ -54,4 +54,6 @@ limitations under the License.
.removeButton {
color: var(--accent);
font-size: var(--font-size-caption);
padding: 6px 0;
}

View File

@@ -0,0 +1,23 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.feedback textarea {
height: 75px;
border-radius: 8px;
}
.feedback {
border-radius: 8px;
}

View File

@@ -209,3 +209,7 @@ limitations under the License.
margin-left: 26px;
width: 100%; /* Ensure that it breaks onto the next row */
}
.description.noLabel {
margin-top: -20px; /* Ensures that there is no weired spacing if the checkbox doesn't have a label */
}

View File

@@ -55,14 +55,14 @@ export function Field({ children, className }: FieldProps): JSX.Element {
}
interface InputFieldProps {
label: string;
label?: string;
type: string;
prefix?: string;
suffix?: string;
id?: string;
checked?: boolean;
className?: string;
description?: string;
description?: string | ReactNode;
disabled?: boolean;
required?: boolean;
// this is a hack. Those variables should be part of `HTMLAttributes<HTMLInputElement> | HTMLAttributes<HTMLTextAreaElement>`
@@ -72,6 +72,7 @@ interface InputFieldProps {
autoCorrect?: string;
autoCapitalize?: string;
value?: string;
defaultValue?: string;
placeholder?: string;
defaultChecked?: boolean;
onChange?: (event: ChangeEvent) => void;
@@ -140,7 +141,14 @@ export const InputField = forwardRef<
</label>
{suffix && <span>{suffix}</span>}
{description && (
<p id={descriptionId} className={styles.description}>
<p
id={descriptionId}
className={
label
? styles.description
: classNames(styles.description, styles.noLabel)
}
>
{description}
</p>
)}

View File

@@ -22,8 +22,6 @@ limitations under the License.
}
.label {
font-weight: 600;
font-size: var(--font-size-subtitle);
margin-top: 0;
margin-bottom: 12px;
}

View File

@@ -0,0 +1,41 @@
/*
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.
*/
.starIcon {
cursor: pointer;
}
.starRating {
display: flex;
justify-content: center;
flex: 1;
}
.inputContainer {
display: inline-block;
}
.hideElement {
border: 0;
clip-path: content-box;
height: 0px;
width: 0px;
margin: -1px;
overflow: hidden;
padding: 0;
width: 1px;
display: inline-block;
}

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 React, { useState } from "react";
import { useTranslation } from "react-i18next";
import styles from "./StarRatingInput.module.css";
import { ReactComponent as StarSelected } from "../icons/StarSelected.svg";
import { ReactComponent as StarUnselected } from "../icons/StarUnselected.svg";
interface Props {
starCount: number;
onChange: (stars: number) => void;
required?: boolean;
}
export function StarRatingInput({
starCount,
onChange,
required,
}: Props): JSX.Element {
const [rating, setRating] = useState(0);
const [hover, setHover] = useState(0);
const { t } = useTranslation();
return (
<div className={styles.starRating}>
{[...Array(starCount)].map((_star, index) => {
index += 1;
return (
<div
className={styles.inputContainer}
onMouseEnter={() => setHover(index)}
onMouseLeave={() => setHover(rating)}
key={index}
>
<input
className={styles.hideElement}
type="radio"
id={"starInput" + String(index)}
value={String(index) + "Star"}
name="star rating"
onChange={(_ev) => {
setRating(index);
onChange(index);
}}
required
/>
<label
className={styles.hideElement}
id={"starInvisibleLabel" + String(index)}
htmlFor={"starInput" + String(index)}
>
{t("{{count}} stars", {
count: index,
})}
</label>
<label
className={styles.starIcon}
id={"starIcon" + String(index)}
htmlFor={"starInput" + String(index)}
>
{index <= (hover || rating) ? (
<StarSelected />
) : (
<StarUnselected />
)}
</label>
</div>
);
})}
</div>
);
}

View File

@@ -61,11 +61,11 @@ function waitForSync(client: MatrixClient) {
data: ISyncStateData
) => {
if (state === "PREPARED") {
client.removeListener(ClientEvent.Sync, onSync);
resolve();
client.removeListener(ClientEvent.Sync, onSync);
} else if (state === "ERROR") {
reject(data?.error);
client.removeListener(ClientEvent.Sync, onSync);
reject(data?.error);
}
};
client.on(ClientEvent.Sync, onSync);
@@ -92,17 +92,26 @@ export async function initClient(
indexedDB = window.indexedDB;
} catch (e) {}
const storeOpts = {} as ICreateClientOpts;
const baseOpts = {
fallbackICEServerAllowed: fallbackICEServerAllowed,
isVoipWithNoMediaAllowed:
Config.get().features?.feature_group_calls_without_video_and_audio,
} as ICreateClientOpts;
if (indexedDB && localStorage && !import.meta.env.DEV) {
storeOpts.store = new IndexedDBStore({
if (indexedDB && localStorage) {
baseOpts.store = new IndexedDBStore({
indexedDB: window.indexedDB,
localStorage,
dbName: SYNC_STORE_NAME,
workerFactory: () => new IndexedDBWorker(),
// We can't use the worker in dev mode because Vite simply doesn't bundle workers
// in dev mode: it expects them to use native modules. Ours don't, and even then only
// Chrome supports it. (It bundles them fine in production mode.)
workerFactory: import.meta.env.DEV
? undefined
: () => new IndexedDBWorker(),
});
} else if (localStorage) {
storeOpts.store = new MemoryStore({ localStorage });
baseOpts.store = new MemoryStore({ localStorage });
}
// Check whether we have crypto data store. If we are restoring a session
@@ -134,14 +143,14 @@ export async function initClient(
}
if (indexedDB) {
storeOpts.cryptoStore = new IndexedDBCryptoStore(
baseOpts.cryptoStore = new IndexedDBCryptoStore(
indexedDB,
CRYPTO_STORE_NAME
);
} else if (localStorage) {
storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
} else {
storeOpts.cryptoStore = new MemoryCryptoStore();
baseOpts.cryptoStore = new MemoryCryptoStore();
}
// XXX: we read from the URL params in RoomPage too:
@@ -155,7 +164,7 @@ export async function initClient(
}
const client = createClient({
...storeOpts,
...baseOpts,
...clientOptions,
useAuthorizationHeader: true,
// Use a relatively low timeout for API calls: this is a realtime app
@@ -206,6 +215,37 @@ export function fullAliasFromRoomName(
return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`;
}
/**
* Applies some basic sanitisation to a room name that the user
* has given us
* @param input The room name from the user
* @param client A matrix client object
*/
export function sanitiseRoomNameInput(input: string): string {
// check to see if the user has enetered a fully qualified room
// alias. If so, turn it into just the localpart because that's what
// we use
const parts = input.split(":", 2);
if (parts.length === 2 && parts[0][0] === "#") {
// looks like a room alias
if (parts[1] === Config.defaultServerName()) {
// it's local to our own homeserver
return parts[0];
} else {
throw new Error("Unsupported remote room alias");
}
}
// that's all we do here right now
return input;
}
/**
* XXX: What is this trying to do? It looks like it's getting the localpart from
* a room alias, but why is it splitting on hyphens and then putting spaces in??
* @param roomId
* @returns
*/
export function roomNameFromRoomId(roomId: string): string {
return roomId
.match(/([^:]+):.*$/)[1]

View File

@@ -47,7 +47,7 @@ export async function findDeviceByName(
*
* @return The available media devices
*/
export async function getDevices(): Promise<MediaDeviceInfo[]> {
export async function getNamedDevices(): Promise<MediaDeviceInfo[]> {
// First get the devices without their labels, to learn what kinds of streams
// we can request
let devices: MediaDeviceInfo[];

197
src/otel/OTelCall.ts Normal file
View File

@@ -0,0 +1,197 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Span } from "@opentelemetry/api";
import { MatrixCall } from "matrix-js-sdk";
import { CallEvent } from "matrix-js-sdk/src/webrtc/call";
import {
TransceiverStats,
CallFeedStats,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { ObjectFlattener } from "./ObjectFlattener";
import { ElementCallOpenTelemetry } from "./otel";
import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan";
import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan";
type StreamId = string;
type MID = string;
/**
* Tracks an individual call within a group call, either to a full-mesh peer or a focus
*/
export class OTelCall {
private readonly trackFeedSpan = new Map<
StreamId,
OTelCallAbstractMediaStreamSpan
>();
private readonly trackTransceiverSpan = new Map<
MID,
OTelCallAbstractMediaStreamSpan
>();
constructor(
public userId: string,
public deviceId: string,
public call: MatrixCall,
public span: Span
) {
if (call.peerConn) {
this.addCallPeerConnListeners();
} else {
this.call.once(
CallEvent.PeerConnectionCreated,
this.addCallPeerConnListeners
);
}
}
public dispose() {
this.call.peerConn.removeEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged
);
this.call.peerConn.removeEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged
);
this.call.peerConn.removeEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged
);
this.call.peerConn.removeEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged
);
this.call.peerConn.removeEventListener(
"icecandidateerror",
this.onIceCandidateError
);
}
private addCallPeerConnListeners = (): void => {
this.call.peerConn.addEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged
);
this.call.peerConn.addEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged
);
this.call.peerConn.addEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged
);
this.call.peerConn.addEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged
);
this.call.peerConn.addEventListener(
"icecandidateerror",
this.onIceCandidateError
);
};
public onCallConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.callConnectionStateChange", {
callConnectionState: this.call.peerConn.connectionState,
});
};
public onCallSignalingStateChanged = (): void => {
this.span.addEvent("matrix.call.callSignalingStateChange", {
callSignalingState: this.call.peerConn.signalingState,
});
};
public onIceConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.iceConnectionStateChange", {
iceConnectionState: this.call.peerConn.iceConnectionState,
});
};
public onIceGatheringStateChanged = (): void => {
this.span.addEvent("matrix.call.iceGatheringStateChange", {
iceGatheringState: this.call.peerConn.iceGatheringState,
});
};
public onIceCandidateError = (ev: Event): void => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(ev, flatObject, "error.", 0);
this.span.addEvent("matrix.call.iceCandidateError", flatObject);
};
public onCallFeedStats(callFeeds: CallFeedStats[]): void {
let prvFeeds: StreamId[] = [...this.trackFeedSpan.keys()];
callFeeds.forEach((feed) => {
if (!this.trackFeedSpan.has(feed.stream)) {
this.trackFeedSpan.set(
feed.stream,
new OTelCallFeedMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
feed
)
);
}
this.trackFeedSpan.get(feed.stream)?.update(feed);
prvFeeds = prvFeeds.filter((prvStreamId) => prvStreamId !== feed.stream);
});
prvFeeds.forEach((prvStreamId) => {
this.trackFeedSpan.get(prvStreamId)?.end();
this.trackFeedSpan.delete(prvStreamId);
});
}
public onTransceiverStats(transceiverStats: TransceiverStats[]): void {
let prvTransSpan: MID[] = [...this.trackTransceiverSpan.keys()];
transceiverStats.forEach((transStats) => {
if (!this.trackTransceiverSpan.has(transStats.mid)) {
this.trackTransceiverSpan.set(
transStats.mid,
new OTelCallTransceiverMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
transStats
)
);
}
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
prvTransSpan = prvTransSpan.filter(
(prvStreamId) => prvStreamId !== transStats.mid
);
});
prvTransSpan.forEach((prvMID) => {
this.trackTransceiverSpan.get(prvMID)?.end();
this.trackTransceiverSpan.delete(prvMID);
});
}
public end(): void {
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
this.trackTransceiverSpan.forEach((transceiverSpan) =>
transceiverSpan.end()
);
this.span.end();
}
}

View File

@@ -0,0 +1,62 @@
import opentelemetry, { Span } from "@opentelemetry/api";
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { ElementCallOpenTelemetry } from "./otel";
import { OTelCallMediaStreamTrackSpan } from "./OTelCallMediaStreamTrackSpan";
type TrackId = string;
export abstract class OTelCallAbstractMediaStreamSpan {
protected readonly trackSpans = new Map<
TrackId,
OTelCallMediaStreamTrackSpan
>();
public readonly span;
public constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
protected readonly type: string
) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
callSpan
);
const options = {
links: [
{
context: callSpan.spanContext(),
},
],
};
this.span = oTel.tracer.startSpan(this.type, options, ctx);
}
protected upsertTrackSpans(tracks: TrackStats[]) {
let prvTracks: TrackId[] = [...this.trackSpans.keys()];
tracks.forEach((t) => {
if (!this.trackSpans.has(t.id)) {
this.trackSpans.set(
t.id,
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t)
);
}
this.trackSpans.get(t.id)?.update(t);
prvTracks = prvTracks.filter((prvTrackId) => prvTrackId !== t.id);
});
prvTracks.forEach((prvTrackId) => {
this.trackSpans.get(prvTrackId)?.end();
this.trackSpans.delete(prvTrackId);
});
}
public abstract update(data: Object): void;
public end(): void {
this.trackSpans.forEach((tSpan) => {
tSpan.end();
});
this.span.end();
}
}

View File

@@ -0,0 +1,57 @@
import { Span } from "@opentelemetry/api";
import {
CallFeedStats,
TrackStats,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { ElementCallOpenTelemetry } from "./otel";
import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
callFeed: CallFeedStats
) {
const postFix =
callFeed.type === "local" && callFeed.prefix === "from-call-feed"
? "(clone)"
: "";
super(oTel, callSpan, `matrix.call.feed.${callFeed.type}${postFix}`);
this.span.setAttribute("feed.streamId", callFeed.stream);
this.span.setAttribute("feed.type", callFeed.type);
this.span.setAttribute("feed.readFrom", callFeed.prefix);
this.span.setAttribute("feed.purpose", callFeed.purpose);
this.prev = {
isAudioMuted: callFeed.isAudioMuted,
isVideoMuted: callFeed.isVideoMuted,
};
this.span.addEvent("matrix.call.feed.initState", this.prev);
}
public update(callFeed: CallFeedStats): void {
if (this.prev.isAudioMuted !== callFeed.isAudioMuted) {
this.span.addEvent("matrix.call.feed.audioMuted", {
isAudioMuted: callFeed.isAudioMuted,
});
this.prev.isAudioMuted = callFeed.isAudioMuted;
}
if (this.prev.isVideoMuted !== callFeed.isVideoMuted) {
this.span.addEvent("matrix.call.feed.isVideoMuted", {
isVideoMuted: callFeed.isVideoMuted,
});
this.prev.isVideoMuted = callFeed.isVideoMuted;
}
const trackStats: TrackStats[] = [];
if (callFeed.video) {
trackStats.push(callFeed.video);
}
if (callFeed.audio) {
trackStats.push(callFeed.audio);
}
this.upsertTrackSpans(trackStats);
}
}

View File

@@ -0,0 +1,62 @@
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
import opentelemetry, { Span } from "@opentelemetry/api";
import { ElementCallOpenTelemetry } from "./otel";
export class OTelCallMediaStreamTrackSpan {
private readonly span: Span;
private prev: TrackStats;
public constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly streamSpan: Span,
data: TrackStats
) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
streamSpan
);
const options = {
links: [
{
context: streamSpan.spanContext(),
},
],
};
const type = `matrix.call.track.${data.label}.${data.kind}`;
this.span = oTel.tracer.startSpan(type, options, ctx);
this.span.setAttribute("track.trackId", data.id);
this.span.setAttribute("track.kind", data.kind);
this.span.setAttribute("track.constrainDeviceId", data.constrainDeviceId);
this.span.setAttribute("track.settingDeviceId", data.settingDeviceId);
this.span.setAttribute("track.label", data.label);
this.span.addEvent("matrix.call.track.initState", {
readyState: data.readyState,
muted: data.muted,
enabled: data.enabled,
});
this.prev = data;
}
public update(data: TrackStats): void {
if (this.prev.muted !== data.muted) {
this.span.addEvent("matrix.call.track.muted", { muted: data.muted });
}
if (this.prev.enabled !== data.enabled) {
this.span.addEvent("matrix.call.track.enabled", {
enabled: data.enabled,
});
}
if (this.prev.readyState !== data.readyState) {
this.span.addEvent("matrix.call.track.readyState", {
readyState: data.readyState,
});
}
this.prev = data;
}
public end(): void {
this.span.end();
}
}

View File

@@ -0,0 +1,54 @@
import { Span } from "@opentelemetry/api";
import {
TrackStats,
TransceiverStats,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { ElementCallOpenTelemetry } from "./otel";
import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: {
direction: string;
currentDirection: string;
};
constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
stats: TransceiverStats
) {
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);
this.span.setAttribute("transceiver.mid", stats.mid);
this.prev = {
direction: stats.direction,
currentDirection: stats.currentDirection,
};
this.span.addEvent("matrix.call.transceiver.initState", this.prev);
}
public update(stats: TransceiverStats): void {
if (this.prev.currentDirection !== stats.currentDirection) {
this.span.addEvent("matrix.call.transceiver.currentDirection", {
currentDirection: stats.currentDirection,
});
this.prev.currentDirection = stats.currentDirection;
}
if (this.prev.direction !== stats.direction) {
this.span.addEvent("matrix.call.transceiver.direction", {
direction: stats.direction,
});
this.prev.direction = stats.direction;
}
const trackStats: TrackStats[] = [];
if (stats.sender) {
trackStats.push(stats.sender);
}
if (stats.receiver) {
trackStats.push(stats.receiver);
}
this.upsertTrackSpans(trackStats);
}
}

View File

@@ -0,0 +1,474 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api";
import {
GroupCall,
MatrixClient,
MatrixEvent,
RoomMember,
} from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/src/logger";
import {
CallError,
CallState,
MatrixCall,
VoipEvent,
} from "matrix-js-sdk/src/webrtc/call";
import {
CallsByUserAndDevice,
GroupCallError,
GroupCallEvent,
GroupCallStatsReport,
} from "matrix-js-sdk/src/webrtc/groupCall";
import {
ConnectionStatsReport,
ByteSentStatsReport,
SummaryStatsReport,
CallFeedReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils";
import { ElementCallOpenTelemetry } from "./otel";
import { ObjectFlattener } from "./ObjectFlattener";
import { OTelCall } from "./OTelCall";
/**
* Represent the span of time which we intend to be joined to a group call
*/
export class OTelGroupCallMembership {
private callMembershipSpan?: Span;
private groupCallContext?: Context;
private myUserId = "unknown";
private myDeviceId: string;
private myMember?: RoomMember;
private callsByCallId = new Map<string, OTelCall>();
private statsReportSpan: {
span: Span | undefined;
stats: OTelStatsReportEvent[];
};
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
constructor(private groupCall: GroupCall, client: MatrixClient) {
const clientId = client.getUserId();
if (clientId) {
this.myUserId = clientId;
const myMember = groupCall.room.getMember(clientId);
if (myMember) {
this.myMember = myMember;
}
}
this.myDeviceId = client.getDeviceId() || "unknown";
this.statsReportSpan = { span: undefined, stats: [] };
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
}
dispose() {
this.groupCall.removeListener(
GroupCallEvent.CallsChanged,
this.onCallsChanged
);
}
public onJoinCall() {
if (!ElementCallOpenTelemetry.instance) return;
if (this.callMembershipSpan !== undefined) {
logger.warn("Call membership span is already started");
return;
}
// Create the main span that tracks the time we intend to be in the call
this.callMembershipSpan =
ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.groupCallMembership"
);
this.callMembershipSpan.setAttribute(
"matrix.confId",
this.groupCall.groupCallId
);
this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId);
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
this.callMembershipSpan.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name"
);
this.groupCallContext = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
this.callMembershipSpan
);
this.callMembershipSpan?.addEvent("matrix.joinCall");
}
public onLeaveCall() {
if (this.callMembershipSpan === undefined) {
logger.warn("Call membership span is already ended");
return;
}
this.callMembershipSpan.addEvent("matrix.leaveCall");
// and end the span to indicate we've left
this.callMembershipSpan.end();
this.callMembershipSpan = undefined;
this.groupCallContext = undefined;
}
public onUpdateRoomState(event: MatrixEvent) {
if (
!event ||
(!event.getType().startsWith("m.call") &&
!event.getType().startsWith("org.matrix.msc3401.call"))
) {
return;
}
this.callMembershipSpan?.addEvent(
`matrix.roomStateEvent_${event.getType()}`,
ObjectFlattener.flattenVoipEvent(event.getContent())
);
}
public onCallsChanged = (calls: CallsByUserAndDevice) => {
for (const [userId, userCalls] of calls.entries()) {
for (const [deviceId, call] of userCalls.entries()) {
if (!this.callsByCallId.has(call.callId)) {
if (ElementCallOpenTelemetry.instance) {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
`matrix.call`,
undefined,
this.groupCallContext
);
// XXX: anonymity
span.setAttribute("matrix.call.target.userId", userId);
span.setAttribute("matrix.call.target.deviceId", deviceId);
const displayName =
this.groupCall.room.getMember(userId)?.name ?? "unknown";
span.setAttribute("matrix.call.target.displayName", displayName);
this.callsByCallId.set(
call.callId,
new OTelCall(userId, deviceId, call, span)
);
}
}
}
}
for (const callTrackingInfo of this.callsByCallId.values()) {
const userCalls = calls.get(callTrackingInfo.userId);
if (
!userCalls ||
!userCalls.has(callTrackingInfo.deviceId) ||
userCalls.get(callTrackingInfo.deviceId).callId !==
callTrackingInfo.call.callId
) {
callTrackingInfo.end();
this.callsByCallId.delete(callTrackingInfo.call.callId);
}
}
};
public onCallStateChange(call: MatrixCall, newState: CallState) {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got call state change for unknown call ID ${call.callId}`);
return;
}
callTrackingInfo.span.addEvent("matrix.call.stateChange", {
state: newState,
});
}
public onSendEvent(call: MatrixCall, event: VoipEvent) {
const eventType = event.eventType as string;
if (
!eventType.startsWith("m.call") &&
!eventType.startsWith("org.matrix.call")
)
return;
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got call send event for unknown call ID ${call.callId}`);
return;
}
if (event.type === "toDevice") {
callTrackingInfo.span.addEvent(
`matrix.sendToDeviceEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event)
);
} else if (event.type === "sendEvent") {
callTrackingInfo.span.addEvent(
`matrix.sendToRoomEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event)
);
}
}
public onReceivedVoipEvent(event: MatrixEvent) {
// These come straight from CallEventHandler so don't have
// a call already associated (in principle we could receive
// events for calls we don't know about).
const callId = event.getContent().call_id;
if (!callId) {
this.callMembershipSpan?.addEvent("matrix.receive_voip_event_no_callid", {
"sender.userId": event.getSender(),
});
logger.error("Received call event with no call ID!");
return;
}
const call = this.callsByCallId.get(callId);
if (!call) {
this.callMembershipSpan?.addEvent(
"matrix.receive_voip_event_unknown_callid",
{
"sender.userId": event.getSender(),
}
);
logger.error("Received call event for unknown call ID " + callId);
return;
}
call.span.addEvent("matrix.receive_voip_event", {
"sender.userId": event.getSender(),
...ObjectFlattener.flattenVoipEvent(event.getContent()),
});
}
public onToggleMicrophoneMuted(newValue: boolean) {
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
"matrix.microphone.muted": newValue,
});
}
public onSetMicrophoneMuted(setMuted: boolean) {
this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
"matrix.microphone.muted": setMuted,
});
}
public onToggleLocalVideoMuted(newValue: boolean) {
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
"matrix.video.muted": newValue,
});
}
public onSetLocalVideoMuted(setMuted: boolean) {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.video.muted": setMuted,
});
}
public onToggleScreensharing(newValue: boolean) {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.screensharing.enabled": newValue,
});
}
public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) {
if (speaking) {
// Ensure that there's an audio activity span for this speaker
let deviceMap = this.speakingSpans.get(member);
if (deviceMap === undefined) {
deviceMap = new Map();
this.speakingSpans.set(member, deviceMap);
}
if (!deviceMap.has(deviceId)) {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.audioActivity",
undefined,
this.groupCallContext
);
span.setAttribute("matrix.userId", member.userId);
span.setAttribute("matrix.displayName", member.rawDisplayName);
deviceMap.set(deviceId, span);
}
} else {
// End the audio activity span for this speaker, if any
const deviceMap = this.speakingSpans.get(member);
deviceMap?.get(deviceId)?.end();
deviceMap?.delete(deviceId);
if (deviceMap?.size === 0) this.speakingSpans.delete(member);
}
}
public onCallError(error: CallError, call: MatrixCall) {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got error for unknown call ID ${call.callId}`);
return;
}
callTrackingInfo.span.recordException(error);
}
public onGroupCallError(error: GroupCallError) {
this.callMembershipSpan?.recordException(error);
}
public onUndecryptableToDevice(event: MatrixEvent) {
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
"sender.userId": event.getSender(),
});
}
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report.report?.callId;
if (callId) {
call = this.callsByCallId.get(callId);
}
if (!call) {
this.callMembershipSpan?.addEvent(
OTelStatsReportType.CallFeedReport + "_unknown_callId",
{
"call.callId": callId,
"call.opponentMemberId": report.report?.opponentMemberId
? report.report?.opponentMemberId
: "unknown",
}
);
logger.error(
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`
);
return;
} else {
call.onCallFeedStats(report.report.callFeeds);
call.onTransceiverStats(report.report.transceiver);
}
}
public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport>
) {
this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport,
statsReport.report
);
}
public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport>
) {
this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport,
statsReport.report
);
}
public buildCallStatsSpan(
type: OTelStatsReportType,
report: ByteSentStatsReport | ConnectionStatsReport
): void {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report?.callId;
if (callId) {
call = this.callsByCallId.get(callId);
}
if (!call) {
this.callMembershipSpan?.addEvent(type + "_unknown_callid", {
"call.callId": callId,
"call.opponentMemberId": report.opponentMemberId
? report.opponentMemberId
: "unknown",
});
logger.error(`Received ${type} with unknown call ID: ${callId}`);
return;
}
const data = ObjectFlattener.flattenReportObject(type, report);
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
call.span
);
const options = {
links: [
{
context: call.span.spanContext(),
},
],
};
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
type,
options,
ctx
);
span.setAttribute("matrix.callId", callId);
span.setAttribute(
"matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown"
);
span.addEvent("matrix.call.connection_stats_event", data);
span.end();
}
public onSummaryStatsReport(
statsReport: GroupCallStatsReport<SummaryStatsReport>
) {
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.SummaryReport;
const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport);
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
const ctx = setSpan(
opentelemetry.context.active(),
this.callMembershipSpan
);
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
"matrix.groupCallMembership.summaryReport",
undefined,
ctx
);
if (span === undefined) {
return;
}
span.setAttribute("matrix.confId", this.groupCall.groupCallId);
span.setAttribute("matrix.userId", this.myUserId);
span.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name"
);
span.addEvent(type, data);
span.end();
}
}
}
interface OTelStatsReportEvent {
type: OTelStatsReportType;
data: Attributes;
}
enum OTelStatsReportType {
ConnectionReport = "matrix.call.stats.connection",
ByteSentReport = "matrix.call.stats.byteSent",
SummaryReport = "matrix.stats.summary",
CallFeedReport = "matrix.stats.call_feed",
}

109
src/otel/ObjectFlattener.ts Normal file
View File

@@ -0,0 +1,109 @@
/*
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 { Attributes } from "@opentelemetry/api";
import { VoipEvent } from "matrix-js-sdk/src/webrtc/call";
import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall";
import {
ByteSentStatsReport,
ConnectionStatsReport,
SummaryStatsReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
export class ObjectFlattener {
public static flattenReportObject(
prefix: string,
report: ConnectionStatsReport | ByteSentStatsReport
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
return flatObject;
}
public static flattenByteSentStatsReportObject(
statsReport: GroupCallStatsReport<ByteSentStatsReport>
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.bytesSent.",
0
);
return flatObject;
}
static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport>
) {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.summary.",
0
);
return flatObject;
}
/* Flattens out an object into a single layer with components
* of the key separated by dots
*/
public static flattenVoipEvent(event: VoipEvent): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
event as unknown as Record<string, unknown>, // XXX Types
flatObject,
"matrix.event.",
0
);
return flatObject;
}
public static flattenObjectRecursive(
obj: Object,
flatObject: Attributes,
prefix: string,
depth: number
): void {
if (depth > 10)
throw new Error(
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
prefix
);
let entries;
if (obj instanceof Map) {
entries = obj.entries();
} else {
entries = Object.entries(obj);
}
for (const [k, v] of entries) {
if (["string", "number", "boolean"].includes(typeof v) || v === null) {
let value;
value = v === null ? "null" : v;
value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value;
flatObject[prefix + k] = value;
} else if (typeof v === "object") {
ObjectFlattener.flattenObjectRecursive(
v,
flatObject,
prefix + k + ".",
depth + 1
);
}
}
}
}

122
src/otel/otel.ts Normal file
View File

@@ -0,0 +1,122 @@
/*
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 { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import opentelemetry, { Tracer } from "@opentelemetry/api";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { logger } from "matrix-js-sdk/src/logger";
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
import { Anonymity } from "../analytics/PosthogAnalytics";
import { Config } from "../config/Config";
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
const SERVICE_NAME = "element-call";
let sharedInstance: ElementCallOpenTelemetry;
export class ElementCallOpenTelemetry {
private _provider: WebTracerProvider;
private _tracer: Tracer;
private _anonymity: Anonymity;
private otlpExporter: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
static globalInit(): void {
const config = Config.get();
// we always enable opentelemetry in general. We only enable the OTLP
// collector if a URL is defined (and in future if another setting is defined)
// Posthog reporting is enabled or disabled
// within the posthog code.
const shouldEnableOtlp = Boolean(config.opentelemetry?.collector_url);
if (!sharedInstance || sharedInstance.isOtlpEnabled !== shouldEnableOtlp) {
logger.info("(Re)starting OpenTelemetry debug reporting");
sharedInstance?.dispose();
sharedInstance = new ElementCallOpenTelemetry(
config.opentelemetry?.collector_url,
config.rageshake?.submit_url
);
}
}
static get instance(): ElementCallOpenTelemetry {
return sharedInstance;
}
constructor(
collectorUrl: string | undefined,
rageshakeUrl: string | undefined
) {
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
const providerConfig = {
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
}),
};
this._provider = new WebTracerProvider(providerConfig);
if (collectorUrl) {
logger.info("Enabling OTLP collector with URL " + collectorUrl);
this.otlpExporter = new OTLPTraceExporter({
url: collectorUrl,
});
this._provider.addSpanProcessor(
new SimpleSpanProcessor(this.otlpExporter)
);
} else {
logger.info("OTLP collector disabled");
}
if (rageshakeUrl) {
this.rageshakeProcessor = new RageshakeSpanProcessor();
this._provider.addSpanProcessor(this.rageshakeProcessor);
}
this._provider.addSpanProcessor(new PosthogSpanProcessor());
opentelemetry.trace.setGlobalTracerProvider(this._provider);
this._tracer = opentelemetry.trace.getTracer(
// This is not the serviceName shown in jaeger
"my-element-call-otl-tracer"
);
}
public dispose(): void {
opentelemetry.trace.setGlobalTracerProvider(null);
this._provider?.shutdown();
}
public get isOtlpEnabled(): boolean {
return Boolean(this.otlpExporter);
}
public get tracer(): Tracer {
return this._tracer;
}
public get provider(): WebTracerProvider {
return this._provider;
}
public get anonymity(): Anonymity {
return this._anonymity;
}
}

View File

@@ -1,141 +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 React, { ChangeEvent, useCallback, useEffect, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { useProfile } from "./useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Modal, ModalContent } from "../Modal";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileModal.module.css";
interface Props {
client: MatrixClient;
onClose: () => void;
[rest: string]: unknown;
}
export function ProfileModal({ client, ...rest }: Props) {
const { onClose } = rest;
const { t } = useTranslation();
const {
success,
error,
loading,
displayName: initialDisplayName,
avatarUrl,
saveProfile,
} = useProfile(client);
const [displayName, setDisplayName] = useState(initialDisplayName || "");
const [removeAvatar, setRemoveAvatar] = useState(false);
const onRemoveAvatar = useCallback(() => {
setRemoveAvatar(true);
}, []);
const onChangeDisplayName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
},
[setDisplayName]
);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const displayNameDataEntry = data.get("displayName");
const avatar: File | string = data.get("avatar");
const avatarSize =
typeof avatar == "string" ? avatar.length : avatar.size;
const displayName =
typeof displayNameDataEntry == "string"
? displayNameDataEntry
: displayNameDataEntry.name;
saveProfile({
displayName,
avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar && (!avatar || avatarSize === 0),
});
},
[saveProfile, removeAvatar]
);
useEffect(() => {
if (success) {
onClose();
}
}, [success, onClose]);
return (
<Modal title={t("Profile")} isDismissable {...rest}>
<ModalContent>
<form onSubmit={onSubmit}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
</FieldRow>
<FieldRow>
<InputField
id="userId"
name="userId"
label={t("User ID")}
type="text"
disabled
value={client.getUserId()}
/>
</FieldRow>
<FieldRow>
<InputField
id="displayName"
name="displayName"
label={t("Display name")}
type="text"
required
autoComplete="off"
placeholder={t("Display name")}
value={displayName}
onChange={onChangeDisplayName}
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow rightAlign>
<Button type="button" variant="secondary" onPress={onClose}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading ? t("Saving…") : t("Save")}
</Button>
</FieldRow>
</form>
</ModalContent>
</Modal>
);
}

View File

@@ -17,20 +17,31 @@ limitations under the License.
.headline {
text-align: center;
margin-bottom: 60px;
white-space: pre;
}
.callEndedContent {
text-align: center;
max-width: 360px;
max-width: 450px;
}
.callEndedContent p {
font-size: var(--font-size-subtitle);
}
.callEndedContent h3 {
margin-bottom: 32px;
}
.callEndedButton {
margin-top: 54px;
margin-left: 30px;
margin-right: 30px !important;
}
.submitButton {
width: 100%;
margin-top: 54px;
margin-left: 30px;
margin-right: 30px !important;
}
.container {

View File

@@ -14,19 +14,129 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { FormEventHandler, useCallback, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import styles from "./CallEndedView.module.css";
import { LinkButton } from "../button";
import feedbackStyle from "../input/FeedbackInput.module.css";
import { Button, LinkButton } from "../button";
import { useProfile } from "../profile/useProfile";
import { Subtitle, Body, Link, Headline } from "../typography/Typography";
import { Body, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { FieldRow, InputField } from "../input/Input";
import { StarRatingInput } from "../input/StarRatingInput";
export function CallEndedView({ client }: { client: MatrixClient }) {
export function CallEndedView({
client,
isPasswordlessUser,
endedCallId,
}: {
client: MatrixClient;
isPasswordlessUser: boolean;
endedCallId: string;
}) {
const { t } = useTranslation();
const history = useHistory();
const { displayName } = useProfile(client);
const [surveySubmitted, setSurverySubmitted] = useState(false);
const [starRating, setStarRating] = useState(0);
const [submitting, setSubmitting] = useState(false);
const [submitDone, setSubmitDone] = useState(false);
const submitSurvery: FormEventHandler<HTMLFormElement> = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
const feedbackText = data.get("feedbackText") as string;
PosthogAnalytics.instance.eventQualitySurvey.track(
endedCallId,
feedbackText,
starRating
);
setSubmitting(true);
setTimeout(() => {
setSubmitDone(true);
setTimeout(() => {
if (isPasswordlessUser) {
// setting this renders the callEndedView with the invitation to create an account
setSurverySubmitted(true);
} else {
// if the user already has an account immediately go back to the home screen
history.push("/");
}
}, 1000);
}, 1000);
},
[endedCallId, history, isPasswordlessUser, starRating]
);
const createAccountDialog = isPasswordlessUser && (
<div className={styles.callEndedContent}>
<Trans>
<p>Why not finish by setting up a password to keep your account?</p>
<p>
You'll be able to keep your name and set an avatar for use on future
calls
</p>
</Trans>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
{t("Create account")}
</LinkButton>
</div>
);
const qualitySurveyDialog = (
<div className={styles.callEndedContent}>
<Trans>
<p>
We'd love to hear your feedback so we can improve your experience.
</p>
</Trans>
<form onSubmit={submitSurvery}>
<FieldRow>
<StarRatingInput starCount={5} onChange={setStarRating} required />
</FieldRow>
<FieldRow>
<InputField
className={feedbackStyle.feedback}
id="feedbackText"
name="feedbackText"
label={t("Your feedback")}
placeholder={t("Your feedback")}
type="textarea"
/>
</FieldRow>{" "}
<FieldRow>
{submitDone ? (
<Trans>
<p>Thanks for your feedback!</p>
</Trans>
) : (
<Button
type="submit"
className={styles.submitButton}
size="lg"
variant="default"
data-testid="home_go"
>
{submitting ? t("Submitting…") : t("Submit")}
</Button>
)}
</FieldRow>
</form>
</div>
);
return (
<>
@@ -39,27 +149,19 @@ export function CallEndedView({ client }: { client: MatrixClient }) {
<div className={styles.container}>
<main className={styles.main}>
<Headline className={styles.headline}>
{t("{{displayName}}, your call is now ended", { displayName })}
{surveySubmitted
? t("{{displayName}}, your call has ended.", {
displayName,
})
: t("{{displayName}}, your call has ended.", {
displayName,
}) +
"\n" +
t("How did it go?")}
</Headline>
<div className={styles.callEndedContent}>
<Trans>
<Subtitle>
Why not finish by setting up a password to keep your account?
</Subtitle>
<Subtitle>
You'll be able to keep your name and set an avatar for use on
future calls
</Subtitle>
</Trans>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
{t("Create account")}
</LinkButton>
</div>
{!surveySubmitted && PosthogAnalytics.instance.isEnabled()
? qualitySurveyDialog
: createAccountDialog}
</main>
<Body className={styles.footer}>
<Link color="primary" to="/">

View File

@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Sentry from "@sentry/react";
import { Resizable } from "re-resizable";
import React, {
useEffect,
@@ -27,13 +28,25 @@ import ReactJson, { CollapsedFieldProps } from "react-json-view";
import mermaid from "mermaid";
import { Item } from "@react-stately/collections";
import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import {
GroupCall,
GroupCallError,
GroupCallEvent,
} from "matrix-js-sdk/src/webrtc/groupCall";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { CallEvent } from "matrix-js-sdk/src/webrtc/call";
import {
CallEvent,
CallState,
CallError,
MatrixCall,
VoipEvent,
} from "matrix-js-sdk/src/webrtc/call";
import styles from "./GroupCallInspector.module.css";
import { SelectInput } from "../input/SelectInput";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
interface InspectorContextState {
eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] };
@@ -108,6 +121,19 @@ function formatTimestamp(timestamp: number | Date) {
return dateFormatter.format(timestamp);
}
function formatType(event: SequenceDiagramMatrixEvent): string {
if (event.content.msgtype === "m.bad.encrypted") return "Undecryptable";
return event.type;
}
function lineForEvent(event: SequenceDiagramMatrixEvent): string {
return `${getUserName(event.from)} ${
event.ignored ? "-x" : "->>"
} ${getUserName(event.to)}: ${formatTimestamp(event.timestamp)} ${formatType(
event
)} ${formatContent(event.type, event.content)}`;
}
export const InspectorContext =
createContext<
[
@@ -187,21 +213,7 @@ export function SequenceDiagramViewer({
participant ${getUserName(localUserId)}
participant Room
participant ${selectedUserId ? getUserName(selectedUserId) : "unknown"}
${
events
? events
.map(
({ to, from, timestamp, type, content, ignored }) =>
`${getUserName(from)} ${ignored ? "-x" : "->>"} ${getUserName(
to
)}: ${formatTimestamp(timestamp)} ${type} ${formatContent(
type,
content
)}`
)
.join("\n ")
: ""
}
${events ? events.map(lineForEvent).join("\n ") : ""}
`;
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
@@ -234,7 +246,7 @@ function reducer(
action: {
type?: CallEvent | ClientEvent | RoomStateEvent;
event?: MatrixEvent;
rawEvent?: Record<string, unknown>;
rawEvent?: VoipEvent;
callStateEvent?: MatrixEvent;
memberStateEvents?: MatrixEvent[];
}
@@ -352,7 +364,7 @@ function reducer(
function useGroupCallState(
client: MatrixClient,
groupCall: GroupCall,
showPollCallStats: boolean
otelGroupCallMembership: OTelGroupCallMembership
): InspectorContextState {
const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(),
@@ -380,33 +392,77 @@ function useGroupCallState(
callStateEvent,
memberStateEvents,
});
otelGroupCallMembership?.onUpdateRoomState(event);
}
function onReceivedVoipEvent(event: MatrixEvent) {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
otelGroupCallMembership?.onReceivedVoipEvent(event);
}
function onSendVoipEvent(event: Record<string, unknown>) {
function onSendVoipEvent(event: VoipEvent, call: MatrixCall) {
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
otelGroupCallMembership?.onSendEvent(call, event);
}
function onCallStateChange(
newState: CallState,
_: CallState,
call: MatrixCall
) {
otelGroupCallMembership?.onCallStateChange(call, newState);
}
function onCallError(error: CallError, call: MatrixCall) {
otelGroupCallMembership.onCallError(error, call);
}
function onGroupCallError(error: GroupCallError) {
otelGroupCallMembership.onGroupCallError(error);
}
function onUndecryptableToDevice(event: MatrixEvent) {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
Sentry.captureMessage("Undecryptable to-device Event");
// probably unnecessary if it's now captured via otel?
PosthogAnalytics.instance.eventUndecryptableToDevice.track(
groupCall.groupCallId
);
otelGroupCallMembership.onUndecryptableToDevice(event);
}
client.on(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.on("calls_changed", onCallsChanged);
groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
groupCall.on(CallEvent.State, onCallStateChange);
groupCall.on(CallEvent.Error, onCallError);
groupCall.on(GroupCallEvent.Error, onGroupCallError);
//client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup);
client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice);
onUpdateRoomState();
return () => {
client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.removeListener("calls_changed", onCallsChanged);
groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
groupCall.removeListener(CallEvent.State, onCallStateChange);
groupCall.removeListener(CallEvent.Error, onCallError);
groupCall.removeListener(GroupCallEvent.Error, onGroupCallError);
//client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup);
client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
client.removeListener(
ClientEvent.UndecryptableToDeviceEvent,
onUndecryptableToDevice
);
};
}, [client, groupCall]);
}, [client, groupCall, otelGroupCallMembership]);
return state;
}
@@ -414,17 +470,19 @@ function useGroupCallState(
interface GroupCallInspectorProps {
client: MatrixClient;
groupCall: GroupCall;
otelGroupCallMembership: OTelGroupCallMembership;
show: boolean;
}
export function GroupCallInspector({
client,
groupCall,
otelGroupCallMembership,
show,
}: GroupCallInspectorProps) {
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
const [selectedUserId, setSelectedUserId] = useState<string>();
const state = useGroupCallState(client, groupCall, show);
const state = useGroupCallState(client, groupCall, otelGroupCallMembership);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setState] = useContext(InspectorContext);

View File

@@ -51,7 +51,7 @@ export function GroupCallLoader({
if (loading) {
return (
<FullScreenView>
<h1>{t("Loading room…")}</h1>
<h1>{t("Loading…")}</h1>
</FullScreenView>
);
}

View File

@@ -32,9 +32,9 @@ import { CallEndedView } from "./CallEndedView";
import { useRoomAvatar } from "./useRoomAvatar";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useMediaHandler } from "../settings/useMediaHandler";
import { findDeviceByName, getDevices } from "../media-utils";
import { findDeviceByName, getNamedDevices } from "../media-utils";
declare global {
interface Window {
@@ -75,12 +75,14 @@ export function GroupCallView({
toggleLocalVideoMuted,
toggleMicrophoneMuted,
toggleScreensharing,
setMicrophoneMuted,
requestingScreenshare,
isScreensharing,
screenshareFeeds,
participants,
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
otelGroupCallMembership,
} = useGroupCall(groupCall, client);
const { t } = useTranslation();
const { setAudioInput, setVideoInput } = useMediaHandler();
@@ -100,12 +102,17 @@ export function GroupCallView({
// Get the available devices so we can match the selected device
// to its ID. This involves getting a media stream (see docs on
// the function) so we only do it once and re-use the result.
const devices = await getDevices();
//
// But we only want to preload the devices if we get audio or video devices included in the widget request!
// By default, the device list is null!
let devices: MediaDeviceInfo[] | null = null;
const { audioInput, videoInput } = ev.detail
.data as unknown as JoinCallData;
if (audioInput !== null) {
// we load the devices because w have an audio device in the widget request
devices = await getNamedDevices();
const deviceId = await findDeviceByName(
audioInput,
"audioinput",
@@ -122,6 +129,11 @@ export function GroupCallView({
}
if (videoInput !== null) {
// we only need to load the devices once time
if (devices === null) {
// we load the devices because w have a video device in the widget request
devices = await getNamedDevices();
}
const deviceId = await findDeviceByName(
videoInput,
"videoinput",
@@ -141,8 +153,7 @@ export function GroupCallView({
groupCall.setLocalVideoMuted(videoInput === null),
]);
await groupCall.enter();
await enter();
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
@@ -157,17 +168,17 @@ export function GroupCallView({
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
}
}, [groupCall, preload, setAudioInput, setVideoInput]);
}, [groupCall, preload, setAudioInput, setVideoInput, enter]);
useEffect(() => {
if (isEmbedded && !preload) {
// In embedded mode, bypass the lobby and just enter the call straight away
groupCall.enter();
enter();
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
}
}, [groupCall, isEmbedded, preload]);
}, [groupCall, isEmbedded, preload, enter]);
useSentryGroupCallHandler(groupCall);
@@ -202,7 +213,11 @@ export function GroupCallView({
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
}
if (!isPasswordlessUser && !isEmbedded) {
if (
!isPasswordlessUser &&
!isEmbedded &&
!PosthogAnalytics.instance.isEnabled()
) {
history.push("/");
}
}, [groupCall, leave, isPasswordlessUser, isEmbedded, history]);
@@ -237,6 +252,7 @@ export function GroupCallView({
onLeave={onLeave}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
otelGroupCallMembership={otelGroupCallMembership}
/>
);
} else {
@@ -251,6 +267,7 @@ export function GroupCallView({
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
setMicrophoneMuted={setMicrophoneMuted}
userMediaFeeds={userMediaFeeds}
activeSpeaker={activeSpeaker}
onLeave={onLeave}
@@ -260,12 +277,28 @@ export function GroupCallView({
roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
otelGroupCallMembership={otelGroupCallMembership}
/>
);
}
} else if (left) {
if (isPasswordlessUser) {
return <CallEndedView client={client} />;
// The call ended view is shown for two reasons: prompting guests to create
// an account, and prompting users that have opted into analytics to provide
// feedback. We don't show a feedback prompt to widget users however (at
// least for now), because we don't yet have designs that would allow widget
// users to dismiss the feedback prompt and close the call window without
// submitting anything.
if (
isPasswordlessUser ||
(PosthogAnalytics.instance.isEnabled() && !isEmbedded)
) {
return (
<CallEndedView
endedCallId={groupCall.groupCallId}
client={client}
isPasswordlessUser={isPasswordlessUser}
/>
);
} else {
// If the user is a regular user, we'll have sent them back to the homepage,
// so just sit here & do nothing: otherwise we would (briefly) mount the
@@ -277,7 +310,7 @@ export function GroupCallView({
} else if (isEmbedded) {
return (
<FullScreenView>
<h1>{t("Loading room…")}</h1>
<h1>{t("Loading…")}</h1>
</FullScreenView>
);
} else {

View File

@@ -15,14 +15,20 @@ limitations under the License.
*/
.inRoom {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 100%;
position: fixed;
height: 100%;
width: 100%;
--footerPadding: 8px;
--footerHeight: calc(50px + 2 * var(--footerPadding));
}
.controlsOverlay {
position: relative;
flex: 1;
display: flex;
}
.centerMessage {
@@ -39,11 +45,27 @@ limitations under the License.
}
.footer {
position: relative;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
height: calc(50px + 2 * 8px);
padding: var(--footerPadding) 0;
/* TODO: Un-hardcode these colors */
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%
);
}
.footer > * {
@@ -60,21 +82,31 @@ limitations under the License.
bottom: 0;
}
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cqmin units */
@container videoTile (width > 0) {
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* To make avatars scale smoothly with their tiles during animations, we
override the styles set on the element */
--avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */
width: var(--avatarSize) !important;
height: var(--avatarSize) !important;
border-radius: 10000px !important;
}
}
@media (min-height: 300px) {
.footer {
height: calc(50px + 2 * 24px);
.inRoom {
--footerPadding: 24px;
}
}
@media (min-width: 800px) {
.footer {
height: calc(50px + 2 * 32px);
.inRoom {
--footerPadding: 32px;
}
}

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.
@@ -24,6 +24,7 @@ import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { OverlayTriggerState } from "@react-stately/overlays";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import type { IWidgetApiRequest } from "matrix-widget-api";
@@ -33,6 +34,8 @@ import {
MicButton,
VideoButton,
ScreenshareButton,
SettingsButton,
InviteButton,
} from "../button";
import {
Header,
@@ -42,20 +45,19 @@ import {
VersionMismatchWarning,
} from "../Header";
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { useMediaHandler } from "../settings/useMediaHandler";
import { useShowInspector, useSpatialAudio } from "../settings/useSetting";
import {
useNewGrid,
useShowInspector,
useSpatialAudio,
} from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { useAudioContext } from "../video-grid/useMediaStream";
import { useFullscreen } from "../video-grid/useFullscreen";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { widget, ElementWidgetActions } from "../widget";
import { useJoinRule } from "./useJoinRule";
import { useUrlParams } from "../UrlParams";
@@ -63,6 +65,14 @@ import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ParticipantInfo } from "./useGroupCall";
import { TileDescriptor } from "../video-grid/TileDescriptor";
import { AudioSink } from "../video-grid/AudioSink";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { VideoTile } from "../video-grid/VideoTile";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -81,6 +91,7 @@ interface Props {
toggleLocalVideoMuted: () => void;
toggleMicrophoneMuted: () => void;
toggleScreensharing: () => void;
setMicrophoneMuted: (muted: boolean) => void;
userMediaFeeds: CallFeed[];
activeSpeaker: CallFeed | null;
onLeave: () => void;
@@ -89,6 +100,7 @@ interface Props {
roomIdOrAlias: string;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean;
otelGroupCallMembership: OTelGroupCallMembership;
}
export function InCallView({
@@ -101,6 +113,7 @@ export function InCallView({
localVideoMuted,
toggleLocalVideoMuted,
toggleMicrophoneMuted,
setMicrophoneMuted,
userMediaFeeds,
activeSpeaker,
onLeave,
@@ -110,10 +123,10 @@ export function InCallView({
roomIdOrAlias,
unencryptedEventsFromUsers,
hideHeader,
otelGroupCallMembership,
}: Props) {
const { t } = useTranslation();
usePreventScroll();
const joinRule = useJoinRule(groupCall.room);
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
@@ -136,11 +149,17 @@ export function InCallView({
const [audioContext, audioDestination] = useAudioContext();
const [showInspector] = useShowInspector();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
const { hideScreensharing } = useUrlParams();
const joinRule = useJoinRule(groupCall.room);
useCallViewKeyboardShortcuts(
containerRef1,
toggleMicrophoneMuted,
toggleLocalVideoMuted,
setMicrophoneMuted
);
useEffect(() => {
widget?.api.transport.send(
layout === "freedom"
@@ -197,6 +216,8 @@ export function InCallView({
focused: screenshareFeeds.length === 0 && callFeed === activeSpeaker,
isLocal: member.userId === localUserId && deviceId === localDeviceId,
presenter,
isSpeaker: callFeed === activeSpeaker,
largeBaseSize: false,
connectionState,
});
}
@@ -209,6 +230,7 @@ export function InCallView({
// Add the screenshares too
for (const screenshareFeed of screenshareFeeds) {
const member = screenshareFeed.getMember()!;
const deviceId = screenshareFeed.deviceId!;
const connectionState = participants
.get(member)
?.get(screenshareFeed.deviceId!)?.connectionState;
@@ -223,6 +245,9 @@ export function InCallView({
focused: true,
isLocal: screenshareFeed.isLocal(),
presenter: false,
isSpeaker: screenshareFeed === activeSpeaker,
largeBaseSize: true,
placeNear: `${member.userId} ${deviceId}`,
connectionState,
});
}
@@ -266,6 +291,8 @@ export function InCallView({
[]
);
const [newGrid] = useNewGrid();
const Grid = newGrid ? NewVideoGrid : VideoGrid;
const prefersReducedMotion = usePrefersReducedMotion();
const renderContent = (): JSX.Element => {
@@ -278,49 +305,41 @@ export function InCallView({
}
if (maximisedParticipant) {
return (
<VideoTileContainer
height={bounds.height}
width={bounds.width}
<VideoTile
targetHeight={bounds.height}
targetWidth={bounds.width}
key={maximisedParticipant.id}
item={maximisedParticipant}
getAvatar={renderAvatar}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={true}
maximised={Boolean(maximisedParticipant)}
fullscreen={maximisedParticipant === fullscreenParticipant}
onFullscreen={toggleFullscreen}
showSpeakingIndicator={false}
/>
);
}
return (
<VideoGrid
<Grid
items={items}
layout={layout}
disableAnimations={prefersReducedMotion || isSafari}
>
{({
item,
...rest
}: {
item: TileDescriptor;
[x: string]: unknown;
}) => (
<VideoTileContainer
key={item.id}
item={item}
{(props) => (
<VideoTile
getAvatar={renderAvatar}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={items.length < 3}
maximised={false}
fullscreen={false}
onFullscreen={toggleFullscreen}
{...rest}
showSpeakingIndicator={items.length > 2}
{...props}
/>
)}
</VideoGrid>
</Grid>
);
};
@@ -329,6 +348,36 @@ export function InCallView({
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openSettings = useCallback(() => {
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]: maximisedParticipant,
});
@@ -343,11 +392,11 @@ export function InCallView({
const audioElements: JSX.Element[] = [];
if (!spatialAudio || maximisedParticipant) {
for (const item of items) {
if (item.isLocal) continue; // We don't want to render own audio
audioElements.push(
<AudioSink
tileDescriptor={item}
audioOutput={audioOutput}
otelGroupCallMembership={otelGroupCallMembership}
key={item.id}
/>
);
@@ -358,38 +407,44 @@ export function InCallView({
if (noControls) {
footer = null;
} else if (reducedControls) {
footer = (
<div className={styles.footer}>
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
<HangupButton onPress={onLeave} />
</div>
);
} else {
footer = (
<div className={styles.footer}>
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
{canScreenshare && !hideScreensharing && !isSafari && (
const buttons: JSX.Element[] = [];
buttons.push(
<MicButton
key="1"
muted={microphoneMuted}
onPress={toggleMicrophoneMuted}
data-testid="incall_mute"
/>,
<VideoButton
key="2"
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
data-testid="incall_videomute"
/>
);
if (!reducedControls) {
if (canScreenshare && !hideScreensharing && !isSafari) {
buttons.push(
<ScreenshareButton
key="3"
enabled={isScreensharing}
onPress={toggleScreensharing}
data-testid="incall_screenshare"
/>
)}
{!maximisedParticipant && (
<OverflowMenu
inCall
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
showInvite={joinRule === JoinRule.Public}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>
)}
<HangupButton onPress={onLeave} />
</div>
);
}
if (!maximisedParticipant) {
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
}
}
buttons.push(
<HangupButton key="6" onPress={onLeave} data-testid="incall_leave" />
);
footer = <div className={styles.footer}>{buttons}</div>;
}
return (
@@ -406,23 +461,38 @@ export function InCallView({
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
<UserMenuContainer preventNavigation />
{joinRule === JoinRule.Public && (
<InviteButton variant="icon" onClick={openInvite} />
)}
</RightNav>
</Header>
)}
{renderContent()}
{footer}
<div className={styles.controlsOverlay}>
{renderContent()}
{footer}
</div>
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
{rageshakeRequestModalState.isOpen && (
{rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomIdOrAlias={roomIdOrAlias}
/>
)}
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && (
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}
</div>
);
}

View File

@@ -41,6 +41,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
data-testid="modal_inviteLink"
/>
</ModalContent>
</Modal>

View File

@@ -137,6 +137,7 @@ export function LobbyView({
size="lg"
disabled={state !== GroupCallState.LocalCallFeedInitialized}
onPress={onEnter}
data-testid="lobby_joinCall"
>
Join call now
</Button>
@@ -146,6 +147,7 @@ export function LobbyView({
value={getRoomUrl(roomIdOrAlias)}
className={styles.copyButton}
copiedMessage={t("Call link copied")}
data-testid="lobby_inviteLink"
>
Copy call link and join later
</CopyButton>

View File

@@ -1,143 +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 React, { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { OverlayTriggerState } from "@react-stately/overlays";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { Menu } from "../Menu";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
import { Config } from "../config/Config";
interface Props {
roomIdOrAlias: string;
inCall: boolean;
groupCall: GroupCall;
showInvite: boolean;
feedbackModalState: OverlayTriggerState;
feedbackModalProps: {
isOpen: boolean;
onClose: () => void;
};
}
export function OverflowMenu({
roomIdOrAlias,
inCall,
groupCall,
showInvite,
feedbackModalState,
feedbackModalProps,
}: Props) {
const { t } = useTranslation();
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
// TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444
const onAction = useCallback(
(key) => {
switch (key) {
case "invite":
inviteModalState.open();
break;
case "settings":
settingsModalState.open();
break;
case "feedback":
feedbackModalState.open();
break;
}
},
[feedbackModalState, inviteModalState, settingsModalState]
);
const tooltip = useCallback(() => t("More"), [t]);
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger tooltip={tooltip} placement="top">
<Button variant="toolbar">
<OverflowIcon />
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label={t("More menu")} onAction={onAction}>
{showInvite && (
<Item key="invite" textValue={t("Invite people")}>
<AddUserIcon />
<span>{t("Invite people")}</span>
</Item>
)}
<Item key="settings" textValue={t("Settings")}>
<SettingsIcon />
<span>{t("Settings")}</span>
</Item>
{Config.get().rageshake?.submit_url && (
<Item key="feedback" textValue={t("Submit feedback")}>
<FeedbackIcon />
<span>{t("Submit feedback")}</span>
</Item>
)}
</Menu>
)}
</PopoverMenuTrigger>
{settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />}
{inviteModalState.isOpen && (
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}
{feedbackModalState.isOpen && (
<FeedbackModal
{...feedbackModalProps}
roomId={groupCall?.room.roomId}
inCall={inCall}
/>
)}
</>
);
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React, { useCallback, useState, useRef } from "react";
import classNames from "classnames";
import { useSpring, animated } from "@react-spring/web";
import { logger } from "@sentry/utils";
import styles from "./PTTButton.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
@@ -68,11 +69,23 @@ export const PTTButton: React.FC<Props> = ({
enqueueNetworkWaiting(true, 100);
startTalking();
}, [enqueueNetworkWaiting, startTalking, buttonHeld]);
const unhold = useCallback(() => {
if (!buttonHeld) return;
setButtonHeld(false);
setNetworkWaiting(false);
stopTalking();
}, [setNetworkWaiting, stopTalking]);
}, [setNetworkWaiting, stopTalking, buttonHeld]);
const onMouseUp = useCallback(() => {
logger.info("Mouse up event: unholding PTT button");
unhold();
}, [unhold]);
const onBlur = useCallback(() => {
logger.info("Blur event: unholding PTT button");
unhold();
}, [unhold]);
const onButtonMouseDown = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
@@ -85,7 +98,7 @@ export const PTTButton: React.FC<Props> = ({
// These listeners go on the window so even if the user's cursor / finger
// leaves the button while holding it, the button stays pushed until
// they stop clicking / tapping.
useEventTarget(window, "mouseup", unhold);
useEventTarget(window, "mouseup", onMouseUp);
useEventTarget(
window,
"touchend",
@@ -103,6 +116,8 @@ export const PTTButton: React.FC<Props> = ({
}
if (!touchFound) return;
logger.info("Touch event ended: unholding PTT button");
e.preventDefault();
unhold();
setActiveTouchId(null);
@@ -163,6 +178,8 @@ export const PTTButton: React.FC<Props> = ({
e.preventDefault();
logger.info("Keyup event for spacebar: unholding PTT button");
unhold();
}
},
@@ -171,7 +188,7 @@ export const PTTButton: React.FC<Props> = ({
);
// TODO: We will need to disable this for a global PTT hotkey to work
useEventTarget(window, "blur", unhold);
useEventTarget(window, "blur", onBlur);
const prefersReducedMotion = usePrefersReducedMotion();
const { shadow } = useSpring({

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.
@@ -27,7 +27,7 @@ import { useTranslation } from "react-i18next";
import { useDelayedState } from "../useDelayedState";
import { useModalTriggerState } from "../Modal";
import { InviteModal } from "./InviteModal";
import { HangupButton, InviteButton } from "../button";
import { HangupButton, InviteButton, SettingsButton } from "../button";
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
import styles from "./PTTCallView.module.css";
import { Facepile } from "../Facepile";
@@ -41,9 +41,10 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
import { usePTTSounds } from "../sound/usePttSounds";
import { PTTClips } from "../sound/PTTClips";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { Size } from "../Avatar";
import { ParticipantInfo } from "./useGroupCall";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
function getPromptText(
networkWaiting: boolean,
@@ -106,6 +107,7 @@ interface Props {
onLeave: () => void;
isEmbedded: boolean;
hideHeader: boolean;
otelGroupCallMembership: OTelGroupCallMembership;
}
export const PTTCallView: React.FC<Props> = ({
@@ -119,12 +121,14 @@ export const PTTCallView: React.FC<Props> = ({
onLeave,
isEmbedded,
hideHeader,
otelGroupCallMembership,
}) => {
const { t } = useTranslation();
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
const { modalState: settingsModalState, modalProps: settingsModalProps } =
useModalTriggerState();
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
const facepileSize = bounds.width < 800 ? Size.SM : Size.MD;
const showControls = bounds.height > 500;
@@ -192,6 +196,7 @@ export const PTTCallView: React.FC<Props> = ({
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
// Never shown in PTT mode, but must be present to collect call state
// https://github.com/vector-im/element-call/issues/328
show={false}
@@ -210,36 +215,29 @@ export const PTTCallView: React.FC<Props> = ({
</Header>
)}
<div className={styles.center}>
{showControls && (
<>
<div className={styles.participants}>
<p>
{t("{{count}} people connected", {
count: participatingMembers.length,
})}
</p>
<Facepile
size={facepileSize}
max={8}
className={styles.facepile}
client={client}
members={participatingMembers}
/>
</div>
<div className={styles.footer}>
<OverflowMenu
inCall
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
showInvite={false}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>
{!isEmbedded && <HangupButton onPress={onLeave} />}
<InviteButton onPress={() => inviteModalState.open()} />
</div>
</>
)}
{/* Always render this because the window will become shorter when the on-screen
keyboard appears, so if we don't render it, the dialog will unmount. */}
<div style={{ display: showControls ? "block" : "none" }}>
<div className={styles.participants}>
<p>
{t("{{count}} people connected", {
count: participatingMembers.length,
})}
</p>
<Facepile
size={facepileSize}
max={8}
className={styles.facepile}
client={client}
members={participatingMembers}
/>
</div>
<div className={styles.footer}>
<SettingsButton onPress={() => settingsModalState.open()} />
{!isEmbedded && <HangupButton onPress={onLeave} />}
<InviteButton onPress={() => inviteModalState.open()} />
</div>
</div>
<div className={styles.pttButtonContainer}>
{showControls &&
@@ -261,7 +259,7 @@ export const PTTCallView: React.FC<Props> = ({
<div className={styles.talkingInfo} />
))}
<PTTButton
enabled={!feedbackModalState.isOpen}
enabled={!inviteModalState.isOpen && !settingsModalState.isOpen}
showTalkOverError={showTalkOverError}
activeSpeakerUserId={activeSpeakerUserId}
activeSpeakerDisplayName={activeSpeakerDisplayName}
@@ -308,6 +306,13 @@ export const PTTCallView: React.FC<Props> = ({
</div>
</div>
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && showControls && (
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}

View File

@@ -74,6 +74,7 @@ export function RoomAuthView() {
name="displayName"
label={t("Display name")}
placeholder={t("Display name")}
data-testid="joincall_displayName"
type="text"
required
autoComplete="off"
@@ -90,7 +91,12 @@ export function RoomAuthView() {
<ErrorMessage error={error} />
</FieldRow>
)}
<Button type="submit" size="lg" disabled={loading}>
<Button
type="submit"
size="lg"
disabled={loading}
data-testid="joincall_joincall"
>
{loading ? t("Loading…") : t("Join call now")}
</Button>
<div id={recaptchaId} />

View File

@@ -1,5 +1,5 @@
/*
Copyright 2021-2022 New Vector Ltd
Copyright 2021-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.
@@ -24,9 +24,9 @@ import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView";
import { useUrlParams } from "../UrlParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { translatedError } from "../TranslatedError";
import { useOptInAnalytics } from "../settings/useSetting";
export const RoomPage: FC = () => {
const { t } = useTranslation();
@@ -46,13 +46,19 @@ export const RoomPage: FC = () => {
const roomIdOrAlias = roomId ?? roomAlias;
if (!roomIdOrAlias) throw translatedError("No room specified", t);
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false);
useEffect(() => {
// If we're not already authed and we've been given a display name as
// During the beta, opt into analytics by default
if (optInAnalytics === null) setOptInAnalytics(true);
}, [optInAnalytics, setOptInAnalytics]);
useEffect(() => {
// If we've finished loading, are not already authed and we've been given a display name as
// a URL param, automatically register a passwordless user
if (!isAuthenticated && displayName) {
if (!loading && !isAuthenticated && displayName) {
setIsRegistering(true);
registerPasswordlessUser(displayName).finally(() => {
setIsRegistering(false);
@@ -63,6 +69,7 @@ export const RoomPage: FC = () => {
displayName,
setIsRegistering,
registerPasswordlessUser,
loading,
]);
const groupCallView = useCallback(
@@ -93,15 +100,13 @@ export const RoomPage: FC = () => {
}
return (
<MediaHandlerProvider client={client}>
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
createPtt={isPtt}
>
{groupCallView}
</GroupCallLoader>
</MediaHandlerProvider>
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
createPtt={isPtt}
>
{groupCallView}
</GroupCallLoader>
);
};

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,21 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { useCallback } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { OverlayTriggerState } from "@react-stately/overlays";
import { MicButton, VideoButton } from "../button";
import { MicButton, SettingsButton, VideoButton } from "../button";
import { useMediaStream } from "../video-grid/useMediaStream";
import { OverflowMenu } from "./OverflowMenu";
import { Avatar } from "../Avatar";
import { useProfile } from "../profile/useProfile";
import styles from "./VideoPreview.module.css";
import { Body } from "../typography/Typography";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
interface Props {
client: MatrixClient;
@@ -59,12 +60,30 @@ export function VideoPreview({
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2;
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openSettings = useCallback(() => {
settingsModalState.open();
}, [settingsModalState]);
return (
<div className={styles.preview} ref={previewRef}>
<video ref={videoRef} muted playsInline disablePictureInPicture />
<video
ref={videoRef}
muted
playsInline
disablePictureInPicture
data-testid="preview_video"
/>
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
{t("Camera/microphone permissions needed to join the call.")}
@@ -95,17 +114,13 @@ export function VideoPreview({
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
/>
<OverflowMenu
roomIdOrAlias={roomIdOrAlias}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
inCall={false}
groupCall={undefined}
showInvite={false}
/>
<SettingsButton onPress={openSettings} />
</div>
</>
)}
{settingsModalState.isOpen && (
<SettingsModal client={client} {...settingsModalProps} />
)}
</div>
);
}

View File

@@ -0,0 +1,65 @@
/*
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 { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
function isObject(x: unknown): x is Record<string, unknown> {
return typeof x === "object" && x !== null;
}
/**
* Checks the state of a room for multiple calls happening in parallel, sending
* the details to PostHog if that is indeed what's happening. (This is unwanted
* as it indicates a split-brain scenario.)
*/
export function checkForParallelCalls(state: RoomState): void {
const now = Date.now();
const participantsPerCall = new Map<string, number>();
// For each participant in each call, increment the participant count
for (const e of state.getStateEvents(EventType.GroupCallMemberPrefix)) {
const content = e.getContent<Record<string, unknown>>();
const calls: unknown[] = Array.isArray(content["m.calls"])
? content["m.calls"]
: [];
for (const call of calls) {
if (isObject(call) && typeof call["m.call_id"] === "string") {
const devices: unknown[] = Array.isArray(call["m.devices"])
? call["m.devices"]
: [];
for (const device of devices) {
if (isObject(device) && (device["expires_ts"] as number) > now) {
const participantCount =
participantsPerCall.get(call["m.call_id"]) ?? 0;
participantsPerCall.set(call["m.call_id"], participantCount + 1);
}
}
}
}
}
if (participantsPerCall.size > 1) {
PosthogAnalytics.instance.trackEvent({
eventName: "ParallelCalls",
participantsPerCall: Object.fromEntries(participantsPerCall),
});
}
}

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