Compare commits

..

206 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
Abhi Jain
33dd2758d7 change ... to … unicode for consistency 2023-02-13 09:55:32 +05:30
Abhi Jain
d40e467b7d changes Loading room... to Loading... 2023-02-08 14:26:56 +05:30
82 changed files with 2688 additions and 362 deletions

View File

@@ -23,9 +23,6 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# This appears to be necessary to stop Vite from OOMing
# https://github.com/vitejs/vite/issues/2433
NODE_OPTIONS: "--max-old-space-size=16384"
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:

View File

@@ -40,9 +40,6 @@ jobs:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
VITE_APP_VERSION: ${{ github.event.release.tag_name }}
# This appears to be necessary to stop Vite from OOMing
# https://github.com/vitejs/vite/issues/2433
NODE_OPTIONS: "--max-old-space-size=16384"
- name: Create Tarball
env:

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;
}
}
}

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",
@@ -19,6 +19,13 @@
"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.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",
@@ -46,7 +53,7 @@
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#da03c3b529576a8fcde6f2c9a171fa6cca012830",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",

View File

@@ -46,7 +46,6 @@
"Join call now": "Влез в разговора сега",
"Join existing call?": "Присъединяване към съществуващ разговор?",
"Leave": "Напусни",
"Loading room…": "Напускане на стаята…",
"Loading…": "Зареждане…",
"Local volume": "Локална сила на звука",
"Logging in…": "Влизане…",

View File

@@ -71,7 +71,6 @@
"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",
@@ -91,12 +90,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}}",
"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",
@@ -136,5 +133,8 @@
"{{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>"
"<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

@@ -45,7 +45,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 …",
@@ -131,15 +130,13 @@
"{{name}} (Connecting...)": "{{name}} (verbindet sich …)",
"Copy": "Kopieren",
"Element Call Home": "Element Call-Startseite",
"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.",
"<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",
"Privacy Policy": "Datenschutzerklärung",
"Expose developer settings in the settings window.": "Zeige die Entwicklereinstellungen im Einstellungsfenster.",
"Developer Settings": "Entwicklereinstellungen",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Mit dem Anwählen dieses Feldes akzeptierst du die Sammlung anonymer Daten, die wir zur Verbesserung deiner Erfahrung verwenden. Weitere Informationen dazu, welche Daten wir sammeln, findest du in unserer "
"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."
}

100
public/locales/el/app.json Normal file
View File

@@ -0,0 +1,100 @@
{
"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": "Επιλέξτε μια επιλογή",
"Saving…": "Αποθήκευση…",
"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": "Ρυθμίσεις",
"Save": "Αποθήκευση",
"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 menu": "Μενού περισσότερα",
"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": "Λεπτομέρειες",
"Description (optional)": "Περιγραφή (προαιρετική)",
"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}} (Συνδέεται...)",
"{{displayName}}, your call is now ended": "{{displayName}}, η κλήση σας τερματίστηκε",
"{{count}} people connected|other": "{{count}} άτομα συνδεδεμένα",
"{{count}} people connected|one": "{{count}} άτομο συνδεδεμένο"
}

View File

@@ -8,6 +8,7 @@
"{{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>",
@@ -21,7 +22,7 @@
"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 ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ",
"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",
@@ -63,7 +64,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…",
@@ -85,7 +85,6 @@
"Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}",
"Press and hold to talk": "Press and hold to talk",
"Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}",
"Privacy Policy": "Privacy Policy",
"Profile": "Profile",
"Recaptcha dismissed": "Recaptcha dismissed",
"Recaptcha not loaded": "Recaptcha not loaded",
@@ -106,7 +105,6 @@
"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}}",
@@ -138,7 +136,6 @@
"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 recent calls": "Your recent calls"

View File

@@ -77,7 +77,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",
@@ -131,13 +130,10 @@
"{{count}} people connected|one": "{{count}} persona conectada",
"Element Call Home": "Inicio de Element Call",
"Copy": "Copiar",
"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...)",
"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>",
"Privacy Policy": "Política de privacidad",
"Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
"Developer Settings": "Ajustes de desarrollador"
}

View File

@@ -70,7 +70,6 @@
"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",
@@ -131,15 +130,13 @@
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika</2> ja <6>teenusetingimused</6>.<9></9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega</12>",
"Element Call Home": "Element Call Home",
"Copy": "Kopeeri",
"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.",
"<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",
"Privacy Policy": "Privaatsuspoliitika",
"Expose developer settings in the settings window.": "Näita seadistuste aknas arendajale vajalikke seadeid.",
"Developer Settings": "Arendaja seadistused",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Selle valiku märkimisel lubad meil koguda anonüümseid andmeid, mida me pruugime sinu kasutajakogemuse parandamiseks. Üksikasjalikumat teavet meie kogutavate andmete kohta leiad siit - "
"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."
}

View File

@@ -21,7 +21,6 @@
"Login to your account": "به حساب کاربری خود وارد شوید",
"Login": "ورود",
"Loading…": "بارگزاری…",
"Loading room…": "بارگزاری اتاق…",
"Leave": "خروج",
"Join existing call?": "پیوست به تماس؟",
"Join call now": "الان به تماس بپیوند",
@@ -127,8 +126,6 @@
"Submit feedback": "بازخورد ارائه دهید",
"Stop sharing screen": "توقف اشتراک‌گذاری صفحه نمایش",
"Spatial audio": "صدای جهت‌دار",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "این که میان‌برهای صفحه‌کلید تک‌کلیده مثل m برای خموشی و ناخموشی میکروفون به کار بیفتند یا نه.",
"Single-key keyboard shortcuts": "میان‌برهای صفحه‌کلید تک‌کلیده",
"Element Call Home": "خانهٔ تماس المنت",
"Copy": "رونوشت",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>اکنون به تماس پیوسته</0><1>یا</1><2>پیوند تماس را رونوشت کرده و بعداً بپیوندید</2>",

View File

@@ -43,7 +43,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…",
@@ -131,15 +130,13 @@
"{{name}} (Connecting...)": "{{name}} (Connexion…)",
"Element Call Home": "Accueil Element Call",
"Copy": "Copier",
"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.",
"<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",
"Privacy Policy": "Politique de confidentialité",
"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 ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "En cochant cette case vous consentez à la collecte de données anonymes, que nous nous utilisons pour améliorer votre expérience. Vous trouverez plus dinformations sur les données collectées dans notre "
"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."
}

View File

@@ -46,7 +46,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…",
@@ -131,15 +130,13 @@
"{{name}} (Connecting...)": "{{name}} (Menghubungkan...)",
"Element Call Home": "Beranda Element Call",
"Copy": "Salin",
"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.",
"<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",
"Privacy Policy": "Kebijakan Privasi kami",
"Expose developer settings in the settings window.": "Ekspos pengaturan pengembang dalam jendela pengaturan.",
"Developer Settings": "Pengaturan Pengembang",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Dengan mencentang kotak ini Anda setuju untuk pengumpulan data anonim, yang kami gunakan untuk meningkatkan pengalaman. Anda dapat mempelajari lebih banyak informasi tentang data yang kami lacak di "
"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."
}

View File

@@ -53,7 +53,6 @@
"Login": "ログイン",
"Logging in…": "ログインしています…",
"Loading…": "読み込んでいます…",
"Loading room…": "ルームを読み込んでいます…",
"Leave": "退出",
"Version: {{version}}": "バージョン:{{version}}",
"Username": "ユーザー名",

View File

@@ -3,7 +3,7 @@
"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",
"{{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",
@@ -73,7 +73,6 @@
"Logging in…": "Logowanie…",
"Local volume": "Lokalna głośność",
"Loading…": "Ładowanie…",
"Loading room…": "Ładowanie pokoju…",
"Leave": "Opuść",
"Join existing call?": "Dołączyć do istniejącego połączenia?",
"Join call now": "Dołącz do połączenia teraz",
@@ -99,7 +98,7 @@
"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",
@@ -125,17 +124,19 @@
"{{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",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Czy włączyć skróty klawiszowe pojedynczych klawiszy, np. 'm' aby wyciszyć/załączyć mikrofon.",
"This feature is only supported on Firefox.": "Ta funkcjonalność jest dostępna tylko w Firefox.",
"Single-key keyboard shortcuts": "Skróty klawiszowe (pojedyncze klawisze)",
"Copy": "Kopiuj",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Wysłanie logó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 teraz</0><1>Or</1><2>Skopiuj link do rozmowy i dołącz później</2>",
"<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>Or</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świetlaj opcje programisty w oknie ustawień.",
"Element Call Home": "Strona główna Element Call",
"Developer Settings": "Opcje programisty",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Zaznaczając to pole, wyrażasz zgodę na gromadzenie anonimowych danych, które wykorzystujemy do poprawy Twoich doświadczeń. Więcej informacji o tym, jakie dane śledzimy, można znaleźć w naszym "
"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>."
}

View File

@@ -86,7 +86,6 @@
"Login to your account": "Войдите в свой аккаунт",
"Login": "Вход",
"Loading…": "Загрузка…",
"Loading room…": "Загрузка комнаты…",
"Leave": "Покинуть",
"Join existing call?": "Присоединиться к существующему звонку?",
"Join call now": "Присоединиться сейчас",
@@ -129,13 +128,15 @@
"{{count}} people connected|one": "{{count}} подключился",
"Element Call Home": "Главная Element Call",
"Copy": "Копировать",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Присоединиться сейчас</0><1>или<1><2>cкопировать ссылку на звонок и присоединиться позже</2>",
"<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}} (Соединение...)",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Включить горячие клавиши, например 'm' чтобы отключить/включить микрофон.",
"This feature is only supported on Firefox.": "Эта возможность доступна только в Firefox.",
"Single-key keyboard shortcuts": "Горячие клавиши",
"<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": "Использовать сеточный показ"
"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,7 +5,6 @@
"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 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.)",
@@ -16,7 +15,6 @@
"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…",
@@ -54,7 +52,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",
@@ -138,8 +135,8 @@
"<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",
"Privacy Policy": "Zásady ochrany osobných údajov",
"Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
"Developer Settings": "Nastavenia pre vývojárov",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Označením tohto políčka súhlasíte so zhromažďovaním anonymných údajov, ktoré používame na zlepšenie vašich skúseností. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich "
"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."
}

View File

@@ -43,7 +43,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…",

View File

@@ -38,8 +38,8 @@
"Share screen": "Поділитися екраном",
"Settings": "Налаштування",
"Sending…": "Надсилання…",
"Sending debug logs…": "Надсилання журналу зневадження…",
"Send debug logs": "Надіслати журнал зневадження",
"Sending debug logs…": "Надсилання журналу налагодження…",
"Send debug logs": "Надіслати журнал налагодження",
"Select an option": "Вибрати опцію",
"Saving…": "Збереження…",
"Save": "Зберегти",
@@ -72,7 +72,6 @@
"Login": "Увійти",
"Logging in…": "Вхід…",
"Local volume": "Локальна гучність",
"Loading room…": "Завантаження кімнати…",
"Leave": "Вийти",
"Join existing call?": "Приєднатися до наявного виклику?",
"Join call now": "Приєднатися до виклику зараз",
@@ -82,7 +81,7 @@
"Inspector": "Інспектор",
"Incompatible versions!": "Несумісні версії!",
"Incompatible versions": "Несумісні версії",
"Include debug logs": "Долучити журнали зневадження",
"Include debug logs": "Долучити журнали налагодження",
"Home": "Домівка",
"Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.",
"Grid layout menu": "Меню у вигляді сітки",
@@ -91,13 +90,13 @@
"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 +113,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>",
@@ -131,15 +130,13 @@
"{{name}} (Connecting...)": "{{name}} (З'єднання...)",
"Element Call Home": "Домівка Element Call",
"Copy": "Копіювати",
"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.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Надсилання журналів зневадження допоможе нам виявити проблему.</0>",
"<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": "Використовувати майбутню сіткову систему",
"Privacy Policy": "Політика приватності",
"Expose developer settings in the settings window.": "Відкрийте налаштування розробника у вікні налаштувань.",
"Developer Settings": "Налаштування розробника",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Ставлячи прапорець у цьому полі, ви погоджуєтеся на збір анонімних даних, які ми використовуємо для поліпшення роботи. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашому "
"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>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику."
}

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": "对讲机通话",
@@ -32,7 +31,6 @@
"Speaker {{n}}": "发言人 {{n}}",
"Speaker": "发言人",
"Spatial audio": "空间音频",
"Single-key keyboard shortcuts": "单键键盘快捷方式",
"Sign out": "注销登录",
"Sign in": "登录",
"Audio": "音频",
@@ -92,7 +90,6 @@
"Logging in…": "登录中……",
"Local volume": "本地音量",
"Loading…": "加载中……",
"Loading room…": "加载房间中……",
"Leave": "离开",
"Join existing call?": "加入现有的通话?",
"Join call now": "现在加入通话",

View File

@@ -12,16 +12,13 @@
"{{count}} people connected|other": "{{count}} 人已連結",
"{{count}} people connected|one": "{{count}} 人已連結",
"Use the upcoming grid system": "使用即將推出的網格系統",
"Privacy Policy": "隱私權政策",
"Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。",
"Developer Settings": "開發者設定",
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "勾選這個選取盒,代表您同意我們以匿名方式收集資料,用於改善您的使用體驗。您可以在下列位置找到我們會收集哪些資料的相關資訊: ",
"<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": "是,加入對話",
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "是否要啟用快捷鍵,例如:開/關麥克風。",
"WebRTC is not supported or is being blocked in this browser.": "此瀏覽器未支援 WebRTC 或 WebRTC 被瀏覽器封鎖。",
"Walkie-talkie call name": "對講機式通話名稱",
"Walkie-talkie call": "即時通話",
@@ -52,7 +49,6 @@
"Speaker {{n}}": "發言者{{n}}",
"Speaker": "發言者",
"Spatial audio": "空間音效",
"Single-key keyboard shortcuts": "快捷鍵",
"Sign out": "登出",
"Sign in": "登入",
"Show call inspector": "顯示通話稽查員",
@@ -94,7 +90,6 @@
"Logging in…": "登入中…",
"Local volume": "您的音量",
"Loading…": "載入中…",
"Loading room…": "載入聊天室…",
"Leave": "離開",
"Join existing call?": "加入已開始的通話嗎?",
"Join call now": "現在加入通話",
@@ -141,5 +136,7 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。",
"Accept microphone permissions to join the call.": "請授權使用您的麥克風以加入通話。",
"Accept camera/microphone permissions to join the call.": "請授權使用您的相機/麥克風以加入對話。",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>何不設定密碼以保留此帳號?</0><1>您可以保留暱稱並設定頭像,以便未來通話時使用</1>"
"<0>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>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。"
}

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

@@ -342,6 +342,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
useEffect(() => {
window.matrixclient = client;
window.isPasswordlessUser = isPasswordlessUser;
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
}, [client, isPasswordlessUser]);
if (error) {

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

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

View File

@@ -58,6 +58,7 @@ export function UserMenu({
key: "user",
icon: UserIcon,
label: displayName,
dataTestid: "usermenu_user",
});
if (isPasswordlessUser && !preventNavigation) {
@@ -65,6 +66,7 @@ export function UserMenu({
key: "login",
label: t("Sign in"),
icon: LoginIcon,
dataTestid: "usermenu_login",
});
}
@@ -73,6 +75,7 @@ export function UserMenu({
key: "logout",
label: t("Sign out"),
icon: LogoutIcon,
dataTestid: "usermenu_logout",
});
}
}
@@ -93,7 +96,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 +115,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

@@ -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

@@ -1,20 +0,0 @@
import { t } from "i18next";
import React from "react";
import { Link } from "../typography/Typography";
export const optInDescription: () => JSX.Element = () => {
return (
<>
<>
{t(
"By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our "
)}
</>
<Link color="primary" href="https://element.io/privacy">
<>{t("Privacy Policy")}</>
</Link>
.
</>
);
};

View File

@@ -102,6 +102,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);
@@ -227,7 +231,7 @@ export class PosthogAnalytics {
.join("");
}
public async identifyUser(analyticsIdGenerator: () => string) {
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.
@@ -319,7 +323,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.
//
@@ -339,7 +348,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
@@ -348,6 +357,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
@@ -385,7 +398,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

View File

@@ -0,0 +1,154 @@
/*
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"]}`;
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,
},
// 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> {}
}

View File

@@ -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

@@ -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

@@ -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?: {

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

@@ -39,11 +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 { optInDescription } from "../analytics/AnalyticsOptInDescription";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
interface Props {
client: MatrixClient;
@@ -54,7 +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, setOptInAnalytics] = useOptInAnalytics();
const [optInAnalytics] = useOptInAnalytics();
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
@@ -133,6 +133,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
type="text"
required
autoComplete="off"
data-testid="home_callName"
/>
<Button
@@ -140,19 +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>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription()}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}
/>
{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

@@ -39,15 +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";
import { optInDescription } from "../analytics/AnalyticsOptInDescription";
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, setOptInAnalytics] = useOptInAnalytics();
const [optInAnalytics] = useOptInAnalytics();
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
@@ -142,6 +142,7 @@ export const UnauthenticatedView: FC = () => {
type="text"
required
autoComplete="off"
data-testid="home_callName"
/>
</FieldRow>
<FieldRow>
@@ -152,19 +153,16 @@ export const UnauthenticatedView: FC = () => {
placeholder={t("Display name")}
type="text"
required
data-testid="home_displayName"
autoComplete="off"
/>
</FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription()}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}
/>
<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>
@@ -175,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} />
@@ -183,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

@@ -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

@@ -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);
@@ -209,6 +211,15 @@ 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();

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

@@ -0,0 +1,119 @@
/*
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 { ObjectFlattener } from "./ObjectFlattener";
/**
* Tracks an individual call within a group call, either to a full-mesh peer or a focus
*/
export class OTelCall {
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);
};
}

View File

@@ -0,0 +1,434 @@
/*
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,
} 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.span.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 onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport>
) {
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.ConnectionReport;
const data =
ObjectFlattener.flattenConnectionStatsReportObject(statsReport);
this.buildStatsEventSpan({ type, data });
}
public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport>
) {
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.ByteSentReport;
const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport);
this.buildStatsEventSpan({ type, data });
}
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();
}
}
private buildStatsEventSpan(event: OTelStatsReportEvent): void {
// @ TODO: fix this - Because on multiple calls we receive multiple stats report spans.
// This could be break if stats arrived in same time from different call objects.
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
const ctx = setSpan(
opentelemetry.context.active(),
this.callMembershipSpan
);
this.statsReportSpan.span =
ElementCallOpenTelemetry.instance?.tracer.startSpan(
"matrix.groupCallMembership.statsReport",
undefined,
ctx
);
if (this.statsReportSpan.span === undefined) {
return;
}
this.statsReportSpan.span.setAttribute(
"matrix.confId",
this.groupCall.groupCallId
);
this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId);
this.statsReportSpan.span.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name"
);
this.statsReportSpan.span.addEvent(event.type, event.data);
this.statsReportSpan.stats.push(event);
} else if (
this.statsReportSpan.span !== undefined &&
this.callMembershipSpan
) {
this.statsReportSpan.span.addEvent(event.type, event.data);
this.statsReportSpan.span.end();
this.statsReportSpan = { span: undefined, stats: [] };
}
}
}
interface OTelStatsReportEvent {
type: OTelStatsReportType;
data: Attributes;
}
enum OTelStatsReportType {
ConnectionReport = "matrix.stats.connection",
ByteSentReport = "matrix.stats.byteSent",
SummaryReport = "matrix.stats.summary",
}

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

@@ -0,0 +1,113 @@
/*
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 flattenConnectionStatsReportObject(
statsReport: GroupCallStatsReport<ConnectionStatsReport>
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.conn.",
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

@@ -119,6 +119,7 @@ export function ProfileModal({ client, ...rest }: Props) {
placeholder={t("Display name")}
value={displayName}
onChange={onChangeDisplayName}
data-testid="profile_displayname"
/>
</FieldRow>
{error && (
@@ -130,7 +131,11 @@ export function ProfileModal({ client, ...rest }: Props) {
<Button type="button" variant="secondary" onPress={onClose}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
<Button
type="submit"
disabled={loading}
data-testid="profile_submit"
>
{loading ? t("Saving…") : t("Save")}
</Button>
</FieldRow>

View File

@@ -28,14 +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, VoipEvent } 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[] };
@@ -353,7 +364,7 @@ function reducer(
function useGroupCallState(
client: MatrixClient,
groupCall: GroupCall,
showPollCallStats: boolean
otelGroupCallMembership: OTelGroupCallMembership
): InspectorContextState {
const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(),
@@ -381,28 +392,55 @@ function useGroupCallState(
callStateEvent,
memberStateEvents,
});
otelGroupCallMembership?.onUpdateRoomState(event);
}
function onReceivedVoipEvent(event: MatrixEvent) {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
otelGroupCallMembership?.onReceivedVoipEvent(event);
}
function onSendVoipEvent(event: VoipEvent) {
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);
@@ -412,8 +450,10 @@ function useGroupCallState(
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);
@@ -422,7 +462,7 @@ function useGroupCallState(
onUndecryptableToDevice
);
};
}, [client, groupCall]);
}, [client, groupCall, otelGroupCallMembership]);
return state;
}
@@ -430,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

@@ -81,7 +81,8 @@ export function GroupCallView({
screenshareFeeds,
participants,
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
otelGroupCallMembership,
} = useGroupCall(groupCall, client);
const { t } = useTranslation();
const { setAudioInput, setVideoInput } = useMediaHandler();
@@ -142,8 +143,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);
@@ -158,17 +158,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);
@@ -238,6 +238,7 @@ export function GroupCallView({
onLeave={onLeave}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
otelGroupCallMembership={otelGroupCallMembership}
/>
);
} else {
@@ -262,6 +263,7 @@ export function GroupCallView({
roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
otelGroupCallMembership={otelGroupCallMembership}
/>
);
}
@@ -279,7 +281,7 @@ export function GroupCallView({
} else if (isEmbedded) {
return (
<FullScreenView>
<h1>{t("Loading room…")}</h1>
<h1>{t("Loading…")}</h1>
</FullScreenView>
);
} else {

View File

@@ -15,7 +15,6 @@ limitations under the License.
*/
.inRoom {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
@@ -26,6 +25,12 @@ limitations under the License.
--footerHeight: calc(50px + 2 * var(--footerPadding));
}
.controlsOverlay {
position: relative;
flex: 1;
display: flex;
}
.centerMessage {
display: flex;
flex: 1;

View File

@@ -73,6 +73,7 @@ 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";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -100,6 +101,7 @@ interface Props {
roomIdOrAlias: string;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean;
otelGroupCallMembership: OTelGroupCallMembership;
}
export function InCallView({
@@ -122,6 +124,7 @@ export function InCallView({
roomIdOrAlias,
unencryptedEventsFromUsers,
hideHeader,
otelGroupCallMembership,
}: Props) {
const { t } = useTranslation();
usePreventScroll();
@@ -154,7 +157,7 @@ export function InCallView({
const { hideScreensharing } = useUrlParams();
useCallViewKeyboardShortcuts(
!feedbackModalState.isOpen,
containerRef1,
toggleMicrophoneMuted,
toggleLocalVideoMuted,
setMicrophoneMuted
@@ -357,11 +360,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}
/>
);
@@ -380,11 +383,13 @@ export function InCallView({
key="1"
muted={microphoneMuted}
onPress={toggleMicrophoneMuted}
data-testid="incall_mute"
/>,
<VideoButton
key="2"
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
data-testid="incall_videomute"
/>
);
@@ -395,6 +400,7 @@ export function InCallView({
key="3"
enabled={isScreensharing}
onPress={toggleScreensharing}
data-testid="incall_screenshare"
/>
);
}
@@ -435,11 +441,14 @@ export function InCallView({
</RightNav>
</Header>
)}
{renderContent()}
{footer}
<div className={styles.controlsOverlay}>
{renderContent()}
{footer}
</div>
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
{rageshakeRequestModalState.isOpen && (

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

@@ -102,7 +102,7 @@ export function OverflowMenu({
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger tooltip={tooltip} placement="top">
<Button variant="toolbar">
<Button variant="toolbar" data-testid="call_more">
<OverflowIcon />
</Button>
</TooltipTrigger>
@@ -111,7 +111,7 @@ export function OverflowMenu({
{showInvite && (
<Item key="invite" textValue={t("Invite people")}>
<AddUserIcon />
<span>{t("Invite people")}</span>
<span data-testid="call_moreInvite">{t("Invite people")}</span>
</Item>
)}
<Item key="settings" textValue={t("Settings")}>

View File

@@ -44,6 +44,7 @@ import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { Size } from "../Avatar";
import { ParticipantInfo } from "./useGroupCall";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
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,6 +121,7 @@ export const PTTCallView: React.FC<Props> = ({
onLeave,
isEmbedded,
hideHeader,
otelGroupCallMembership,
}) => {
const { t } = useTranslation();
const { modalState: inviteModalState, modalProps: inviteModalProps } =
@@ -192,6 +195,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}

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

@@ -27,6 +27,7 @@ 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,9 +47,15 @@ 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(() => {
// 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

View File

@@ -64,7 +64,13 @@ export function VideoPreview({
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.")}

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),
});
}
}

View File

@@ -22,16 +22,27 @@ import {
GroupCallErrorCode,
GroupCallUnknownDeviceError,
GroupCallError,
GroupCallStatsReportEvent,
GroupCallStatsReport,
} from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next";
import { IWidgetApiRequest } from "matrix-widget-api";
import { MatrixClient, RoomStateEvent } from "matrix-js-sdk";
import {
ByteSentStatsReport,
ConnectionStatsReport,
SummaryStatsReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { usePageUnload } from "./usePageUnload";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { TranslatedError, translatedError } from "../TranslatedError";
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { ElementCallOpenTelemetry } from "../otel/otel";
import { checkForParallelCalls } from "./checkForParallelCalls";
export enum ConnectionState {
EstablishingCall = "establishing call", // call hasn't been established yet
@@ -66,6 +77,7 @@ export interface UseGroupCallReturnType {
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>;
otelGroupCallMembership: OTelGroupCallMembership;
}
interface State {
@@ -84,6 +96,13 @@ interface State {
hasLocalParticipant: boolean;
}
// This is a bit of a hack, but we keep the opentelemetry tracker object at the file
// level so that it doesn't pop in & out of existence as react mounts & unmounts
// components. The right solution is probably for this to live in the js-sdk and have
// the same lifetime as groupcalls themselves.
let groupCallOTelMembership: OTelGroupCallMembership;
let groupCallOTelMembershipGroupCallId: string;
function getParticipants(
groupCall: GroupCall
): Map<RoomMember, Map<string, ParticipantInfo>> {
@@ -124,7 +143,10 @@ function getParticipants(
return participants;
}
export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
export function useGroupCall(
groupCall: GroupCall,
client: MatrixClient
): UseGroupCallReturnType {
const [
{
state,
@@ -158,6 +180,19 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
hasLocalParticipant: false,
});
if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) {
if (groupCallOTelMembership) groupCallOTelMembership.dispose();
// If the user disables analytics, this will stay around until they leave the call
// so analytics will be disabled once they leave.
if (ElementCallOpenTelemetry.instance) {
groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client);
groupCallOTelMembershipGroupCallId = groupCall.groupCallId;
} else {
groupCallOTelMembership = undefined;
}
}
const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer(
(state: Set<string>, newVal: string) => {
return new Set(state).add(newVal);
@@ -175,6 +210,11 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
[]
);
const leaveCall = useCallback(() => {
groupCallOTelMembership?.onLeaveCall();
groupCall.leave();
}, [groupCall]);
useEffect(() => {
// disable the media action keys, otherwise audio elements get paused when
// the user presses media keys or unplugs headphones, etc.
@@ -305,6 +345,24 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
}
}
function onConnectionStatsReport(
report: GroupCallStatsReport<ConnectionStatsReport>
): void {
groupCallOTelMembership?.onConnectionStatsReport(report);
}
function onByteSentStatsReport(
report: GroupCallStatsReport<ByteSentStatsReport>
): void {
groupCallOTelMembership?.onByteSentStatsReport(report);
}
function onSummaryStatsReport(
report: GroupCallStatsReport<SummaryStatsReport>
): void {
groupCallOTelMembership?.onSummaryStatsReport(report);
}
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
groupCall.on(
@@ -320,6 +378,19 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
groupCall.on(GroupCallEvent.Error, onError);
groupCall.on(
GroupCallStatsReportEvent.ConnectionStats,
onConnectionStatsReport
);
groupCall.on(
GroupCallStatsReportEvent.ByteSentStats,
onByteSentStatsReport
);
groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport);
groupCall.room.currentState.on(
RoomStateEvent.Update,
checkForParallelCalls
);
updateState({
error: null,
@@ -367,12 +438,28 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
onParticipantsChanged
);
groupCall.removeListener(GroupCallEvent.Error, onError);
groupCall.leave();
groupCall.removeListener(
GroupCallStatsReportEvent.ConnectionStats,
onConnectionStatsReport
);
groupCall.removeListener(
GroupCallStatsReportEvent.ByteSentStats,
onByteSentStatsReport
);
groupCall.removeListener(
GroupCallStatsReportEvent.SummaryStats,
onSummaryStatsReport
);
groupCall.room.currentState.off(
RoomStateEvent.Update,
checkForParallelCalls
);
leaveCall();
};
}, [groupCall, updateState]);
}, [groupCall, updateState, leaveCall]);
usePageUnload(() => {
groupCall.leave();
leaveCall();
});
const initLocalCallFeed = useCallback(
@@ -391,17 +478,21 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
// This must be called before we start trying to join the call, as we need to
// have started tracking by the time calls start getting created.
groupCallOTelMembership?.onJoinCall();
groupCall.enter().catch((error) => {
console.error(error);
updateState({ error });
});
}, [groupCall, updateState]);
const leave = useCallback(() => groupCall.leave(), [groupCall]);
const toggleLocalVideoMuted = useCallback(() => {
const toggleToMute = !groupCall.isLocalVideoMuted();
groupCall.setLocalVideoMuted(toggleToMute);
groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute);
// TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter?
PosthogAnalytics.instance.eventMuteCamera.track(
toggleToMute,
groupCall.groupCallId
@@ -411,6 +502,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
const setMicrophoneMuted = useCallback(
(setMuted) => {
groupCall.setMicrophoneMuted(setMuted);
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
PosthogAnalytics.instance.eventMuteMicrophone.track(
setMuted,
groupCall.groupCallId
@@ -421,10 +513,13 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
const toggleMicrophoneMuted = useCallback(() => {
const toggleToMute = !groupCall.isMicrophoneMuted();
groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute);
setMicrophoneMuted(toggleToMute);
}, [groupCall, setMicrophoneMuted]);
const toggleScreensharing = useCallback(async () => {
groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing);
if (!groupCall.isScreensharing()) {
// toggling on
updateState({ requestingScreenshare: true });
@@ -525,7 +620,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
error,
initLocalCallFeed,
enter,
leave,
leave: leaveCall,
toggleLocalVideoMuted,
toggleMicrophoneMuted,
toggleScreensharing,
@@ -537,5 +632,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
participants,
hasLocalParticipant,
unencryptedEventsFromUsers,
otelGroupCallMembership: groupCallOTelMembership,
};
}

View File

@@ -32,6 +32,8 @@ import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
import { translatedError } from "../TranslatedError";
import { widget } from "../widget";
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
export interface GroupCallLoadState {
loading: boolean;
error?: Error;
@@ -94,10 +96,13 @@ export const useLoadGroupCall = (
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
const room = await fetchOrCreateRoom();
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
const groupCall = client.getGroupCallForRoom(room.roomId);
let groupCall = client.getGroupCallForRoom(room.roomId);
logger.debug("Got group call", groupCall?.groupCallId);
if (groupCall) return groupCall;
if (groupCall) {
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
return groupCall;
}
if (
!widget &&
@@ -112,12 +117,14 @@ export const useLoadGroupCall = (
createPtt ? "PTT" : "video"
} call`
);
return await client.createGroupCall(
groupCall = await client.createGroupCall(
room.roomId,
createPtt ? GroupCallType.Voice : GroupCallType.Video,
createPtt,
GroupCallIntent.Room
);
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
return groupCall;
}
// We don't have permission to create the call, so all we can do is wait
@@ -126,6 +133,7 @@ export const useLoadGroupCall = (
const onGroupCallIncoming = (groupCall: GroupCall) => {
if (groupCall?.room.roomId === room.roomId) {
clearTimeout(timeout);
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
client.off(
GroupCallEventHandlerEvent.Incoming,
onGroupCallIncoming

View File

@@ -20,7 +20,7 @@ limitations under the License.
}
.tabContainer {
margin: 27px 16px;
padding: 27px 20px;
}
.fieldRowText {
@@ -33,5 +33,5 @@ The "Developer" item in the tab bar can be toggled.
Without a defined width activating the developer tab makes the tab container jump to the right.
*/
.tabLabel {
width: 80px;
min-width: 80px;
}

View File

@@ -16,7 +16,7 @@ limitations under the License.
import React from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
@@ -28,19 +28,17 @@ import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
import { SelectInput } from "../input/SelectInput";
import { useMediaHandler } from "./useMediaHandler";
import {
useKeyboardShortcuts,
useSpatialAudio,
useShowInspector,
useOptInAnalytics,
canEnableSpatialAudio,
useNewGrid,
useDeveloperSettingsTab,
} from "./useSetting";
import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button";
import { useDownloadDebugLog } from "./submit-rageshake";
import { Body } from "../typography/Typography";
import { optInDescription } from "../analytics/AnalyticsOptInDescription";
import { Body, Caption } from "../typography/Typography";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
interface Props {
isOpen: boolean;
@@ -66,11 +64,21 @@ export const SettingsModal = (props: Props) => {
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const [developerSettingsTab, setDeveloperSettingsTab] =
useDeveloperSettingsTab();
const [keyboardShortcuts, setKeyboardShortcuts] = useKeyboardShortcuts();
const [newGrid, setNewGrid] = useNewGrid();
const downloadDebugLog = useDownloadDebugLog();
const optInDescription = (
<Caption>
<Trans>
<AnalyticsNotice />
<br />
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.
</Trans>
</Caption>
);
return (
<Modal
title={t("Settings")}
@@ -122,16 +130,16 @@ export const SettingsModal = (props: Props) => {
label={t("Spatial audio")}
type="checkbox"
checked={spatialAudio}
disabled={!canEnableSpatialAudio()}
disabled={setSpatialAudio === null}
description={
canEnableSpatialAudio()
? t(
setSpatialAudio === null
? t("This feature is only supported on Firefox.")
: t(
"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.)"
)
: t("This feature is only supported on Firefox.")
}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSpatialAudio(event.target.checked)
setSpatialAudio!(event.target.checked)
}
/>
</FieldRow>
@@ -166,28 +174,13 @@ export const SettingsModal = (props: Props) => {
</>
}
>
<h4>Keyboard</h4>
<FieldRow>
<InputField
id="keyboardShortcuts"
label={t("Single-key keyboard shortcuts")}
type="checkbox"
checked={keyboardShortcuts}
description={t(
"Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic."
)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setKeyboardShortcuts(event.target.checked)
}
/>
</FieldRow>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription()}
description={optInDescription}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}

View File

@@ -25,6 +25,14 @@ import { useClient } from "../ClientContext";
import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal";
import { Config } from "../config/Config";
import { ElementCallOpenTelemetry } from "../otel/otel";
const gzip = (text: string): Blob => {
// encode as UTF-8
const buf = new TextEncoder().encode(text);
// compress
return new Blob([pako.gzip(buf)]);
};
interface RageShakeSubmitOptions {
sendLogs: boolean;
@@ -235,14 +243,15 @@ export function useSubmitRageshake(): {
const logs = await getLogsForReport();
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);
// compress
buf = pako.gzip(buf);
body.append("compressed-log", new Blob([buf]), entry.id);
body.append("compressed-log", gzip(entry.lines), entry.id);
}
body.append(
"file",
gzip(ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump()),
"traces.json.gz"
);
if (inspectorState) {
body.append(
"file",

View File

@@ -17,6 +17,11 @@ limitations under the License.
import { EventEmitter } from "events";
import { useMemo, useState, useEffect, useCallback } from "react";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
type Setting<T> = [T, (value: T) => void];
type DisableableSetting<T> = [T, ((value: T) => void) | null];
// Bus to notify other useSetting consumers when a setting is changed
export const settingsBus = new EventEmitter();
@@ -24,10 +29,7 @@ const getSettingKey = (name: string): string => {
return `matrix-setting-${name}`;
};
// Like useState, but reads from and persists the value to localStorage
const useSetting = <T>(
name: string,
defaultValue: T
): [T, (value: T) => void] => {
const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
const key = useMemo(() => getSettingKey(name), [name]);
const [value, setValue] = useState<T>(() => {
@@ -65,7 +67,7 @@ export const setSetting = <T>(name: string, newValue: T) => {
settingsBus.emit(name, newValue);
};
export const canEnableSpatialAudio = () => {
const canEnableSpatialAudio = () => {
const { userAgent } = navigator;
// Spatial audio means routing audio through audio contexts. On Chrome,
// this bypasses the AEC processor and so breaks echo cancellation.
@@ -79,17 +81,24 @@ export const canEnableSpatialAudio = () => {
return userAgent.includes("Firefox");
};
export const useSpatialAudio = (): [boolean, (val: boolean) => void] => {
export const useSpatialAudio = (): DisableableSetting<boolean> => {
const settingVal = useSetting("spatial-audio", false);
if (canEnableSpatialAudio()) return settingVal;
return [false, (_: boolean) => {}];
return [false, null];
};
export const useShowInspector = () => useSetting("show-inspector", false);
export const useOptInAnalytics = () => useSetting("opt-in-analytics", false);
export const useKeyboardShortcuts = () =>
useSetting("keyboard-shortcuts", true);
// null = undecided
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
const settingVal = useSetting<boolean | null>("opt-in-analytics", null);
if (PosthogAnalytics.instance.isEnabled()) return settingVal;
return [false, null];
};
export const useNewGrid = () => useSetting("new-grid", false);
export const useDeveloperSettingsTab = () =>
useSetting("developer-settings-tab", false);

View File

@@ -88,7 +88,9 @@ limitations under the License.
.tabContainer {
width: 100%;
flex-direction: row;
margin: 27px 16px;
padding: 27px 20px;
box-sizing: border-box;
overflow: hidden;
}
.tabList {

View File

@@ -14,47 +14,55 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useState } from "react";
import { RefObject, useCallback, useRef } from "react";
import { getSetting } from "./settings/useSetting";
import { useEventTarget } from "./useEvents";
/**
* Determines whether focus is in the same part of the tree as the given
* element (specifically, if an ancestor or descendant of it is focused).
*/
const mayReceiveKeyEvents = (e: HTMLElement): boolean => {
const focusedElement = document.activeElement;
return (
focusedElement !== null &&
(focusedElement.contains(e) || e.contains(focusedElement))
);
};
export function useCallViewKeyboardShortcuts(
enabled: boolean,
focusElement: RefObject<HTMLElement | null>,
toggleMicrophoneMuted: () => void,
toggleLocalVideoMuted: () => void,
setMicrophoneMuted: (muted: boolean) => void
) {
const [spacebarHeld, setSpacebarHeld] = useState(false);
const spacebarHeld = useRef(false);
// These event handlers are set on the window because we want users to be able
// to trigger them without going to the trouble of focusing something
useEventTarget(
window,
"keydown",
useCallback(
(event: KeyboardEvent) => {
if (!enabled) return;
// Check if keyboard shortcuts are enabled
const keyboardShortcuts = getSetting("keyboard-shortcuts", true);
if (!keyboardShortcuts) {
return;
}
if (focusElement.current === null) return;
if (!mayReceiveKeyEvents(focusElement.current)) return;
if (event.key === "m") {
toggleMicrophoneMuted();
} else if (event.key == "v") {
toggleLocalVideoMuted();
} else if (event.key === " " && !spacebarHeld) {
setSpacebarHeld(true);
} else if (event.key === " " && !spacebarHeld.current) {
spacebarHeld.current = true;
setMicrophoneMuted(false);
}
},
[
enabled,
spacebarHeld,
focusElement,
toggleLocalVideoMuted,
toggleMicrophoneMuted,
setMicrophoneMuted,
setSpacebarHeld,
]
)
);
@@ -64,19 +72,15 @@ export function useCallViewKeyboardShortcuts(
"keyup",
useCallback(
(event: KeyboardEvent) => {
if (!enabled) return;
// Check if keyboard shortcuts are enabled
const keyboardShortcuts = getSetting("keyboard-shortcuts", true);
if (!keyboardShortcuts) {
return;
}
if (focusElement.current === null) return;
if (!mayReceiveKeyEvents(focusElement.current)) return;
if (event.key === " ") {
setSpacebarHeld(false);
spacebarHeld.current = false;
setMicrophoneMuted(true);
}
},
[enabled, setMicrophoneMuted, setSpacebarHeld]
[focusElement, setMicrophoneMuted]
)
);
@@ -84,10 +88,10 @@ export function useCallViewKeyboardShortcuts(
window,
"blur",
useCallback(() => {
if (spacebarHeld) {
setSpacebarHeld(false);
if (spacebarHeld.current) {
spacebarHeld.current = false;
setMicrophoneMuted(true);
}
}, [setMicrophoneMuted, setSpacebarHeld, spacebarHeld])
}, [setMicrophoneMuted, spacebarHeld])
);
}

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { TileDescriptor } from "./TileDescriptor";
import { useCallFeed } from "./useCallFeed";
import { useMediaStream } from "./useMediaStream";
@@ -23,6 +24,7 @@ import { useMediaStream } from "./useMediaStream";
interface Props {
tileDescriptor: TileDescriptor;
audioOutput: string;
otelGroupCallMembership?: OTelGroupCallMembership;
}
// Renders and <audio> element on the page playing the given stream
@@ -30,8 +32,12 @@ interface Props {
export const AudioSink: React.FC<Props> = ({
tileDescriptor,
audioOutput,
otelGroupCallMembership,
}: Props) => {
const { localVolume, stream } = useCallFeed(tileDescriptor.callFeed);
const { localVolume, stream } = useCallFeed(
tileDescriptor.callFeed,
otelGroupCallMembership
);
const audioElementRef = useMediaStream(
stream,

View File

@@ -245,6 +245,7 @@ export const NewVideoGrid: FC<Props> = ({
opacity: 0,
scale: 0,
shadow: 1,
shadowSpread: 0,
zIndex: 1,
x,
y,

View File

@@ -23,7 +23,7 @@ import {
useSprings,
} from "@react-spring/web";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
import styles from "./VideoGrid.module.css";
@@ -51,6 +51,7 @@ export interface TileSpring {
opacity: number;
scale: number;
shadow: number;
shadowSpread: number;
zIndex: number;
x: number;
y: number;
@@ -172,8 +173,16 @@ function getOneOnOneLayoutTilePositions(
const gridAspectRatio = gridWidth / gridHeight;
const smallPip = gridAspectRatio < 1 || gridWidth < 700;
const pipWidth = smallPip ? 114 : 230;
const pipHeight = smallPip ? 163 : 155;
const maxPipWidth = smallPip ? 114 : 230;
const maxPipHeight = smallPip ? 163 : 155;
// Cap the PiP size at 1/3 the remote tile size, preserving aspect ratio
const pipScaleFactor = Math.min(
1,
remotePosition.width / 3 / maxPipWidth,
remotePosition.height / 3 / maxPipHeight
);
const pipWidth = maxPipWidth * pipScaleFactor;
const pipHeight = maxPipHeight * pipScaleFactor;
const pipGap = getPipGap(gridAspectRatio, gridWidth);
const pipMinX = remotePosition.x + pipGap;
@@ -754,7 +763,13 @@ export function VideoGrid({
const lastLayoutRef = useRef<Layout>(layout);
const isMounted = useIsMounted();
const [gridRef, gridBounds] = useMeasure({ polyfill: ResizeObserver });
// The 'polyfill' argument to useMeasure is not a polyfill at all but is the impl that is always used
// if passed, whether the browser has native support or not, so pass in either the browser native
// version or the ponyfill (yes, pony) because Juggle's resizeobserver ponyfill is being weirdly
// buggy for me on my dev env my never updating the size until the window resizes.
const [gridRef, gridBounds] = useMeasure({
polyfill: window.ResizeObserver ?? JuggleResizeObserver,
});
useEffect(() => {
setTileState(({ tiles, ...rest }) => {
@@ -886,6 +901,8 @@ export function VideoGrid({
// Whether the tile positions were valid at the time of the previous
// animation
const tilePositionsWereValid = tilePositionsValid.current;
const oneOnOneLayout =
tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused);
return (tileIndex: number) => {
const tile = tiles[tileIndex];
@@ -905,12 +922,14 @@ export function VideoGrid({
opacity: 1,
zIndex: 2,
shadow: 15,
shadowSpread: 0,
immediate: (key: string) =>
disableAnimations ||
key === "zIndex" ||
key === "x" ||
key === "y" ||
key === "shadow",
key === "shadow" ||
key === "shadowSpread",
from: {
shadow: 0,
scale: 0,
@@ -968,10 +987,14 @@ export function VideoGrid({
opacity: remove ? 0 : 1,
zIndex: tilePosition.zIndex,
shadow: 1,
shadowSpread: oneOnOneLayout && tile.item.isLocal ? 1 : 0,
from,
reset,
immediate: (key: string) =>
disableAnimations || key === "zIndex" || key === "shadow",
disableAnimations ||
key === "zIndex" ||
key === "shadow" ||
key === "shadowSpread",
// If we just stopped dragging a tile, give it time for the
// animation to settle before pushing its z-index back down
delay: (key: string) => (key === "zIndex" ? 500 : 0),

View File

@@ -20,7 +20,10 @@ limitations under the License.
top: 0;
width: var(--tileWidth);
height: var(--tileHeight);
border-radius: 8px;
--tileRadius: 8px;
border-radius: var(--tileRadius);
box-shadow: rgba(0, 0, 0, 0.5) 0px var(--tileShadow)
calc(2 * var(--tileShadow)) var(--tileShadowSpread);
overflow: hidden;
cursor: pointer;
@@ -44,15 +47,21 @@ limitations under the License.
transform: scaleX(-1);
}
.videoTile.speaking::after {
.videoTile::after {
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
content: "";
border-radius: 20px;
border-radius: var(--tileRadius);
box-shadow: inset 0 0 0 4px var(--accent) !important;
opacity: 0;
transition: opacity ease 0.15s;
}
.videoTile.speaking::after {
opacity: 1;
}
.videoTile.maximised {
@@ -82,6 +91,12 @@ limitations under the License.
z-index: 1;
}
.infoBubble > svg {
height: 16px;
width: 16px;
margin-right: 4px;
}
.toolbar {
position: absolute;
top: 0;
@@ -125,10 +140,6 @@ limitations under the License.
bottom: 16px;
}
.memberName > * {
margin-right: 6px;
}
.memberName > :last-child {
margin-right: 0px;
}
@@ -174,6 +185,6 @@ limitations under the License.
@media (min-width: 800px) {
.videoTile {
border-radius: 20px;
--tileRadius: 20px;
}
}

View File

@@ -20,8 +20,8 @@ import classNames from "classnames";
import { useTranslation } from "react-i18next";
import styles from "./VideoTile.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
import { AudioButton, FullscreenButton } from "../button/Button";
import { ConnectionState } from "../room/useGroupCall";
@@ -47,6 +47,7 @@ interface Props {
opacity?: SpringValue<number>;
scale?: SpringValue<number>;
shadow?: SpringValue<number>;
shadowSpread?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;
@@ -79,6 +80,7 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
opacity,
scale,
shadow,
shadowSpread,
zIndex,
x,
y,
@@ -141,9 +143,6 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
style={{
opacity,
scale,
boxShadow: shadow?.to(
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
),
zIndex,
x,
y,
@@ -152,8 +151,11 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
// but React's types say no
"--tileWidth": width?.to((w) => `${w}px`),
"--tileHeight": height?.to((h) => `${h}px`),
"--tileShadow": shadow?.to((s) => `${s}px`),
"--tileShadowSpread": shadowSpread?.to((s) => `${s}px`),
}}
ref={ref as ForwardedRef<HTMLDivElement>}
data-testid="videoTile"
{...rest}
>
{toolbarButtons.length > 0 && !maximised && (
@@ -177,13 +179,19 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
Mute state is currently sent over to-device messages, which
aren't quite real-time, so this is an important kludge to make
sure no one appears muted when they've clearly begun talking. */
audioMuted && !videoMuted && !speaking && <MicMutedIcon />
speaking || !audioMuted ? <MicIcon /> : <MicMutedIcon />
}
{videoMuted && <VideoMutedIcon />}
<span title={caption}>{caption}</span>
<span data-testid="videoTile_caption" title={caption}>
{caption}
</span>
</div>
))}
<video ref={mediaRef} playsInline disablePictureInPicture />
<video
data-testid="videoTile_video"
ref={mediaRef}
playsInline
disablePictureInPicture
/>
</animated.div>
);
}

View File

@@ -47,6 +47,7 @@ interface Props {
opacity?: SpringValue<number>;
scale?: SpringValue<number>;
shadow?: SpringValue<number>;
shadowSpread?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;

View File

@@ -18,6 +18,8 @@ import { useState, useEffect } from "react";
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
interface CallFeedState {
callFeed: CallFeed | undefined;
isLocal: boolean;
@@ -46,40 +48,46 @@ function getCallFeedState(callFeed: CallFeed | undefined): CallFeedState {
};
}
export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
export function useCallFeed(
callFeed: CallFeed | undefined,
otelGroupCallMembership?: OTelGroupCallMembership
): CallFeedState {
const [state, setState] = useState<CallFeedState>(() =>
getCallFeedState(callFeed)
);
useEffect(() => {
function onSpeaking(speaking: boolean) {
setState((prevState) => ({ ...prevState, speaking }));
}
function onMuteStateChanged(audioMuted: boolean, videoMuted: boolean) {
setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
}
function onLocalVolumeChanged(localVolume: number) {
setState((prevState) => ({ ...prevState, localVolume }));
}
function onUpdateCallFeed() {
setState(getCallFeedState(callFeed));
}
onUpdateCallFeed();
if (callFeed) {
const onSpeaking = (speaking: boolean) => {
otelGroupCallMembership?.onSpeaking(
callFeed.getMember()!,
callFeed.deviceId!,
speaking
);
setState((prevState) => ({ ...prevState, speaking }));
};
const onMuteStateChanged = (audioMuted: boolean, videoMuted: boolean) => {
setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
};
const onLocalVolumeChanged = (localVolume: number) => {
setState((prevState) => ({ ...prevState, localVolume }));
};
callFeed.on(CallFeedEvent.Speaking, onSpeaking);
callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
callFeed.on(CallFeedEvent.LocalVolumeChanged, onLocalVolumeChanged);
callFeed.on(CallFeedEvent.NewStream, onUpdateCallFeed);
callFeed.on(CallFeedEvent.Disposed, onUpdateCallFeed);
}
onUpdateCallFeed();
return () => {
if (callFeed) {
return () => {
callFeed.removeListener(CallFeedEvent.Speaking, onSpeaking);
callFeed.removeListener(
CallFeedEvent.MuteStateChanged,
@@ -90,9 +98,9 @@ export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
onLocalVolumeChanged
);
callFeed.removeListener(CallFeedEvent.NewStream, onUpdateCallFeed);
}
};
}, [callFeed]);
};
}
}, [callFeed, otelGroupCallMembership]);
return state;
}

View File

@@ -0,0 +1,243 @@
import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall";
import {
AudioConcealment,
ConnectionStatsReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { ObjectFlattener } from "../../src/otel/ObjectFlattener";
/*
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.
*/
describe("ObjectFlattener", () => {
const noConcealment: AudioConcealment = {
concealedAudio: 0,
totalAudioDuration: 0,
};
const statsReport: GroupCallStatsReport<ConnectionStatsReport> = {
report: {
bandwidth: { upload: 426, download: 0 },
bitrate: {
upload: 426,
download: 0,
audio: {
upload: 124,
download: 0,
},
video: {
upload: 302,
download: 0,
},
},
packetLoss: {
total: 0,
download: 0,
upload: 0,
},
framerate: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", 0],
["LOCAL_VIDEO_TRACK_ID", 30],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", 0],
["REMOTE_VIDEO_TRACK_ID", 60],
]),
},
resolution: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
]),
},
jitter: new Map([
["REMOTE_AUDIO_TRACK_ID", 2],
["REMOTE_VIDEO_TRACK_ID", 50],
]),
codec: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", "opus"],
["LOCAL_VIDEO_TRACK_ID", "v8"],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", "opus"],
["REMOTE_VIDEO_TRACK_ID", "v9"],
]),
},
transport: [
{
ip: "ff11::5fa:abcd:999c:c5c5:50000",
type: "udp",
localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
isFocus: true,
localCandidateType: "host",
remoteCandidateType: "host",
networkType: "ethernet",
rtt: NaN,
},
{
ip: "10.10.10.2:22222",
type: "tcp",
localIp: "10.10.10.100:33333",
isFocus: true,
localCandidateType: "srfx",
remoteCandidateType: "srfx",
networkType: "ethernet",
rtt: null,
},
],
audioConcealment: new Map([
["REMOTE_AUDIO_TRACK_ID", noConcealment],
["REMOTE_VIDEO_TRACK_ID", noConcealment],
]),
totalAudioConcealment: noConcealment,
},
};
describe("on flattenObjectRecursive", () => {
it("should flatter an Map object", () => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report.resolution,
flatObject,
"matrix.stats.conn.resolution.",
0
);
expect(flatObject).toEqual({
"matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1,
"matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1,
"matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
"matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
"matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1,
"matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1,
"matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
"matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
});
});
it("should flatter an Array object", () => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report.transport,
flatObject,
"matrix.stats.conn.transport.",
0
);
expect(flatObject).toEqual({
"matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000",
"matrix.stats.conn.transport.0.type": "udp",
"matrix.stats.conn.transport.0.localIp":
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
"matrix.stats.conn.transport.0.isFocus": true,
"matrix.stats.conn.transport.0.localCandidateType": "host",
"matrix.stats.conn.transport.0.remoteCandidateType": "host",
"matrix.stats.conn.transport.0.networkType": "ethernet",
"matrix.stats.conn.transport.0.rtt": "NaN",
"matrix.stats.conn.transport.1.ip": "10.10.10.2:22222",
"matrix.stats.conn.transport.1.type": "tcp",
"matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333",
"matrix.stats.conn.transport.1.isFocus": true,
"matrix.stats.conn.transport.1.localCandidateType": "srfx",
"matrix.stats.conn.transport.1.remoteCandidateType": "srfx",
"matrix.stats.conn.transport.1.networkType": "ethernet",
"matrix.stats.conn.transport.1.rtt": "null",
});
});
});
describe("on flattenConnectionStatsReportObject", () => {
it("should flatten a Report to otel Attributes Object", () => {
expect(
ObjectFlattener.flattenConnectionStatsReportObject(statsReport)
).toEqual({
"matrix.stats.conn.bandwidth.download": 0,
"matrix.stats.conn.bandwidth.upload": 426,
"matrix.stats.conn.bitrate.audio.download": 0,
"matrix.stats.conn.bitrate.audio.upload": 124,
"matrix.stats.conn.bitrate.download": 0,
"matrix.stats.conn.bitrate.upload": 426,
"matrix.stats.conn.bitrate.video.download": 0,
"matrix.stats.conn.bitrate.video.upload": 302,
"matrix.stats.conn.codec.local.LOCAL_AUDIO_TRACK_ID": "opus",
"matrix.stats.conn.codec.local.LOCAL_VIDEO_TRACK_ID": "v8",
"matrix.stats.conn.codec.remote.REMOTE_AUDIO_TRACK_ID": "opus",
"matrix.stats.conn.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9",
"matrix.stats.conn.framerate.local.LOCAL_AUDIO_TRACK_ID": 0,
"matrix.stats.conn.framerate.local.LOCAL_VIDEO_TRACK_ID": 30,
"matrix.stats.conn.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0,
"matrix.stats.conn.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60,
"matrix.stats.conn.jitter.REMOTE_AUDIO_TRACK_ID": 2,
"matrix.stats.conn.jitter.REMOTE_VIDEO_TRACK_ID": 50,
"matrix.stats.conn.packetLoss.download": 0,
"matrix.stats.conn.packetLoss.total": 0,
"matrix.stats.conn.packetLoss.upload": 0,
"matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1,
"matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1,
"matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
"matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
"matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1,
"matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1,
"matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
"matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
"matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000",
"matrix.stats.conn.transport.0.type": "udp",
"matrix.stats.conn.transport.0.localIp":
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
"matrix.stats.conn.transport.0.isFocus": true,
"matrix.stats.conn.transport.0.localCandidateType": "host",
"matrix.stats.conn.transport.0.remoteCandidateType": "host",
"matrix.stats.conn.transport.0.networkType": "ethernet",
"matrix.stats.conn.transport.0.rtt": "NaN",
"matrix.stats.conn.transport.1.ip": "10.10.10.2:22222",
"matrix.stats.conn.transport.1.type": "tcp",
"matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333",
"matrix.stats.conn.transport.1.isFocus": true,
"matrix.stats.conn.transport.1.localCandidateType": "srfx",
"matrix.stats.conn.transport.1.remoteCandidateType": "srfx",
"matrix.stats.conn.transport.1.networkType": "ethernet",
"matrix.stats.conn.transport.1.rtt": "null",
"matrix.stats.conn.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0,
"matrix.stats.conn.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0,
"matrix.stats.conn.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0,
"matrix.stats.conn.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0,
"matrix.stats.conn.totalAudioConcealment.concealedAudio": 0,
"matrix.stats.conn.totalAudioConcealment.totalAudioDuration": 0,
});
});
});
describe("on flattenByteSendStatsReportObject", () => {
const byteSent = {
report: new Map([
["4aa92608-04c6-428e-8312-93e17602a959", 132093],
["a08e4237-ee30-4015-a932-b676aec894b1", 913448],
]),
};
it("should flatten a Report to otel Attributes Object", () => {
expect(
ObjectFlattener.flattenByteSentStatsReportObject(byteSent)
).toEqual({
"matrix.stats.bytesSent.4aa92608-04c6-428e-8312-93e17602a959": 132093,
"matrix.stats.bytesSent.a08e4237-ee30-4015-a932-b676aec894b1": 913448,
});
});
});
});

View File

@@ -0,0 +1,171 @@
/*
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 { Mocked, mocked } from "jest-mock";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
const withFakeTimers = (continuation: () => void) => {
jest.useFakeTimers();
try {
continuation();
} finally {
jest.useRealTimers();
}
};
const withMockedPosthog = (
continuation: (posthog: Mocked<PosthogAnalytics>) => void
) => {
const posthog = mocked({
trackEvent: jest.fn(),
} as unknown as PosthogAnalytics);
const instanceSpy = jest
.spyOn(PosthogAnalytics, "instance", "get")
.mockReturnValue(posthog);
try {
continuation(posthog);
} finally {
instanceSpy.mockRestore();
}
};
const mockRoomState = (
groupCallMemberContents: Record<string, unknown>[]
): RoomState => {
const stateEvents = groupCallMemberContents.map((content) => ({
getContent: () => content,
}));
return { getStateEvents: () => stateEvents } as unknown as RoomState;
};
test("checkForParallelCalls does nothing if all participants are in the same call", () => {
withFakeTimers(() => {
withMockedPosthog((posthog) => {
const roomState = mockRoomState([
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Call",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
{
"m.call_id": null, // invalid
"m.devices": [
{
device_id: "Element Android",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
null, // invalid
],
},
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Desktop",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
],
},
]);
checkForParallelCalls(roomState);
expect(posthog.trackEvent).not.toHaveBeenCalled();
});
});
});
test("checkForParallelCalls sends diagnostics to PostHog if there is a split-brain", () => {
withFakeTimers(() => {
withMockedPosthog((posthog) => {
const roomState = mockRoomState([
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Call",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
{
"m.call_id": "2",
"m.devices": [
{
device_id: "Element Android",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
],
},
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Desktop",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
{
"m.call_id": "2",
"m.devices": [
{
device_id: "Element Call",
session_id: "a",
expires_ts: Date.now() - 1000,
},
],
},
],
},
]);
checkForParallelCalls(roomState);
expect(posthog.trackEvent).toHaveBeenCalledWith({
eventName: "ParallelCalls",
participantsPerCall: {
"1": 2,
"2": 1,
},
});
});
});
});

396
yarn.lock
View File

@@ -1209,13 +1209,20 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.18.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.13.9", "@babel/runtime@^7.9.2":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78"
@@ -1821,10 +1828,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5":
version "0.1.0-alpha.5"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658"
integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw==
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.8":
version "0.1.0-alpha.8"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.8.tgz#18dd8e7fb56602d2999d8a502b49e902a2bb3782"
integrity sha512-hdmbbGXKrN6JNo3wdBaR5Zs3lXlzllT3U43ViNTlabB3nKkOZQnEAN/Isv+4EQSgz1+8897veI9Q8sqlQX22oA==
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
version "3.2.14"
@@ -1910,6 +1917,138 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@opentelemetry/api@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8"
integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g==
"@opentelemetry/context-zone-peer-dep@1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-1.9.1.tgz#634b1a25eebc68484d3568865ee5a2321b6b020d"
integrity sha512-4qaNi2noNMlT3DhOzXN4qKDiyZFjowj2vnfdtcAHZUwpIP0MQlpE3JYCr+2w7zKGJDfEOp2hg2A9Dkn8TqvzSw==
"@opentelemetry/context-zone@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone/-/context-zone-1.9.1.tgz#1f1c48fb491283ab32320b3d95e542a3a3e86035"
integrity sha512-Kx2n9ftRokgHUAI6CIxsNepCsEP/fggDBH3GT27GdZkqgPYZqBn+nlTS23dB6etjWcSRd0piJnT3OIEnaxyIGA==
dependencies:
"@opentelemetry/context-zone-peer-dep" "1.9.1"
zone.js "^0.11.0"
"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.8.0":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a"
integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg==
dependencies:
"@opentelemetry/semantic-conventions" "1.9.1"
"@opentelemetry/exporter-jaeger@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-jaeger/-/exporter-jaeger-1.9.1.tgz#941d39c2d425021c734354bbc280a4ae19f95aad"
integrity sha512-6F10NMOtBT3HdxpT0IwYf1BX8RzZB7SpqHTvZsB2vzUvxVAyoLX8+cuo6Ke9sHS9YMqoTA3rER5x9kC6NOxEMQ==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/sdk-trace-base" "1.9.1"
"@opentelemetry/semantic-conventions" "1.9.1"
jaeger-client "^3.15.0"
"@opentelemetry/exporter-trace-otlp-http@^0.35.1":
version "0.35.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.35.1.tgz#9bf988f91fb145b29a051bce8ff5ef85029ca575"
integrity sha512-EJgAsrvscKsqb/GzF1zS74vM+Z/aQRhrFE7hs/1GK1M9bLixaVyMGwg2pxz1wdYdjxS1mqpHMhXU+VvMvFCw1w==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/otlp-exporter-base" "0.35.1"
"@opentelemetry/otlp-transformer" "0.35.1"
"@opentelemetry/resources" "1.9.1"
"@opentelemetry/sdk-trace-base" "1.9.1"
"@opentelemetry/instrumentation-document-load@^0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.31.1.tgz#a535a5d1d71706701d3ff560a700b9dd03e4fb59"
integrity sha512-Ej4EB3m7GXzj4o8zF73amcnqXroN6/QdURjDAOgxN27zvvurR84larzGD7PjqgzzdtV+T7e/0BK07M0I2eA8PQ==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.35.1"
"@opentelemetry/sdk-trace-base" "^1.0.0"
"@opentelemetry/sdk-trace-web" "^1.8.0"
"@opentelemetry/semantic-conventions" "^1.0.0"
"@opentelemetry/instrumentation-user-interaction@^0.32.1":
version "0.32.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-user-interaction/-/instrumentation-user-interaction-0.32.1.tgz#654c0352c2f7d5bb6cc21f07f9ec56f18f2cc854"
integrity sha512-27we7cENzEtO2oCRiEkYG4cFe1v94ybeLvM+5jqNDkZF7UY0GlctCW+jvqf569Z3Gs7yHrakO2sZf4EMEfTFWg==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.35.1"
"@opentelemetry/sdk-trace-web" "^1.8.0"
"@opentelemetry/instrumentation@^0.35.1":
version "0.35.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.35.1.tgz#065bdbc4771137347e648eb4c6c6de6e9e97e4d1"
integrity sha512-EZsvXqxenbRTSNsft6LDcrT4pjAiyZOx3rkDNeqKpwZZe6GmZtsXaZZKuDkJtz9fTjOGjDHjZj9/h80Ya9iIJw==
dependencies:
require-in-the-middle "^5.0.3"
semver "^7.3.2"
shimmer "^1.2.1"
"@opentelemetry/otlp-exporter-base@0.35.1":
version "0.35.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.35.1.tgz#535166608d5d36e6c959b2857d01245ee3a916b1"
integrity sha512-Sc0buJIs8CfUeQCL/12vDDjBREgsqHdjboBa/kPQDgMf008OBJSM02Ijj6T1TH+QVHl/VHBBEVJF+FTf0EH9Vg==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/otlp-transformer@0.35.1":
version "0.35.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.35.1.tgz#d4333b71324b83dbb1b0b3a4cfd769b3e214c6f9"
integrity sha512-c0HXcJ49MKoWSaA49J8PXlVx48CeEFpL0odP6KBkVT+Bw6kAe8JlI3mIezyN05VCDJGtS2I5E6WEsE+DJL1t9A==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/resources" "1.9.1"
"@opentelemetry/sdk-metrics" "1.9.1"
"@opentelemetry/sdk-trace-base" "1.9.1"
"@opentelemetry/resources@1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f"
integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/semantic-conventions" "1.9.1"
"@opentelemetry/sdk-metrics@1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.9.1.tgz#babc162a81df9884c16b1e67c2dd26ab478f3080"
integrity sha512-AyhKDcA8NuV7o1+9KvzRMxNbATJ8AcrutKilJ6hWSo9R5utnzxgffV4y+Hp4mJn84iXxkv+CBb99GOJ2A5OMzA==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/resources" "1.9.1"
lodash.merge "4.6.2"
"@opentelemetry/sdk-trace-base@1.9.1", "@opentelemetry/sdk-trace-base@^1.0.0":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84"
integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/resources" "1.9.1"
"@opentelemetry/semantic-conventions" "1.9.1"
"@opentelemetry/sdk-trace-web@^1.8.0", "@opentelemetry/sdk-trace-web@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.9.1.tgz#9734c62dfb554336779c0eb4f78bb622d8bde988"
integrity sha512-VCnr8IYW1GYonGF8M3nDqUGFjf2jcL3nlhnNyF3PKGw6OI7xNCBR+65IgW5Va7QhDP0D01jRVJ9oNuTshrVewA==
dependencies:
"@opentelemetry/core" "1.9.1"
"@opentelemetry/sdk-trace-base" "1.9.1"
"@opentelemetry/semantic-conventions" "1.9.1"
"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.0":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad"
integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg==
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2"
@@ -2132,51 +2271,51 @@
"@react-aria/utils" "^3.13.1"
clsx "^1.1.1"
"@react-spring/animated@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54"
integrity sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA==
"@react-spring/animated@~9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.2.tgz#0119db8075e91d693ec45c42575541e01b104a70"
integrity sha512-ipvleJ99ipqlnHkz5qhSsgf/ny5aW0ZG8Q+/2Oj9cI7LCc7COdnrSO6V/v8MAX3JOoQNzfz6dye2s5Pt5jGaIA==
dependencies:
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/shared" "~9.7.2"
"@react-spring/types" "~9.7.2"
"@react-spring/core@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.4.5.tgz#4616e1adc18dd10f5731f100ebdbe9518b89ba3c"
integrity sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ==
"@react-spring/core@~9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.2.tgz#804ebadee45a6adff00886454d6f1c5d97ee219d"
integrity sha512-fF512edZT/gKVCA90ZRxfw1DmELeVwiL4OC2J6bMUlNr707C0h4QRoec6DjzG27uLX2MvS1CEatf9KRjwZR9/w==
dependencies:
"@react-spring/animated" "~9.4.5"
"@react-spring/rafz" "~9.4.5"
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/animated" "~9.7.2"
"@react-spring/rafz" "~9.7.2"
"@react-spring/shared" "~9.7.2"
"@react-spring/types" "~9.7.2"
"@react-spring/rafz@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.4.5.tgz#84f809f287f2a66bbfbc66195db340482f886bd7"
integrity sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ==
"@react-spring/rafz@~9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.2.tgz#77e7088c215e05cf893851cd87ceb40d89f2a7d7"
integrity sha512-kDWMYDQto3+flkrX3vy6DU/l9pxQ4TVW91DglQEc11iDc7shF4+WVDRJvOVLX+xoMP7zyag1dMvlIgvQ+dvA/A==
"@react-spring/shared@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.4.5.tgz#4c3ad817bca547984fb1539204d752a412a6d829"
integrity sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA==
"@react-spring/shared@~9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.2.tgz#b8485617bdcc9f6348b245922051fb534e07c566"
integrity sha512-6U9qkno+9DxlH5nSltnPs+kU6tYKf0bPLURX2te13aGel8YqgcpFYp5Av8DcN2x3sukinAsmzHUS/FRsdZMMBA==
dependencies:
"@react-spring/rafz" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/rafz" "~9.7.2"
"@react-spring/types" "~9.7.2"
"@react-spring/types@~9.4.5":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.4.5.tgz#9c71e5ff866b5484a7ef3db822bf6c10e77bdd8c"
integrity sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg==
"@react-spring/types@~9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.2.tgz#e04dd72755d88b0e3163ba143ecd8ba78b68a5b0"
integrity sha512-GEflx2Ex/TKVMHq5g5MxQDNNPNhqg+4Db9m7+vGTm8ttZiyga7YQUF24shgRNebKIjahqCuei16SZga8h1pe4g==
"@react-spring/web@^9.4.4":
version "9.4.5"
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.4.5.tgz#b92f05b87cdc0963a59ee149e677dcaff09f680e"
integrity sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA==
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.2.tgz#76e53dd24033764c3062f9927f88b0f3194688d4"
integrity sha512-7qNc7/5KShu2D05x7o2Ols2nUE7mCKfKLaY2Ix70xPMfTle1sZisoQMBFgV9w/fSLZlHZHV9P0uWJqEXQnbV4Q==
dependencies:
"@react-spring/animated" "~9.4.5"
"@react-spring/core" "~9.4.5"
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@react-spring/animated" "~9.7.2"
"@react-spring/core" "~9.7.2"
"@react-spring/shared" "~9.7.2"
"@react-spring/types" "~9.7.2"
"@react-stately/collections@^3.3.4", "@react-stately/collections@^3.4.1":
version "3.4.1"
@@ -4164,6 +4303,11 @@ ansi-align@^3.0.0:
dependencies:
string-width "^4.1.0"
ansi-color@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a"
integrity sha512-bF6xLaZBLpOQzgYUtYEhJx090nPSZk1BQ/q2oyBK9aMMcJHzx9uXGCjI2Y+LebsN4Jwoykr0V9whbPiogdyHoQ==
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -4985,6 +5129,16 @@ buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
bufrw@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa"
integrity sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ==
dependencies:
ansi-color "^0.2.1"
error "^7.0.0"
hexer "^1.5.0"
xtend "^4.0.0"
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -5132,15 +5286,10 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359:
version "1.0.30001363"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15"
integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg==
caniuse-lite@^1.0.30001400:
version "1.0.30001425"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001425.tgz#52917791a453eb3265143d2cd08d80629e82c735"
integrity sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw==
caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359, caniuse-lite@^1.0.30001400:
version "1.0.30001460"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz"
integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.4.0"
@@ -5584,7 +5733,12 @@ content-disposition@0.5.4:
dependencies:
safe-buffer "5.2.1"
content-type@^1.0.4, content-type@~1.0.4:
content-type@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
@@ -6901,6 +7055,21 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.3.4"
error@7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
integrity sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw==
dependencies:
string-template "~0.2.1"
xtend "~4.0.0"
error@^7.0.0:
version "7.2.1"
resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894"
integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==
dependencies:
string-template "~0.2.1"
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
@@ -8581,6 +8750,16 @@ heimdalljs@^0.2.6:
dependencies:
rsvp "~3.2.1"
hexer@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653"
integrity sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg==
dependencies:
ansi-color "^0.2.1"
minimist "^1.1.0"
process "^0.10.0"
xtend "^4.0.0"
highlight.js@^10.4.1, highlight.js@~10.7.0:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
@@ -9468,6 +9647,17 @@ iterate-value@^1.0.2:
es-get-iterator "^1.0.2"
iterate-iterator "^1.0.1"
jaeger-client@^3.15.0:
version "3.19.0"
resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.19.0.tgz#9b5bd818ebd24e818616ee0f5cffe1722a53ae6e"
integrity sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw==
dependencies:
node-int64 "^0.4.0"
opentracing "^0.14.4"
thriftrw "^3.5.0"
uuid "^8.3.2"
xorshift "^1.1.1"
jest-changed-files@^29.2.0:
version "29.2.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289"
@@ -10211,7 +10401,7 @@ lodash.flow@^3.3.0:
resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a"
integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==
lodash.merge@^4.6.2:
lodash.merge@4.6.2, lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
@@ -10235,9 +10425,14 @@ log-symbols@^4.1.0:
is-unicode-supported "^0.1.0"
loglevel@^1.7.1:
version "1.8.0"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==
version "1.8.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4"
integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==
long@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f"
integrity sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
@@ -10362,12 +10557,12 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#da03c3b529576a8fcde6f2c9a171fa6cca012830":
version "24.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/da03c3b529576a8fcde6f2c9a171fa6cca012830"
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac":
version "25.1.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5"
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.8"
another-json "^0.2.0"
bs58 "^5.0.0"
content-type "^1.0.4"
@@ -10380,9 +10575,9 @@ matrix-events-sdk@0.0.1:
uuid "9"
matrix-widget-api@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5"
integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q==
version "1.4.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz#e426ec16a013897f3a4a9c2bff423f54ab0ba745"
integrity sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
@@ -10619,6 +10814,11 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.1.0:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
@@ -10701,6 +10901,11 @@ mktemp@~0.4.0:
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==
module-details-from-path@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b"
integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==
moment-mini@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.24.0.tgz#fa68d98f7fe93ae65bf1262f6abb5fb6983d8d18"
@@ -11083,6 +11288,11 @@ open@^8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
opentracing@^0.14.4:
version "0.14.7"
resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5"
integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -11940,6 +12150,11 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725"
integrity sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -12494,7 +12709,12 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.13.7:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
@@ -12670,6 +12890,15 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
require-in-the-middle@^5.0.3:
version "5.2.0"
resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz#4b71e3cc7f59977100af9beb76bf2d056a5a6de2"
integrity sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==
dependencies:
debug "^4.1.1"
module-details-from-path "^1.0.3"
resolve "^1.22.1"
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@@ -12714,7 +12943,7 @@ resolve.exports@^1.1.0:
resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9"
integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2:
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
@@ -13067,6 +13296,11 @@ shelljs@0.8.4:
interpret "^1.0.0"
rechoir "^0.6.2"
shimmer@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
side-channel@^1.0.3, side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@@ -13356,6 +13590,11 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
string-template@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -13677,6 +13916,15 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
thriftrw@^3.5.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.12.0.tgz#30857847755e7f036b2e0a79d11c9f55075539d9"
integrity sha512-4YZvR4DPEI41n4Opwr4jmrLGG4hndxr7387kzRFIIzxHQjarPusH4lGXrugvgb7TtPrfZVTpZCVe44/xUxowEw==
dependencies:
bufrw "^1.3.0"
error "7.0.2"
long "^2.4.0"
through2-filter@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
@@ -13866,6 +14114,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tslib@^2.3.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
@@ -14315,6 +14568,11 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@@ -14833,6 +15091,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xorshift@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-1.2.0.tgz#30a4cdd8e9f8d09d959ed2a88c42a09c660e8148"
integrity sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -14904,6 +15167,13 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zone.js@^0.11.0:
version "0.11.8"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.8.tgz#40dea9adc1ad007b5effb2bfed17f350f1f46a21"
integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA==
dependencies:
tslib "^2.3.0"
zwitch@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"