Compare commits
135 Commits
valere/dev
...
hs/disable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53727bac58 | ||
|
|
016c4dcac9 | ||
|
|
44a2809f79 | ||
|
|
a33fda5637 | ||
|
|
eca716b74d | ||
|
|
8691473329 | ||
|
|
d24da1859e | ||
|
|
342dd2e7d7 | ||
|
|
b2c594d36c | ||
|
|
9b9c08ed61 | ||
|
|
317b2dc796 | ||
|
|
0774d18f92 | ||
|
|
6e9dc34008 | ||
|
|
db5c7cf9c7 | ||
|
|
63122c7f6b | ||
|
|
4be395500f | ||
|
|
df7bd8ff2b | ||
|
|
2374a3fd33 | ||
|
|
5811794f31 | ||
|
|
e201258af3 | ||
|
|
76465d0e63 | ||
|
|
a2cf4c8139 | ||
|
|
65d358df58 | ||
|
|
32cb8541f4 | ||
|
|
62cfe09c7b | ||
|
|
8e32deaba5 | ||
|
|
8311b1619c | ||
|
|
fc449bcfbf | ||
|
|
34a8977dd1 | ||
|
|
db04cbfbfc | ||
|
|
c8d3d58940 | ||
|
|
d85cf5f929 | ||
|
|
1e32b355ce | ||
|
|
1193a22658 | ||
|
|
2541f810fa | ||
|
|
7961bb35be | ||
|
|
c599d6eae9 | ||
|
|
075a4cbe0d | ||
|
|
cd845b69d2 | ||
|
|
779f792cbf | ||
|
|
e9c43856d0 | ||
|
|
1e8b87408c | ||
|
|
1428df8567 | ||
|
|
b8acdc3cec | ||
|
|
880e07c07f | ||
|
|
07522d6704 | ||
|
|
58ed7ef93e | ||
|
|
c1694fe706 | ||
|
|
a775bcd62a | ||
|
|
7724cbf9be | ||
|
|
fe65c1f4da | ||
|
|
a91c71a8e4 | ||
|
|
e30142a43b | ||
|
|
c15551c9f5 | ||
|
|
f410a11934 | ||
|
|
3122ccf4e4 | ||
|
|
03527e9d7a | ||
|
|
36be15436a | ||
|
|
04c03b1e72 | ||
|
|
38e60208ef | ||
|
|
fb8de061f3 | ||
|
|
acd26c61ef | ||
|
|
e475f56af5 | ||
|
|
9486ed5d77 | ||
|
|
7ba4df7781 | ||
|
|
db65a5308a | ||
|
|
482e2f8f6f | ||
|
|
cd5b7cdc47 | ||
|
|
e097e58212 | ||
|
|
31892f46cd | ||
|
|
c0dccf89cb | ||
|
|
44ddb40eea | ||
|
|
5b8839b577 | ||
|
|
33bf63d8d2 | ||
|
|
a7cc11ac81 | ||
|
|
25f04030fc | ||
|
|
5d0289f3c9 | ||
|
|
3da9013426 | ||
|
|
1a1e5a9db8 | ||
|
|
87574451a9 | ||
|
|
f8a051ee11 | ||
|
|
a0d778083d | ||
|
|
8de6ddceb0 | ||
|
|
8b3b72dfb4 | ||
|
|
db59679ad4 | ||
|
|
f08ae36f9e | ||
|
|
dc789e63f2 | ||
|
|
30024877fc | ||
|
|
94085bff00 | ||
|
|
aa0f3a1126 | ||
|
|
0ed47c2588 | ||
|
|
770346676c | ||
|
|
8c1fa3fd70 | ||
|
|
217fa40ddb | ||
|
|
a733461845 | ||
|
|
6667fc54c0 | ||
|
|
c0aab96968 | ||
|
|
610032ad1d | ||
|
|
e4d14b4892 | ||
|
|
8509efb48b | ||
|
|
6dcc44b631 | ||
|
|
fce7b6d456 | ||
|
|
ef7c9a166a | ||
|
|
dba8a43495 | ||
|
|
1a3e88c19a | ||
|
|
955c22c8dc | ||
|
|
02f9d0822f | ||
|
|
e133289a7f | ||
|
|
518039f7c3 | ||
|
|
f5094be194 | ||
|
|
fd426191af | ||
|
|
f9d87ea934 | ||
|
|
77ba88b76c | ||
|
|
8615d3adb0 | ||
|
|
7971ea76cd | ||
|
|
5d992a398d | ||
|
|
1aa76925f7 | ||
|
|
4888efe2f7 | ||
|
|
879816ac6d | ||
|
|
6de8820590 | ||
|
|
415f83ace5 | ||
|
|
c9fb80f702 | ||
|
|
9bf63cce25 | ||
|
|
16947e7c39 | ||
|
|
b84c8ab82b | ||
|
|
078c67bfb2 | ||
|
|
81555c8d51 | ||
|
|
d294be8bd4 | ||
|
|
51672599ba | ||
|
|
2ade9b5c1e | ||
|
|
735d63d393 | ||
|
|
31bb46485f | ||
|
|
f53558cb81 | ||
|
|
0c194617a3 | ||
|
|
7c5336fc40 |
@@ -28,8 +28,6 @@ module.exports = {
|
||||
rules: {
|
||||
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
// We should use the js-sdk logger, never console directly.
|
||||
"no-console": ["error"],
|
||||
"react/display-name": "error",
|
||||
// Encourage proper usage of Promises:
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
@@ -46,6 +44,15 @@ module.exports = {
|
||||
"rxjs/no-exposed-subjects": "error",
|
||||
"rxjs/finnish": ["error", { names: { "^this$": false } }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["src/*/**"],
|
||||
rules: {
|
||||
// In application code we should use the js-sdk logger, never console directly.
|
||||
"no-console": ["error"],
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
|
||||
2
.github/workflows/changelog-label.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
steps:
|
||||
- uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||
with:
|
||||
REQUIRED_LABELS_ANY: "PR-Bug-Fix,PR-Documentation,PR-Task,PR-Feature,PR-Improvement,PR-Developer-Experience,dependencies"
|
||||
REQUIRED_LABELS_ANY: "PR-Bug-Fix,PR-Documentation,PR-Task,PR-Feature,PR-Improvement,PR-Developer-Experience,dependencies,PR-Breaking-Change"
|
||||
REQUIRED_LABELS_ANY_DESCRIPTION: "Select at least one 'PR-' label"
|
||||
BANNED_LABELS: "banned"
|
||||
|
||||
@@ -44,6 +44,8 @@ jobs:
|
||||
run: |
|
||||
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "TAG=latest" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\-rc\.[0-9]+$ ]]; then
|
||||
echo "TAG=rc" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "TAG=other" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -163,6 +165,8 @@ jobs:
|
||||
run: |
|
||||
if [[ "${{ needs.versioning.outputs.TAG }}" == "latest" ]]; then
|
||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
||||
elif [[ "${{ needs.versioning.outputs.TAG }}" == "rc" ]]; then
|
||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "ARTIFACT_VERSION=${{ needs.versioning.outputs.UNPREFIXED_VERSION }}-SNAPSHOT" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
3
.gitignore
vendored
@@ -8,6 +8,7 @@ dist-ssr
|
||||
.idea/
|
||||
public/config.json
|
||||
backend/synapse_tmp/*
|
||||
backend/synapse_tmp_othersite/*
|
||||
/coverage
|
||||
config.json
|
||||
|
||||
@@ -28,4 +29,4 @@ yarn-error.log
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.cache/
|
||||
|
||||
29
README.md
@@ -143,7 +143,8 @@ via the `org.matrix.msc4143.rtc_foci` key, e.g.:
|
||||
where the format for MatrixRTC using LiveKit backend is defined in
|
||||
[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
|
||||
In the example above Matrix clients do discover a focus of type `livekit` which
|
||||
points them to a Matrix LiveKit JWT Auth Service via `livekit_service_url`.
|
||||
points them to a [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||
via `livekit_service_url`.
|
||||
|
||||
### Backend Selection
|
||||
|
||||
@@ -154,9 +155,9 @@ points them to a Matrix LiveKit JWT Auth Service via `livekit_service_url`.
|
||||
the **first participant who joined the call** defines via the `foci_preferred`
|
||||
key in their `org.matrix.msc3401.call.member` which actual MatrixRTC backend
|
||||
will be used for this call.
|
||||
- During the actual call join flow, the **LiveKit JWT Auth Service** provides
|
||||
the client with the **LiveKit SFU WebSocket URL** and an **access JWT token**
|
||||
in order to exchange media via WebRTC.
|
||||
- During the actual call join flow, the **[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)**
|
||||
provides the client with the **LiveKit SFU WebSocket URL** and an
|
||||
**access JWT token** in order to exchange media via WebRTC.
|
||||
|
||||
The example below illustrates how backend selection works across **Matrix
|
||||
federation**, using the setup from sites A, B, and C. It demonstrates backend
|
||||
@@ -205,22 +206,22 @@ See also:
|
||||
### Backend
|
||||
|
||||
A docker compose file `dev-backend-docker-compose.yml` is provided to start the
|
||||
whole stack of components which is required for a local development environment:
|
||||
whole stack of components which is required for a local development environment
|
||||
including federation:
|
||||
|
||||
- Minimum Synapse Setup (servername: `synapse.m.localhost`)
|
||||
- LiveKit Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
|
||||
- Minimum LiveKit SFU Setup using dev defaults for config
|
||||
- Redis db for completeness
|
||||
- Minimum Synapse Setup (servernameis: `synapse.m.localhost`, `synapse.othersite.m.localhost`)
|
||||
- MatrixRTC Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
|
||||
- Minimum LiveKit SFU setup using dev defaults for config
|
||||
- Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS)
|
||||
- Hostnames: `m.localhost`, `*.m.localhost`
|
||||
- Hostnames: `m.localhost`, `*.m.localhost`, `*.othersite.m.localhost`
|
||||
- Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted
|
||||
certificates
|
||||
- Minimum TLS reverse proxy for
|
||||
- Synapse homeserver: `synapse.m.localhost`
|
||||
- MatrixRTC backend: `matrix-rtc.m.localhost`
|
||||
- Synapse homeserver: `synapse.m.localhost` and `synapse.othersite.m.localhost`
|
||||
- MatrixRTC backend: `matrix-rtc.m.localhost` and `matrix-rtc.othersite.m.localhost`
|
||||
- Local Element Call development `call.m.localhost` via `yarn dev --host `
|
||||
- Element Web `app.m.localhost`
|
||||
- Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT
|
||||
- Element Web `app.m.localhost` and `app.othersite.m.localhost`
|
||||
- Note certificates will expire on Thr, 20 September 2035 14:27:35 CEST
|
||||
|
||||
These use a test 'secret' published in this repository, so this must be used
|
||||
only for local development and **_never be exposed to the public Internet._**
|
||||
|
||||
64
backend/dev_homeserver-othersite.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
server_name: "synapse.othersite.m.localhost"
|
||||
public_baseurl: https://synapse.othersite.m.localhost/
|
||||
|
||||
pid_file: /data/homeserver.pid
|
||||
|
||||
listeners:
|
||||
- port: 18008
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation, openid]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: sqlite3
|
||||
args:
|
||||
database: /data/homeserver.db
|
||||
|
||||
media_store_path: /data/media_store
|
||||
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||
|
||||
# Due to custom TLS certificate with domains
|
||||
# - m.localhost, localhost
|
||||
# - *.m.localhost
|
||||
# - *.othersite.m.localhost
|
||||
# we disable certificate verification to allow for federation
|
||||
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||
federation_verify_certificates: false
|
||||
ip_range_blacklist: []
|
||||
trusted_key_servers:
|
||||
- server_name: "synapse.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
msc3266_enabled: true
|
||||
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||
# correctly track the state of the room.
|
||||
msc4222_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||
# duration (null), which disallows sending delayed events.
|
||||
max_event_delay_duration: 24h
|
||||
|
||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||
#
|
||||
# Each ratelimiting configuration is made of two parameters:
|
||||
# - per_second: number of requests a client can send per second.
|
||||
# - burst_count: number of requests a client can send before being throttled.
|
||||
|
||||
rc_message:
|
||||
# This needs to match at least the heart-beat frequency plus a bit of headroom
|
||||
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
||||
per_second: 0.5
|
||||
burst_count: 30
|
||||
@@ -19,8 +19,18 @@ database:
|
||||
|
||||
media_store_path: /data/media_store
|
||||
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||
|
||||
# Due to custom TLS certificate with domains
|
||||
# - m.localhost, localhost
|
||||
# - *.m.localhost
|
||||
# - *.othersite.m.localhost
|
||||
# we disable certificate verification to allow for federation.
|
||||
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||
federation_verify_certificates: false
|
||||
ip_range_blacklist: []
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
- server_name: "synapse.othersite.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
@@ -34,6 +44,13 @@ experimental_features:
|
||||
# duration (null), which disallows sending delayed events.
|
||||
max_event_delay_duration: 24h
|
||||
|
||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||
#
|
||||
# Each ratelimiting configuration is made of two parameters:
|
||||
@@ -45,10 +62,3 @@ rc_message:
|
||||
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
||||
per_second: 0.5
|
||||
burst_count: 30
|
||||
|
||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
20
backend/dev_livekit-othersite.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
port: 17880
|
||||
bind_addresses:
|
||||
- "0.0.0.0"
|
||||
rtc:
|
||||
tcp_port: 17881
|
||||
port_range_start: 50300
|
||||
port_range_end: 50400
|
||||
use_external_ip: false
|
||||
turn:
|
||||
enabled: false
|
||||
domain: localhost
|
||||
cert_file: ""
|
||||
key_file: ""
|
||||
tls_port: 5349
|
||||
udp_port: 443
|
||||
external_tls: true
|
||||
keys:
|
||||
devkey: secret
|
||||
room:
|
||||
auto_create: false
|
||||
@@ -6,11 +6,6 @@ rtc:
|
||||
port_range_start: 50100
|
||||
port_range_end: 50200
|
||||
use_external_ip: false
|
||||
#redis:
|
||||
# address: redis:6379
|
||||
# username: ""
|
||||
# password: ""
|
||||
# db: 0
|
||||
turn:
|
||||
enabled: false
|
||||
domain: localhost
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Synapse reverse proxy including .well-known/matrix/client
|
||||
# domain synapse.m.localhost
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
@@ -26,14 +27,53 @@ server {
|
||||
# This is also required for development environment.
|
||||
# Reason: the lk-jwt-service uses the federation API for the openid token
|
||||
# verification, which requires TLS
|
||||
location / {
|
||||
proxy_pass "http://homeserver:8008";
|
||||
location ~ ^(/_matrix|/_synapse/client) {
|
||||
proxy_pass "http://homeserver:8008";
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# Synapse reverse proxy including .well-known/matrix/client
|
||||
# domain synapse.othersite.m.localhost
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen 443 ssl;
|
||||
listen 8448 ssl;
|
||||
listen [::]:443 ssl;
|
||||
listen [::]:8448 ssl;
|
||||
server_name synapse.othersite.m.localhost;
|
||||
ssl_certificate /root/ssl/cert.pem;
|
||||
ssl_certificate_key /root/ssl/key.pem;
|
||||
|
||||
# well-known config adding rtc_foci backend
|
||||
# Note well-known is currently not effective due to:
|
||||
# https://spec.matrix.org/v1.12/client-server-api/#well-known-uri the spec
|
||||
# says it must be at https://$server_name/... (implied port 443) Hence, we
|
||||
# currently rely for local development environment on deprecated config.json
|
||||
# setting for livekit_service_url
|
||||
location /.well-known/matrix/client {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
return 200 '{"m.homeserver": {"base_url": "https://synapse.othersite.m.localhost"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrix-rtc.othersite.m.localhost/livekit/jwt"}]}';
|
||||
default_type application/json;
|
||||
}
|
||||
|
||||
# Reverse proxy for Matrix Synapse Homeserver
|
||||
# This is also required for development environment.
|
||||
# Reason: the lk-jwt-service uses the federation API for the openid token
|
||||
# verification, which requires TLS
|
||||
location ~ ^(/_matrix|/_synapse/client) {
|
||||
proxy_pass "http://homeserver-1:18008";
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
@@ -41,6 +81,7 @@ server {
|
||||
}
|
||||
|
||||
# MatrixRTC reverse proxy
|
||||
# domain matrix-rtc.m.localhost
|
||||
# - MatrixRTC Authorization Service
|
||||
# - LiveKit SFU websocket signaling connection
|
||||
upstream jwt-auth-services {
|
||||
@@ -49,16 +90,14 @@ upstream jwt-auth-services {
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
listen 8448 ssl;
|
||||
listen [::]:8448 ssl;
|
||||
server_name matrix-rtc.m.localhost;
|
||||
ssl_certificate /root/ssl/cert.pem;
|
||||
ssl_certificate_key /root/ssl/key.pem;
|
||||
|
||||
http2 on;
|
||||
|
||||
|
||||
location ^~ /livekit/jwt/ {
|
||||
|
||||
@@ -94,6 +133,54 @@ server {
|
||||
|
||||
}
|
||||
|
||||
# MatrixRTC reverse proxy
|
||||
# domain matrix-rtc.othersite.m.localhost
|
||||
# - MatrixRTC Authorization Service
|
||||
# - LiveKit SFU websocket signaling connection
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name matrix-rtc.othersite.m.localhost;
|
||||
ssl_certificate /root/ssl/cert.pem;
|
||||
ssl_certificate_key /root/ssl/key.pem;
|
||||
|
||||
http2 on;
|
||||
|
||||
|
||||
location ^~ /livekit/jwt/ {
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# JWT Service running at port 16080
|
||||
proxy_pass http://auth-service-1:16080/;
|
||||
|
||||
}
|
||||
|
||||
location ^~ /livekit/sfu/ {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_send_timeout 120;
|
||||
proxy_read_timeout 120;
|
||||
proxy_buffering off;
|
||||
|
||||
proxy_set_header Accept-Encoding gzip;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# LiveKit SFU websocket connection running at port 17880
|
||||
proxy_pass http://livekit-sfu-1:17880/;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# Convenience reverse proxy for the call.m.localhost domain to yarn dev --host
|
||||
server {
|
||||
listen 80;
|
||||
@@ -159,3 +246,36 @@ server {
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# Convenience reverse proxy app.othersite.m.localhost for element web
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name app.othersite.m.localhost;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name app.othersite.m.localhost;
|
||||
ssl_certificate /root/ssl/cert.pem;
|
||||
ssl_certificate_key /root/ssl/key.pem;
|
||||
|
||||
|
||||
location ^~ / {
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_pass http://element-web-1:18081;
|
||||
proxy_ssl_verify off;
|
||||
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDGjCCAgKgAwIBAgIUGdiFHhH4KL2pqBjMQHQ+PVIkSV8wDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy
|
||||
MDJaFw0zNTA1MDMxMDMyMDJaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
|
||||
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDA2y0hjmNn1vRsVSdy
|
||||
8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH9jdT
|
||||
tG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeFw9fw
|
||||
eRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZNWnie
|
||||
Ui7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAveL9K
|
||||
FGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BLPiQU
|
||||
KGKrAgMBAAGjUDBOMB0GA1UdDgQWBBQJqBjMu61c1p24txw/y+kv3D+V6DAfBgNV
|
||||
HSMEGDAWgBQJqBjMu61c1p24txw/y+kv3D+V6DAMBgNVHRMEBTADAQH/MA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQB8m2YfFGLugNt5vAAOvNxVqDA8c72yCVYr3CBCpmTIEY5Z
|
||||
d3qVGhG9//ux6+J8ntkSwd9nV5GJyYXHukCG1VavnAWolWdNF/WAllf0jhLuz7kD
|
||||
/cJnuI1By4tBsBmSz851i6HJ4t5k99Be+6GQVzi0e7zzfxTHZE4xP2J6Ox8QbPsP
|
||||
n0m76nIp/WbWaJqzvIIjJhmUUPPv+4wN+eOArgjiGLzptM2qTtGZtd0c9nS5gvep
|
||||
+mEbSUN9zkhAroZf80wf+hEvy+fJ94VbZ9QjTzTg7odZLrsXGIe8DaG63EYRQ25b
|
||||
W5iYBAreln5fGSt7qHsGfqwZibTEk/Lx3dydO1Kg
|
||||
MIIDGjCCAgKgAwIBAgIUbSbx+1UGptOTGefqEn7Zh3yoChIwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA5MjIxMjI3
|
||||
MzVaFw0zNTA5MjAxMjI3MzVaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
|
||||
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHODfkrFsOkqCnXnTb
|
||||
QWz3LkUtNCoVvM7wFouirRnITJYG+lFwF+zNl89Eaq+uUN4bwd8ml1ZuR9p+1azC
|
||||
SlklD5adhCR/ErknfUWamQEf6amSs3p0NnqnhXbnDEEbQOwNaPU/aGc6aw0+I9O6
|
||||
NQ/H830GlVuKd24Bfv0mx6Imo0Hi9jxKYhqFh80nmltk2uyXefaJxuo1jXBhwLyC
|
||||
DW8RVj55QvkZyBUzid8yslxrlo0LHKCCjZflwJJv5f+jaubkH5c0qxVaoR4+Liyt
|
||||
X/4viIwt3Mhj04ppudTvt973mTbjRG5haCz9y7OkT1mMWhc0xrdMFX+gjPERYS2H
|
||||
Ru/RAgMBAAGjUDBOMB0GA1UdDgQWBBTXNfLAKVayGQda/JZLPszrpz6LVzAfBgNV
|
||||
HSMEGDAWgBTXNfLAKVayGQda/JZLPszrpz6LVzAMBgNVHRMEBTADAQH/MA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQCvGfyopHHgZB+horGH6i/Xg41V+r4d0o092F1Lfr4vh86e
|
||||
XMakRw92vsyk/iWOnLPNPcpVWzPcvINaCs/bahgnGSOAnrA4jjcXqymyGIy/6xc5
|
||||
1EeZAxehiL9E5q4LQ841HDX0gps4ZzUO1BRYQcjG9Rdts83JO2ekkfHkZdNj2eQr
|
||||
KOrr92Na1/w+EQdo/T9Rs2ES623xKEOxPqb8d/rx5Z4DdeuGx1u+3AfS76Lpo4ni
|
||||
EJ0g1ImqdSUtiOLzeCQh6pqqb+vuFbxAyeyYSAJ49847EtFBvZCmWmPL2JICg9uq
|
||||
7rKW/qDfEK9GUs0GWCs3+mJkNvOOxBwtMuQrL7ZF
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA2y0hjmNn1vRs
|
||||
VSdy8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH
|
||||
9jdTtG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeF
|
||||
w9fweRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZN
|
||||
WnieUi7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAv
|
||||
eL9KFGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BL
|
||||
PiQUKGKrAgMBAAECggEAAPX2kxi5AQ7ul82SzT1KgpSXyDHLdYaUyAoYnaX9RO+B
|
||||
8ylmpyeqygs4+KQS4EMJm9jpo85Oy37bIKdG3kljU6wQcKlL5Y+ZUOo1nzpV6fid
|
||||
hGVs6ts8VXw8KshKQ9AyccZ8L/pirUfgOffgTwfjY7/90zceAL/s98GuZWc62nkX
|
||||
55joQv/OikqYfAGP/U6Bp2Zyf23DwJB09Z3B6NnZj/ZyAbDrDEHuA15LhCOcCczp
|
||||
IU/mFEywBPHT9Tg4w4Beq78PeAETvku2UalYRLhP3RLlXr2oEbwUtINRVt2QjZ85
|
||||
Esps4uCqL/mgQluIebtudD9HL/YMlNPXue1mDXFxJQKBgQDgZZY4yJBcf488T1V6
|
||||
HNm06b/LvVGj253pKgw14hpY1xQu3Ymgzv1GEqzhSYdzxhpmj0tMUNHxAp+YdGQu
|
||||
SZ0wcPKhw0aYVkIjDRYDC3Wn5GJhyIEYHGYMo/n4l49UzHRBPOTDzp49DkHTKBgh
|
||||
XgIIazYT3CkjTIMRrkUv+qfIPQKBgQDcBGu/mqbjxs4sN3zqPS4aB21o6t6W0sXs
|
||||
ZP9w6RlTPQi5U2oRbftjZtYc0bbEgkMUImB1HwYPQT5pJ+MyC414xDvSc2exBr5d
|
||||
To6yyPIy78Tf5PHM12fpKV92nSvoz/pSjYcGxxDtKfPqu+t8mOJfjCV1lLLA+xuB
|
||||
DDaE4p8dBwKBgQCdAne6A5v/HMH8UQZeCxHJpESvKiiVnnU/UEx651nID7XvlNNX
|
||||
0X0mKqsMd4ZvW43ddSYan/JF0LAa3FW8jYWO/3jF9vzOWoysOdvNBZetgf/Uq5ao
|
||||
aDZ/YbzmVCXWD7jIbPMkjs3pqrAkL0mzDzQc7+dGviWKrV6IYIfIqnn7gQKBgDCz
|
||||
vdIk/qpO+JZrFfiX4Fucp0hhLTJ/p5ZDaRPqVVPKn+K+Jy2ChfIj8mNgvK9VEloj
|
||||
nexvGJ1J2PHYBX+vdPp1nbRhHWPfVUY8PHQw7QP/dToGaMvqJrNDGEGeWvjnCMc7
|
||||
UtdaO1H0Rm0AegkTopB56lTTvJnhO95eALd7nrMDAoGAEPdzJtWoKafp49svhSj0
|
||||
hiXQv2SPBwVUN4LZ4SOWiXUcmYYm80aNpYKLkBxYjrfqFWhE7NUHLGp8YorQWKY2
|
||||
acD9AReHk/xku0ABy6jeYmSCmCxASxst5liKD+l12sk0gB0rk5MBxB4Uu1MIbQZ2
|
||||
aCASX3AVD2/XyC2MKkzc8Eg=
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHODfkrFsOkqCn
|
||||
XnTbQWz3LkUtNCoVvM7wFouirRnITJYG+lFwF+zNl89Eaq+uUN4bwd8ml1ZuR9p+
|
||||
1azCSlklD5adhCR/ErknfUWamQEf6amSs3p0NnqnhXbnDEEbQOwNaPU/aGc6aw0+
|
||||
I9O6NQ/H830GlVuKd24Bfv0mx6Imo0Hi9jxKYhqFh80nmltk2uyXefaJxuo1jXBh
|
||||
wLyCDW8RVj55QvkZyBUzid8yslxrlo0LHKCCjZflwJJv5f+jaubkH5c0qxVaoR4+
|
||||
LiytX/4viIwt3Mhj04ppudTvt973mTbjRG5haCz9y7OkT1mMWhc0xrdMFX+gjPER
|
||||
YS2HRu/RAgMBAAECggEABhB9CxYAE5p9D3s9nWsJcSDUdELRQSYlOoPFLmeMkF9c
|
||||
dcvq7LmduMh1Q8TnoivOBxRIwbj7pZHEYfYJM0TmH82wrQzXu5KLVltm4gTkVt9b
|
||||
DR8vjBgYdb8HVpM17Cl2xhW62XpJIiseFRUsHc/9sf2Egc3MIpPuIleGR0budbSW
|
||||
ybBkqEokTYTSiAztcu3G+VN0U9MsJgLMa8HApya7M48ojdrhzngVHZRUOXul9o7u
|
||||
zYJWSxPHIIYp5C4pYQBAx8OttThwKK1A9lwbQ2EJx0KnTbBC6O5Gna/jENpGd1h2
|
||||
rzK/9MONtsjln7IejP+4mDlNupS6SF3zzHPBHjqKAQKBgQDtXUIKPiVTFS45yWtK
|
||||
XD62s3j8jfIi+22b/C30fCPtppn0cm/0zY+vovgWVUBnQXkExafRthZCuxnE8ry7
|
||||
E29S40+4z9yivAC9dz7vHZUbyIFP6VG9WyhUYo+/WqOIePyh+iBISQ9TA1DneIYz
|
||||
+VZ8iU5GvdybUPl2C5WN8seaoQKBgQDW3EwVN2EEkChLRJbQYN2qpjn+0vYESMJ8
|
||||
K0sgMRtgh4+/T2Xb9b8O/dd87Fi/4oaUqWZ2E2sdsXq8P/IEo0cv6SRfHMy7GyxL
|
||||
RM7ztwUfMC4LVWi0ZIXMrm4gRDGN2XjGvhkX6fU2lSf6azWL1K3wI3amNV2b7P7d
|
||||
ItpvdkH3MQKBgQCXf29YJEQkXB9t6J3fDzND3xb4cwy5wSo7ZeBa7CTuWOhoeeX1
|
||||
JIJyAp0/e9goT0SThChRlFtu6gZPivJkoMnr6IOInLrg7we15fc4HPR/kCDgxTVT
|
||||
m2wJOAMxigNYZogwRfn2yRLL1BD+PBHD+H936xcX1bSJOUyPSGOC/xLhIQKBgQCb
|
||||
kCDd85ygyycBaAWxlZCor3WqFF/fNjbp5Aaepi9mMoBXSUs8eK7+UbelURHozEAY
|
||||
fpYaw3B4rTlp9vppdTZjb+/PlXB9v+zQCl+0gTyKGj4cIpiOk4F0co51eipOw7f4
|
||||
XUaZ0+CgxlmNq/W26iONjH+pU1YVQQA+Z6+zp/GW4QKBgQCrzYgeugxxqgJzyIRu
|
||||
0njJkIg+T5gHvsQrtpzq7LVob+HBiBiT7eDOeGDXTK8F//sk969QVrDMQsTMvGW9
|
||||
sG1oTqxciALTMqkJTf8+hT9Uogir0/iTbJUzTt5vPYpQOEQwQHIXMUTjZ9C6NDKT
|
||||
QlmeMCxeWyPYqoMfwKmdtDP/Iw==
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDZzCCAk+gAwIBAgIUXizLjwkdqepX0bh0K3abeJxj68IwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMzU5
|
||||
MTFaFw0zNTA1MDMxMzU5MTFaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrzGSScSgaQuZdELGFYiLiYRwr
|
||||
LKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI39WmH95aX3d+A
|
||||
U7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ouuN6AOdzWs8hp
|
||||
RYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFft1ZXxIZOCjDs
|
||||
rEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71IfieLCEzMTP1VXa
|
||||
tP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZrQUGnL2RtAgMB
|
||||
AAGjgaIwgZ8wHwYDVR0jBBgwFoAUCagYzLutXNaduLccP8vpL9w/legwCQYDVR0T
|
||||
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMAYDVR0RBCkw
|
||||
J4IJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdDAdBgNVHQ4E
|
||||
FgQUfdh1p52ZgWyZcBgBXGwKi4EnUE0wDQYJKoZIhvcNAQELBQADggEBAKrHEuB6
|
||||
33j8+EwSHw3zrvt/DRXK2BDHI1Ir9JcztSunaKAjZXVvf/dvZp0Xs1dEdJIdnv6G
|
||||
iZYhBbOqDqpQZbf2h/h0kuu5yZSBUdnQXnYNxlhp2UaC/UEgw5iZT/p1rm7RjVie
|
||||
y4Dp2WytV5iZOLmLj6xDvd3DXazgJPWIRX8p8qJZbKTkwCjTr7nDIj8jjG1sVFf7
|
||||
1RJBO5/6WSnImrpDmlLUrvjiKvbxcdseDJyBOhTwdRdSk4S2M+s5tR5j2I1gXLOq
|
||||
J5ioN76+SCrTY0K0WKRy9oOXWO1/X3+VYcekp+0F3SGkd5w17jylCv1XIGHAdEsQ
|
||||
v2z2/aMI/7sAD2Q=
|
||||
MIIDgDCCAmigAwIBAgIUT9NYpZbrAKokSPSTE3zzsAMowvEwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA5MjIxMjI3
|
||||
MzVaFw0zNTA5MjAxMjI3MzVaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK1DNwTQWmyK71Ar56NvmSMQ8s
|
||||
qUY3jGqqPVORjfDUtDCrPPdCxT+ZlnsAgdonElWoWqczMrSyBRgfJlZMd4lEvt6V
|
||||
EEiZGUvA/lG1XIVgrx1kMSHKBoJj7lCBN6r3IWmYe6CxgfZurgp+7Z22i6cGMOnQ
|
||||
0XduX5Asup6zk5V7AE6i9eKrJsUjYmRBXtk099IitkER4TMqh6WxJmFF+eV9P/ax
|
||||
fxkon+bQWITwP1PLC1UOTK7lR0EcVan5aY6WMs/6RfO4Gw/dvuiVG1jCrVcaKNGT
|
||||
PYqmQqs+MOvyIqJ9kYELRZu+6bhPWSXk2ESpSIUIPH9twfnmWrncneIJR24/AgMB
|
||||
AAGjgbswgbgwHwYDVR0jBBgwFoAU1zXywClWshkHWvyWSz7M66c+i1cwCQYDVR0T
|
||||
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwSQYDVR0RBEIw
|
||||
QIIJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdIIXKi5vdGhl
|
||||
cnNpdGUubS5sb2NhbGhvc3QwHQYDVR0OBBYEFIkGX+cEJ1ISKIwuT1zzp7uHJ90e
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBAQBnnnfB7KmyYo16ZYUCmoqGhbM4p8npeYTh5ySb
|
||||
K01YwGCnMU1qGfJnKHaRwQ2+KtVGZnpBdjmsHcOUetA3V2BirPaYowMCMtaI36LD
|
||||
LnxvboSZLX0mgEYuN7HmxW4a7fSelDecTYa7xti1sNhE/w8xW7Lky046/DousyUy
|
||||
d9x3wJ183GGj1W2p6bR1E4sqTr/VbmoULQxnqA3GUNOxW3lRL5e8lQ6jJVRmMF4k
|
||||
92BtMPrI/m7jwHj0f/WBLI8mdJ/O/W/NxQOG475FZePDfrg+MkeXPChPggf42/ou
|
||||
AMm56FNB7e1l0b1Fots730RfpCPuXpiAxL4pisS0X1dMVeeM
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrzGSScSgaQuZd
|
||||
ELGFYiLiYRwrLKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI3
|
||||
9WmH95aX3d+AU7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ou
|
||||
uN6AOdzWs8hpRYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFf
|
||||
t1ZXxIZOCjDsrEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71Ifi
|
||||
eLCEzMTP1VXatP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZr
|
||||
QUGnL2RtAgMBAAECggEAJaFQii8U/KOYt9vXNoMnZvSkaeSQLLhn2V6Kciu1CtWE
|
||||
aMTWLsFE6nk+G5xXkYcTmM3T0GghtH3u5CjyI6EcsEkeEorCZJt0wbmayDmqiekR
|
||||
LfMzOdHuTHX5+edPgMGYYG1BFyRKyYFsjH1b5zRFZhXdGQnrl5760GsVlz9D1KZQ
|
||||
iHcT+q1S2tmZeoUukQnADENKXUMCyTGM5FCddgNtsWnGDsTDayh7hUdvDkB+mW4G
|
||||
lSp+BZuc3PCwpbD6qkXvfugWs6CUAAtXoV3ceWgxQ+TEnNlwxaG1AyugfgNUBolk
|
||||
8xgeZt4r5QId03jsHDf7hpBAofcaCd5EMIIQYFvWoQKBgQDlbAvAzEFPTZZn2nRV
|
||||
Xagw4xjqVc1LLEKLCWq0N5rEkwn0h90Dz5N7/3NuonP/sIDsDHCbyiOYBI1Ck6Xi
|
||||
0WuB+OyKDh+xeF2mekN9G9ywPahdK5lT/TVsxXFyZlwtVv1x/6KBO4yv5URizxqU
|
||||
gyAPDDxfD/KcNjkOBaodWEwQGQKBgQC/s2gPDBtQkjLwkHXchBomLww5eLlVrac1
|
||||
WK4UX6uSdOgrjJ375OOgMTxe8NVZdOuAKytGXRWDwgH3nVWvuZhe7dGlX3JMuSer
|
||||
e9VwDpBESrvqcR4ruL6wm8wej6BXyjH0wD3FHb0S5HfuBDxTn+4bDwrbRzOUMNgy
|
||||
lSppuflxdQKBgQDiZcIfazFT8evn5nMAvuC4BZNTxIJHmZC9JfjPiUPIkpWzYtOe
|
||||
7BvNtKOT3Op9uw8uYYRKqKqBXJSNy6ha8XCXHS9HeXKbLn20SFkLQBCDNwVLlDfF
|
||||
40zyXtF6JDr4XyzSb4NM5pgKCER5AYloXxGm59s3sEQpFXUuOjbKqJS/GQKBgAoI
|
||||
c7vF4HAZFr1sch62cz/oWnVvkhOf4Q5zs7ixQSOLJtOQqnwSgK9TpFs7s47ZBbJR
|
||||
kBRAru2Ua9Hv1Bo8VnMxczV6h1roneDlvEf/GyHX33nnrbKQGrrXjJlU3wl5NaAf
|
||||
p5v3cHvapUQ5yIZ/6lBUOzc6xMJOxCHxmKSr7Rg5AoGAbEE4lt6Xh2dnBPJ81eNI
|
||||
IDrw/3ITY53qAY4Bx88CByIFuu8CEUdUZprh98jSl6ic1tMinZfUhRMwABLrUD51
|
||||
DGst8iGLPD9u83iMcUHI/L+p7AbxrKLvWXZrF5UZm440c9mSWqfhPaTBosPtNDsG
|
||||
LfETwH1flKXMTXd2xA9RTE4=
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK1DNwTQWmyK71
|
||||
Ar56NvmSMQ8sqUY3jGqqPVORjfDUtDCrPPdCxT+ZlnsAgdonElWoWqczMrSyBRgf
|
||||
JlZMd4lEvt6VEEiZGUvA/lG1XIVgrx1kMSHKBoJj7lCBN6r3IWmYe6CxgfZurgp+
|
||||
7Z22i6cGMOnQ0XduX5Asup6zk5V7AE6i9eKrJsUjYmRBXtk099IitkER4TMqh6Wx
|
||||
JmFF+eV9P/axfxkon+bQWITwP1PLC1UOTK7lR0EcVan5aY6WMs/6RfO4Gw/dvuiV
|
||||
G1jCrVcaKNGTPYqmQqs+MOvyIqJ9kYELRZu+6bhPWSXk2ESpSIUIPH9twfnmWrnc
|
||||
neIJR24/AgMBAAECggEANRp6vzfDN4gKWoaV5TyYegCTNv+4rRl74cd9pjmx6Jam
|
||||
uWaUXCx1etpNqPPWcG1Z9OKLLRnk+kjgKGOqq4mObGvGreNeBot7bHOJZADtwMMI
|
||||
YG+Gp7StlclS1YoEHoDmezA/AcqDgTXa9KF0rdMBb1sGFJCLAuBNSJCxtVV6CQIz
|
||||
X26uT0m+Wx8MQyQWA7Sqy6DQNJo++IZkvr7a3cidqBOUPs+QvnIV5JsUb2gp5tGn
|
||||
zk+ObeRjoFFWYAN/NK7bneRenkP40m3MSL8ZfaEuuonui7CrxM1SiQyq2N1u/Aoy
|
||||
OE1JtNaVPbLBo6kG5al7Sj4Z0zhRt+iv93S2lZMkBQKBgQD2+FpLTqyLO1NDOFkE
|
||||
kxU+LdLOx0OV9wASC0ApPOu1dHMG6ksByr7TWeiu6GJDgajusPB7NVPOt2cm4iWU
|
||||
xPxXPO5l87uiSvu80h5uG4Qdj8KEijHkdap2wbVkU/mm8lBKC36jyBQIlJKySyXY
|
||||
zSEMfLK9jQPKz5cKKT3dVj/fAwKBgQDSPq9oks6K96MAB66o6cm214otQlnTQkPM
|
||||
xgjtjddX+Lp9tgihGvtSfPbyy89oUDHCfKvW/AHG52e5dec5YUi6mVdHEWbk33Kt
|
||||
BoQuxeK3XseIDlD/JD9Dd7KfUyO5w2jtYLfNdqez41O4qj2N52m1KwJYTwMsc8Kq
|
||||
izVgkC5hFQKBgBFAc/5CtqbbNAvECePZ6mf3h3xOSxhUsrqP8qFu0gBQ7CAVibvM
|
||||
T9wvsaNWNFcG3age0A2rQfl0sk3zCjEEOaRWa0jP59GEb2VXQCzs2yO9gRcFGEsf
|
||||
NRMqoOMrQos47gbeGrCSL2QSDNVLjo9AdQiMRWgcS6GFMsXQ77NgbQHFAoGBAI4a
|
||||
YGTGFWRITJvQlXUFz5kNxg8hMaVgvILDt3UY0dxb+XDOgLajjgsK+77Pkrhmu7tA
|
||||
mMUOQAU4kxr/XfGil43H5v3Z/Tnk7ZWVOfKDPeHC5gpH4ucQkNIBLXISt6rvMRSA
|
||||
srrk4CTuGcBPEJvBNemF0Gfvv61j8MdkoAdMbIyhAoGAfGR6yZLBmRMsW5PKmcpT
|
||||
nq2oSeUpmtGZra6pWz/3XU7AgrCLcx1DmqEjm4w7y5NQJmxyMZqqdTJILCjr3Srt
|
||||
+2F0NqQL6Li+xQGibAvDj0Jxyol38RvFC0J/w2vQmuF0hTuH95yknSd7FPXK+DPG
|
||||
qYgXLjun9dht6kx9vGJ69wI=
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -34,5 +34,6 @@ subjectAltName = @alt_names
|
||||
DNS.1 = localhost
|
||||
DNS.2 = m.localhost
|
||||
DNS.3 = *.m.localhost
|
||||
DNS.4 = *.othersite.m.localhost
|
||||
EOF
|
||||
)
|
||||
|
||||
53
backend/ew.test.othersite.config.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://synapse.othersite.m.localhost",
|
||||
"server_name": "synapse.othersite.m.localhost"
|
||||
}
|
||||
},
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": false,
|
||||
"disable_login_language_selector": false,
|
||||
"disable_3pid_login": false,
|
||||
"force_verification": false,
|
||||
"brand": "Element",
|
||||
"integrations_ui_url": "https://scalar.vector.im/",
|
||||
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||
"integrations_widgets_urls": [
|
||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar.vector.im/api",
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar-staging.vector.im/api",
|
||||
"https://scalar-staging.riot.im/scalar/api"
|
||||
],
|
||||
"default_widget_container_height": 280,
|
||||
"default_country_code": "GB",
|
||||
"show_labs_settings": false,
|
||||
"features": {
|
||||
"feature_element_call_video_rooms": true,
|
||||
"feature_video_rooms": true,
|
||||
"feature_group_calls": true,
|
||||
"feature_release_announcement": false
|
||||
},
|
||||
"default_federate": true,
|
||||
"default_theme": "light",
|
||||
"room_directory": {
|
||||
"servers": ["matrix.org"]
|
||||
},
|
||||
"enable_presence_by_hs_url": {
|
||||
"https://matrix.org": false,
|
||||
"https://matrix-client.matrix.org": false
|
||||
},
|
||||
"setting_defaults": {
|
||||
"breadcrumbs": true,
|
||||
"feature_group_calls": true
|
||||
},
|
||||
"jitsi": {
|
||||
"preferred_domain": "meet.element.io"
|
||||
},
|
||||
"element_call": {
|
||||
"participant_limit": 8,
|
||||
"brand": "Element Call"
|
||||
},
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||
}
|
||||
81
backend/playwright_homeserver-othersite.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
server_name: "synapse.othersite.m.localhost"
|
||||
public_baseurl: https://synapse.othersite.m.localhost/
|
||||
|
||||
pid_file: /data/homeserver.pid
|
||||
|
||||
listeners:
|
||||
- port: 18008
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation, openid]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: sqlite3
|
||||
args:
|
||||
database: /data/homeserver.db
|
||||
|
||||
media_store_path: /data/media_store
|
||||
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||
|
||||
# Due to custom TLS certificate with domains
|
||||
# - m.localhost, localhost
|
||||
# - *.m.localhost
|
||||
# - *.othersite.m.localhost
|
||||
# we disable certificate verification to allow for federation.
|
||||
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||
federation_verify_certificates: false
|
||||
ip_range_blacklist: []
|
||||
trusted_key_servers:
|
||||
- server_name: "synapse.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
msc3266_enabled: true
|
||||
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||
# correctly track the state of the room.
|
||||
msc4222_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||
# duration (null), which disallows sending delayed events.
|
||||
max_event_delay_duration: 24h
|
||||
|
||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||
#
|
||||
# Each ratelimiting configuration is made of two parameters:
|
||||
# - per_second: number of requests a client can send per second.
|
||||
# - burst_count: number of requests a client can send before being throttled.
|
||||
|
||||
rc_message:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_delayed_event_mgmt:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
@@ -19,8 +19,18 @@ database:
|
||||
|
||||
media_store_path: /data/media_store
|
||||
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||
|
||||
# Due to custom TLS certificate with domains
|
||||
# - m.localhost, localhost
|
||||
# - *.m.localhost
|
||||
# - *.othersite.m.localhost
|
||||
# we disable certificate verification to allow for federation.
|
||||
# WARNING: DO NOT USE IN PRODUCTION!!!
|
||||
federation_verify_certificates: false
|
||||
ip_range_blacklist: []
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
- server_name: "synapse.othersite.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
@@ -34,6 +44,13 @@ experimental_features:
|
||||
# duration (null), which disallows sending delayed events.
|
||||
max_event_delay_duration: 24h
|
||||
|
||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||
#
|
||||
# Each ratelimiting configuration is made of two parameters:
|
||||
@@ -44,6 +61,10 @@ rc_message:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_delayed_event_mgmt:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
@@ -58,10 +79,3 @@ rc_login:
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
bind 0.0.0.0
|
||||
protected-mode yes
|
||||
port 6379
|
||||
timeout 0
|
||||
tcp-keepalive 300
|
||||
@@ -24,8 +24,31 @@ services:
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
auth-service-1:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||
pull_policy: always
|
||||
hostname: auth-server-1
|
||||
environment:
|
||||
- LIVEKIT_JWT_PORT=16080
|
||||
- LIVEKIT_URL=wss://matrix-rtc.othersite.m.localhost/livekit/sfu
|
||||
- LIVEKIT_KEY=devkey
|
||||
- LIVEKIT_SECRET=secret
|
||||
# If the configured homeserver runs on localhost, it'll probably be using
|
||||
# a self-signed certificate
|
||||
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
|
||||
- LIVEKIT_FULL_ACCESS_HOMESERVERS=*
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
ports:
|
||||
# HOST_PORT:CONTAINER_PORT
|
||||
- 16080:16080
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
pull_policy: always
|
||||
hostname: livekit-sfu
|
||||
command: --dev --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
@@ -43,20 +66,30 @@ services:
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
command: redis-server /etc/redis.conf
|
||||
livekit-1:
|
||||
image: livekit/livekit-server:latest
|
||||
pull_policy: always
|
||||
hostname: livekit-sfu-1
|
||||
command: --dev --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
# The SFU seems to work far more reliably when we let it share the host
|
||||
# network rather than opening specific ports (but why?? we're not missing
|
||||
# any…)
|
||||
ports:
|
||||
# HOST_PORT:CONTAINER_PORT
|
||||
- 6379:6379
|
||||
- 17880:17880/tcp
|
||||
- 17881:17881/tcp
|
||||
- 17882:17882/tcp
|
||||
- 50300-50400:50300-50400/udp
|
||||
volumes:
|
||||
- ./backend/redis.conf:/etc/redis.conf:Z
|
||||
- ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
synapse:
|
||||
hostname: homeserver
|
||||
image: docker.io/matrixdotorg/synapse:latest
|
||||
pull_policy: always
|
||||
environment:
|
||||
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
|
||||
# Needed for rootless podman-compose such that the uid/gid mapping does
|
||||
@@ -71,6 +104,24 @@ services:
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
synapse-1:
|
||||
hostname: homeserver-1
|
||||
image: docker.io/matrixdotorg/synapse:latest
|
||||
pull_policy: always
|
||||
environment:
|
||||
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
|
||||
# Needed for rootless podman-compose such that the uid/gid mapping does
|
||||
# fit local user uid. If the container runs as root (uid 0) it is fine as
|
||||
# it actually maps to your non-root user on the host (e.g. 1000).
|
||||
# Otherwise uid mapping will not match your non-root user.
|
||||
- UID=0
|
||||
- GID=0
|
||||
volumes:
|
||||
- ./backend/synapse_tmp_othersite:/data:Z
|
||||
- ./backend/dev_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
element-web:
|
||||
image: ghcr.io/element-hq/element-web:develop
|
||||
pull_policy: always
|
||||
@@ -83,10 +134,24 @@ services:
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
element-web-1:
|
||||
image: ghcr.io/element-hq/element-web:develop
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- ./backend/ew.test.othersite.config.json:/app/config.json:Z
|
||||
environment:
|
||||
ELEMENT_WEB_PORT: 18081
|
||||
ports:
|
||||
# HOST_PORT:CONTAINER_PORT
|
||||
- "18081:18081"
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
nginx:
|
||||
# see backend/dev_tls_setup for how to generate the tls certs
|
||||
hostname: synapse.m.localhost
|
||||
image: nginx:latest
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
|
||||
- ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z
|
||||
@@ -104,4 +169,7 @@ services:
|
||||
networks:
|
||||
ecbackend:
|
||||
aliases:
|
||||
- synapse.m.localhost
|
||||
- synapse.othersite.m.localhost
|
||||
- matrix-rtc.m.localhost
|
||||
- matrix-rtc.othersite.m.localhost
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 929 KiB After Width: | Height: | Size: 941 KiB |
|
Before Width: | Height: | Size: 886 KiB After Width: | Height: | Size: 908 KiB |
@@ -64,7 +64,7 @@ rc_delayed_event_mgmt:
|
||||
```
|
||||
|
||||
As a prerequisite for the
|
||||
[Matrix LiveKit JWT auth service](https://github.com/element-hq/lk-jwt-service)
|
||||
[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||
make sure that your Synapse server has either a `federation` or `openid`
|
||||
[listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners).
|
||||
|
||||
@@ -77,7 +77,7 @@ required for each site deployment.
|
||||
|
||||
As depicted above in the `example.com` site deployment, Element Call requires a
|
||||
[Livekit SFU](https://github.com/livekit/livekit) alongside a
|
||||
[Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service)
|
||||
[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||
to implement
|
||||
[MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
|
||||
|
||||
@@ -89,7 +89,7 @@ the example above, this results in:
|
||||
| Service | Endpoint | Example |
|
||||
| -------- | ------- | ------- |
|
||||
| [Livekit SFU](https://github.com/livekit/livekit) WebSocket signalling connection | `/livekit/sfu` | `matrix-rtc.example.com/livekit/sfu` |
|
||||
| [Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
|
||||
| [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
|
||||
|
||||
Using Nginx, you can achieve this by:
|
||||
|
||||
@@ -102,7 +102,7 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# JWT Service running at port 8080
|
||||
# MatrixRTC Authorization Service running at port 8080
|
||||
proxy_pass http://localhost:8080/;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,32 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
Or Using Caddy, you can achieve this by:
|
||||
|
||||
```caddy configuration file
|
||||
# Route for lk-jwt-service with livekit/jwt prefix
|
||||
@jwt_service path /livekit/jwt/sfu/get /livekit/jwt/healthz
|
||||
handle @jwt_service {
|
||||
uri strip_prefix /livekit/jwt
|
||||
reverse_proxy http://[::1]:8080 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Server {host}
|
||||
header_up X-Real-IP {remote_addr}
|
||||
header_up X-Forwarded-For {remote_addr}
|
||||
}
|
||||
}
|
||||
|
||||
# Default route for livekit
|
||||
handle {
|
||||
reverse_proxy http://localhost:7880 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Server {host}
|
||||
header_up X-Real-IP {remote_addr}
|
||||
header_up X-Forwarded-For {remote_addr}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### MatrixRTC backend announcement
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -145,10 +171,6 @@ server {
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://matrix-rtc-2.example.com/livekit/jwt"
|
||||
},
|
||||
{
|
||||
"type": "nextgen_new_foci_type",
|
||||
"props_for_nextgen_foci": "val"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -218,7 +240,7 @@ server {
|
||||
There are currently two different config files. `.env` holds variables that are
|
||||
used at build time, while `public/config.json` holds variables that are used at
|
||||
runtime. Documentation and default values for `public/config.json` can be found
|
||||
in [ConfigOptions.ts](src/config/ConfigOptions.ts).
|
||||
in [ConfigOptions.ts](../src/config/ConfigOptions.ts).
|
||||
|
||||
> [!CAUTION]
|
||||
> Please note configuring MatrixRTC backend via `config.json` of
|
||||
|
||||
@@ -48,6 +48,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
||||
|
||||
| Name | Values | Required for widget | Required for SPA | Description |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `intent` | `start_call`, `join_existing`, `start_call_dm`, `join_existing_dm. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. |
|
||||
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
||||
| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
||||
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
||||
@@ -59,7 +60,6 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
||||
| `header` | `none`, `standard` or `app_bar` | No, defaults to `standard` | No, defaults to `standard` | The style of headers to show. `standard` is the default arrangement, `none` hides the header entirely, and `app_bar` produces a header with a back button like you might see in mobile apps. The callback for the back button is `window.controls.onBackButtonPressed`. |
|
||||
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
||||
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
||||
| `intent` | `start_call` or `join_existing` | No, defaults to `start_call` | No, defaults to `start_call` | The intent of the user with respect to the call. e.g. if they clicked a Start Call button, this would be `start_call`. If it was a Join Call button, it would be `join_existing`. |
|
||||
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
||||
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
||||
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
||||
@@ -70,6 +70,8 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st
|
||||
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
||||
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
||||
| `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. |
|
||||
| `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. |
|
||||
| `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. |
|
||||
|
||||
### Widget-only parameters
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
|
||||
[versions]
|
||||
android_gradle_plugin = "8.11.0"
|
||||
android_gradle_plugin = "8.11.1"
|
||||
|
||||
[libraries]
|
||||
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
|
||||
|
||||
[plugins]
|
||||
android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" }
|
||||
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.33.0" }
|
||||
maven_publish = { id = "com.vanniktech.maven.publish", version = "0.34.0" }
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import com.vanniktech.maven.publish.SonatypeHost
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.maven.publish)
|
||||
@@ -27,7 +25,7 @@ android {
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
|
||||
publishToMavenCentral(automaticRelease = true)
|
||||
|
||||
signAllPublications()
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default {
|
||||
import type { UserConfig } from "i18next-parser";
|
||||
|
||||
const config: UserConfig = {
|
||||
keySeparator: ".",
|
||||
namespaceSeparator: false,
|
||||
contextSeparator: "|",
|
||||
@@ -26,3 +28,5 @@ export default {
|
||||
input: ["src/**/*.{ts,tsx}"],
|
||||
sort: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
11
knip.ts
@@ -1,8 +1,15 @@
|
||||
import { KnipConfig } from "knip";
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type KnipConfig } from "knip";
|
||||
|
||||
export default {
|
||||
vite: {
|
||||
config: ["vite.config.js", "vite-embedded.config.js"],
|
||||
config: ["vite.config.ts", "vite-embedded.config.ts"],
|
||||
},
|
||||
entry: ["src/main.tsx", "i18next-parser.config.ts"],
|
||||
ignoreBinaries: [
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Zobrazit možnost sluchátek pro iPhone na všech platformách",
|
||||
"crypto_version": "Kryptografická verze: {{version}}",
|
||||
"debug_tile_layout_label": "Ladění rozložení dlaždic",
|
||||
"device_id": "ID zařízení: {{id}}",
|
||||
@@ -81,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Volání není podporováno",
|
||||
"call_not_found": "Volání nebylo nalezeno",
|
||||
"call_not_found_description": "<0>Zdá se, že tento odkaz nepatří k žádnému existujícímu volání. Zkontrolujte, zda máte správný odkaz, nebo <1>vytvořte nový</1>.</0>",
|
||||
"call_not_found_description": "<0>Zdá se, že tento odkaz nepatří k žádnému existujícímu hovoru. Zkontrolujte, zda máte správný odkaz, nebo<2> vytvořte nový</2>.</0>",
|
||||
"connection_lost": "Spojení ztraceno",
|
||||
"connection_lost_description": "Hovor byl přerušen.",
|
||||
"e2ee_unsupported": "Nekompatibilní prohlížeč",
|
||||
@@ -104,6 +105,11 @@
|
||||
"knock_reject_heading": "Přístup odepřen",
|
||||
"reason": "Důvod"
|
||||
},
|
||||
"handset": {
|
||||
"overlay_back_button": "Zpět do režimu reproduktoru",
|
||||
"overlay_description": "Funguje pouze při používání aplikace",
|
||||
"overlay_title": "Režim sluchátka"
|
||||
},
|
||||
"hangup_button_label": "Ukončit hovor",
|
||||
"header_label": "Domov Element Call",
|
||||
"header_participants_label": "Účastníci",
|
||||
@@ -173,8 +179,11 @@
|
||||
"devices": {
|
||||
"camera": "Fotoaparát",
|
||||
"camera_numbered": "Fotoaparát {{n}}",
|
||||
"change_device_button": "Změnit zvukové zařízení",
|
||||
"default": "Výchozí",
|
||||
"default_named": "Výchozí <2> ({{name}}) </2>",
|
||||
"handset": "Sluchátko",
|
||||
"loudspeaker": "Reproduktor",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Reproduktor",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Vis mulighed for iPhone-høretelefon på alle platforme",
|
||||
"crypto_version": "Krypto-version: {{version}}",
|
||||
"debug_tile_layout_label": "Fejlfinding af fliselayout",
|
||||
"device_id": "Enheds-id: {{id}}",
|
||||
@@ -70,6 +71,7 @@
|
||||
"livekit_server_info": "LiveKit Serverinfo",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"mute_all_audio": "Slå al lyd fra (deltagere, reaktioner, deltagelseslyde)",
|
||||
"show_connection_stats": "Vis forbindelsesstatistik",
|
||||
"show_non_member_tiles": "Vis fliser for medier fra ikke-medlemmer",
|
||||
"url_params": "URL-parametre",
|
||||
@@ -80,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Opkald er ikke understøttet",
|
||||
"call_not_found": "Opkald ikke fundet",
|
||||
"call_not_found_description": "<0>Det link ser ikke ud til at høre til et eksisterende opkald. Tjek at du har det rigtige link, eller<1> opret et nyt</1>.</0>",
|
||||
"call_not_found_description": "<0>Det link ser ikke ud til at høre til et eksisterende opkald. Tjek at du har det rigtige link, eller <2> opret et nyt</2>.</0>",
|
||||
"connection_lost": "Forbindelsen gik tabt",
|
||||
"connection_lost_description": "Du blev afbrudt fra opkaldet.",
|
||||
"e2ee_unsupported": "Inkompatibel browser",
|
||||
@@ -92,6 +94,8 @@
|
||||
"matrix_rtc_focus_missing": "Serveren er ikke konfigureret til at arbejde med {{brand}}{{domain}}. Kontakt venligst din serveradministrator (domæne:{{domain}}, fejlkode: {{ errorCode }}).",
|
||||
"open_elsewhere": "Åbnet i en anden fane",
|
||||
"open_elsewhere_description": "{{brand}} er blevet åbnet i en anden fane. Hvis det ikke lyder rigtigt, kan du prøve at genindlæse siden.",
|
||||
"room_creation_restricted": "Kunne ikke oprette opkald",
|
||||
"room_creation_restricted_description": "Oprettelse af opkald er muligvis begrænset til autoriserede brugere. Prøv igen senere, eller kontakt din serveradministrator, hvis problemet fortsætter.",
|
||||
"unexpected_ec_error": "Der opstod en uventet fejl (<0>Fejlkode:</0> <1> {{ errorCode }}</1>). Kontakt venligst din serveradministrator."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -103,6 +107,11 @@
|
||||
"knock_reject_heading": "Adgang nægtet",
|
||||
"reason": "Årsag: {{reason}}"
|
||||
},
|
||||
"handset": {
|
||||
"overlay_back_button": "Tilbage til højttalertilstand",
|
||||
"overlay_description": "Virker kun, når du bruger appen",
|
||||
"overlay_title": "Telefon-højtaler"
|
||||
},
|
||||
"hangup_button_label": "Afslut opkald",
|
||||
"header_label": "Element Ring hjem",
|
||||
"header_participants_label": "Deltagere",
|
||||
@@ -164,12 +173,18 @@
|
||||
"effect_volume_description": "Juster den lydstyrke som reaktioner og håndsoprækninger afspilles med.",
|
||||
"effect_volume_label": "Lydstyrke for lydeffekter"
|
||||
},
|
||||
"background_blur_header": "Baggrund",
|
||||
"background_blur_label": "Gør videoens baggrund sløret",
|
||||
"blur_not_supported_by_browser": "(Baggrundssløring understøttes ikke af denne enhed.)",
|
||||
"developer_tab_title": "Udvikler",
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Skift lydenhed",
|
||||
"default": "Standard",
|
||||
"default_named": "Standard <2>({{name}})</2>",
|
||||
"handset": "Telefon",
|
||||
"loudspeaker": "Højttaler",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Højttaler",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"profile": "Profil",
|
||||
"reaction": "Reaktion",
|
||||
"reactions": "Reaktionen",
|
||||
"reconnecting": "Verbindung wird wiederhergestellt...",
|
||||
"settings": "Einstellungen",
|
||||
"unencrypted": "Nicht verschlüsselt",
|
||||
"username": "Benutzername",
|
||||
@@ -82,7 +83,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Anrufe werden nicht unterstützt",
|
||||
"call_not_found": "Anruf nicht gefunden",
|
||||
"call_not_found_description": "<0>Dieser Link scheint zu keinem bestehenden Anruf zu gehören. Vergewissern Sie sich, dass Sie den richtigen Link haben, oder <1> erstellen Sie einen neuen</1>. </0>",
|
||||
"call_not_found_description": "<0>Dieser Link scheint zu keinem bestehenden Anruf zu gehören. Es sollte geprüft werden, ob der Link korrekt ist, oder <2>ein neuer erstellt werden</2>.</0>",
|
||||
"connection_lost": "Verbindung verloren",
|
||||
"connection_lost_description": "Ihre Verbindung zum Anruf wurde unterbrochen.",
|
||||
"e2ee_unsupported": "Inkompatibler Browser",
|
||||
@@ -94,6 +95,8 @@
|
||||
"matrix_rtc_focus_missing": "Der Server ist nicht für die Verwendung mit {{brand}} konfiguriert. Bitte den Serveradministrator kontaktieren (Domain: {{domain}}, Fehlercode: {{ errorCode }}).",
|
||||
"open_elsewhere": "In einem anderen Tab geöffnet",
|
||||
"open_elsewhere_description": "{{brand}} wurde in einem anderen Tab geöffnet. Wenn das nicht richtig klingt, versuchen Sie, die Seite neu zu laden.",
|
||||
"room_creation_restricted": "Anruf konnte nicht erstellt werden",
|
||||
"room_creation_restricted_description": "Das Erstellen von Anrufen ist nur für autorisierte Nutzer möglich. Versuche es später erneut oder kontaktiere deinen Serveradministrator, falls das Problem weiterhin besteht.",
|
||||
"unexpected_ec_error": "Ein unerwarteter Fehler ist aufgetreten (<0>Fehlercode: </0> <1>{{ errorCode }}</1>). Bitte den Serveradministrator kontaktieren."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -105,6 +108,11 @@
|
||||
"knock_reject_heading": "Zugriff verweigert",
|
||||
"reason": "Grund: {{reason}}"
|
||||
},
|
||||
"handset": {
|
||||
"overlay_back_button": "Zurück zum Lautsprechermodus",
|
||||
"overlay_description": "Nur wenn App im Vordergrund nutzbar",
|
||||
"overlay_title": "Ohrhörer Modus"
|
||||
},
|
||||
"hangup_button_label": "Anruf beenden",
|
||||
"header_label": "Element Call-Startseite",
|
||||
"header_participants_label": "Teilnehmende",
|
||||
@@ -173,9 +181,11 @@
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Audiogerät wechseln",
|
||||
"default": "Standard",
|
||||
"default_named": "Standard<2> ({{name}} )</2>",
|
||||
"handset": "Ohrhörer",
|
||||
"loudspeaker": "Lautsprecher",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon{{n}}",
|
||||
"speaker": "Lautsprecher",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"profile": "Profile",
|
||||
"reaction": "Reaction",
|
||||
"reactions": "Reactions",
|
||||
"reconnecting": "Reconnecting…",
|
||||
"settings": "Settings",
|
||||
"unencrypted": "Not encrypted",
|
||||
"username": "Username",
|
||||
@@ -94,6 +95,8 @@
|
||||
"matrix_rtc_focus_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
|
||||
"open_elsewhere": "Opened in another tab",
|
||||
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
|
||||
"room_creation_restricted": "Failed to create call",
|
||||
"room_creation_restricted_description": "Call creation might be restricted to authorized users only. Try again later, or contact your server admin if the problem persists.",
|
||||
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server admin."
|
||||
},
|
||||
"group_call_loader": {
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Näita iPhone'i kuulari valikut kõikidel platvormidel",
|
||||
"crypto_version": "Krüptoteekide versioon: {{version}}",
|
||||
"debug_tile_layout_label": "Meediapaanide paigutus",
|
||||
"device_id": "Seadme tunnus: {{id}}",
|
||||
@@ -81,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Kõne pole toetatud",
|
||||
"call_not_found": "Kõnet ei leidu",
|
||||
"call_not_found_description": "<0>See link ei tundu olema seotud ühegi olemasoleva kõnega. Kontrolli, et sul on õige link või <1>loo uus</1>.</0>",
|
||||
"call_not_found_description": "<0>See link ei tundu olema seotud ühegi olemasoleva kõnega. Kontrolli, et sul on õige link või <2>loo uus</2>.</0>",
|
||||
"connection_lost": "Ühendus on katkenud",
|
||||
"connection_lost_description": "Sinu ühendus selle kõnega on katkenud.",
|
||||
"e2ee_unsupported": "Mitteühilduv brauser",
|
||||
@@ -93,6 +94,8 @@
|
||||
"matrix_rtc_focus_missing": "See server pole seadistatud töötama rakendusega {{brand}}. Palun võta ühendust serveri halduriga (domeen: {{domain}}, veakood: {{ errorCode }}).",
|
||||
"open_elsewhere": "Avatud teisel vahekaardil",
|
||||
"open_elsewhere_description": "{{brand}} on avatud teisel vahekaardil. Kui see ei tundu olema õige, proovi selle lehe uuesti laadimist.",
|
||||
"room_creation_restricted": "Kõne loomine ei õnnestunud",
|
||||
"room_creation_restricted_description": "Kõne loomine võib olla lubatud ainult volitatud kasutajatele. Proovi hiljem uuesti või probleemi püsimisel võta ühendust oma serveri haldajaga.",
|
||||
"unexpected_ec_error": "Tekkis ootamatu viga (<0>Veakood:</0> <1>{{ errorCode }}</1>). Palun võta ühendust serveri haldajaga."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -104,6 +107,10 @@
|
||||
"knock_reject_heading": "Liitumine pole lubatud",
|
||||
"reason": "Põhjus"
|
||||
},
|
||||
"handset": {
|
||||
"overlay_back_button": "Tagasi esineja vaatesse",
|
||||
"overlay_description": "See toimib vaid rakenduse kasutamise ajal"
|
||||
},
|
||||
"hangup_button_label": "Lõpeta kõne",
|
||||
"header_label": "Avaleht: Element Call",
|
||||
"header_participants_label": "Osalejad",
|
||||
@@ -172,8 +179,10 @@
|
||||
"devices": {
|
||||
"camera": "Kaamera",
|
||||
"camera_numbered": "Kaamera {{n}}",
|
||||
"change_device_button": "Muuda heliseadet",
|
||||
"default": "Vaikimisi",
|
||||
"default_named": "Vaikimisi <2>({{name}})</2>",
|
||||
"loudspeaker": "Valjuhääldi",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Kõlar",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Видео"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Показать опцию наушников для iPhone на всех платформах",
|
||||
"crypto_version": "Версия криптографии: {{version}}",
|
||||
"debug_tile_layout_label": "Отладка расположения плиток",
|
||||
"device_id": "Идентификатор устройства: {{id}}",
|
||||
@@ -70,6 +71,7 @@
|
||||
"livekit_server_info": "Информация о сервере LiveKit",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix ID: {{id}}",
|
||||
"mute_all_audio": "Отключить все звуки (участников, реакции, звуки присоединения)",
|
||||
"show_connection_stats": "Показать статистику подключений",
|
||||
"show_non_member_tiles": "Показать плитки для медиафайлов, не являющихся участниками",
|
||||
"url_params": "Параметры URL-адреса",
|
||||
@@ -172,6 +174,7 @@
|
||||
"devices": {
|
||||
"camera": "Камера",
|
||||
"camera_numbered": "Камера {{n}}",
|
||||
"change_device_button": "Изменить аудиоустройство",
|
||||
"default": "По умолчанию",
|
||||
"default_named": "По умолчанию <2>({{name}})</2>",
|
||||
"microphone": "Микрофон",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Zobraziť možnosť slúchadla iPhone na všetkých platformách",
|
||||
"crypto_version": "Krypto verzia: {{version}}",
|
||||
"debug_tile_layout_label": "Ladenie rozloženia dlaždíc",
|
||||
"device_id": "ID zariadenia: {{id}}",
|
||||
@@ -81,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Hovor nie je podporovaný",
|
||||
"call_not_found": "Hovor nebol nájdený",
|
||||
"call_not_found_description": "<0>Zdá sa, že tento odkaz nepatrí k žiadnemu existujúcemu hovoru. Skontrolujte, či máte správny odkaz, alebo <1> vytvorte nový</1>. </0>",
|
||||
"call_not_found_description": "<0>Zdá sa, že tento odkaz nepatrí k žiadnemu existujúcemu hovoru. Skontrolujte, či máte správny odkaz, alebo <2>vytvorte nový</2>.</0>",
|
||||
"connection_lost": "Strata spojenia",
|
||||
"connection_lost_description": "Boli ste odpojení od hovoru.",
|
||||
"e2ee_unsupported": "Nekompatibilný prehliadač",
|
||||
@@ -173,8 +174,10 @@
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Zmeniť zvukové zariadenie",
|
||||
"default": "Predvolené",
|
||||
"default_named": "Predvolené <2>({{name}})</2>",
|
||||
"loudspeaker": "Reproduktor",
|
||||
"microphone": "Mikrofón",
|
||||
"microphone_numbered": "Mikrofón {{n}}",
|
||||
"speaker": "Reproduktor",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"developer_mode": {
|
||||
"always_show_iphone_earpiece": "Visa iPhone-hörsnäckealternativ på alla plattformar",
|
||||
"crypto_version": "Kryptoversion: {{version}}",
|
||||
"debug_tile_layout_label": "Felsök panelarrangemang",
|
||||
"device_id": "Enhets-ID: {{id}}",
|
||||
@@ -70,6 +71,7 @@
|
||||
"livekit_server_info": "LiveKit-serverinfo",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
"matrix_id": "Matrix-ID: {{id}}",
|
||||
"mute_all_audio": "Tysta allt ljud (deltagare, reaktioner, anslutningsljud)",
|
||||
"show_connection_stats": "Visa anslutningsstatistik",
|
||||
"show_non_member_tiles": "Visa paneler för media som inte är medlemmar",
|
||||
"url_params": "URL-parametrar",
|
||||
@@ -80,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Call stöds inte",
|
||||
"call_not_found": "Samtal hittades inte",
|
||||
"call_not_found_description": "<0>Den länken verkar inte tillhöra något befintligt samtal. Kontrollera att du har rätt länk, eller <1>skapa en ny</1>.</0>",
|
||||
"call_not_found_description": "<0>Den länken verkar inte tillhöra något befintligt samtal. Kontrollera att du har rätt länk, eller <2>skapa en ny</2>.</0>",
|
||||
"connection_lost": "Anslutning förlorad",
|
||||
"connection_lost_description": "Du kopplades bort från samtalet.",
|
||||
"e2ee_unsupported": "Inkompatibel webbläsare",
|
||||
@@ -92,6 +94,8 @@
|
||||
"matrix_rtc_focus_missing": "Servern är inte konfigurerad för att fungera med {{brand}}. Vänligen kontakta serveradministratören (Domän: {{domain}}, Felkod: {{ errorCode }}).",
|
||||
"open_elsewhere": "Öppnades i en annan flik",
|
||||
"open_elsewhere_description": "{{brand}} har öppnats i en annan flik. Om det inte låter rätt, pröva att ladda om sidan.",
|
||||
"room_creation_restricted": "Misslyckades att skapa samtal",
|
||||
"room_creation_restricted_description": "Samtalsskapande kan vara begränsat till endast behöriga användare. Försök igen senare eller kontakta serveradministratören om problemet kvarstår.",
|
||||
"unexpected_ec_error": "Ett oväntat fel inträffade (<0>Felkod:</0> <1>{{ errorCode }}</1>). Kontakta din serveradministratör."
|
||||
},
|
||||
"group_call_loader": {
|
||||
@@ -103,6 +107,11 @@
|
||||
"knock_reject_heading": "Inte tillåten att gå med",
|
||||
"reason": "Anledning"
|
||||
},
|
||||
"handset": {
|
||||
"overlay_back_button": "Tillbaka till högtalarläge",
|
||||
"overlay_description": "Fungerar bara när appen används",
|
||||
"overlay_title": "Telefonläge"
|
||||
},
|
||||
"hangup_button_label": "Avsluta samtal",
|
||||
"header_label": "Element Call Hem",
|
||||
"header_participants_label": "Deltagare",
|
||||
@@ -164,12 +173,18 @@
|
||||
"effect_volume_description": "Justera volymen vid vilken reaktioner och handuppräckningseffekter spelas",
|
||||
"effect_volume_label": "Ljudeffektsvolym"
|
||||
},
|
||||
"background_blur_header": "Bakgrund",
|
||||
"background_blur_label": "Gör bakgrunden i videon suddig",
|
||||
"blur_not_supported_by_browser": "(Bakgrundssuddighet stöds inte av den här enheten.)",
|
||||
"developer_tab_title": "Utvecklare",
|
||||
"devices": {
|
||||
"camera": "Kamera",
|
||||
"camera_numbered": "Kamera {{n}}",
|
||||
"change_device_button": "Byt ljudenhet",
|
||||
"default": "Förval",
|
||||
"default_named": "Förval <2>({{name}})</2>",
|
||||
"handset": "Telefonlur",
|
||||
"loudspeaker": "Högtalare",
|
||||
"microphone": "Mikrofon",
|
||||
"microphone_numbered": "Mikrofon {{n}}",
|
||||
"speaker": "Högtalare",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@mediapipe/tasks-vision": "^0.10.18",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vector-im/compound-design-tokens": "^5.0.0",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
@@ -129,7 +129,7 @@
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"vaul": "^1.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-generate-file": "^0.3.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-svgr": "^4.0.0",
|
||||
|
||||
@@ -2,3 +2,6 @@ services:
|
||||
synapse:
|
||||
volumes:
|
||||
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
|
||||
synapse-1:
|
||||
volumes:
|
||||
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||
|
||||
@@ -40,6 +40,11 @@ test("Start a new call then leave and show the feedback screen", async ({
|
||||
// The tooltip with the name should be visible
|
||||
await expect(page.getByTestId("name_tag")).toContainText("John Doe");
|
||||
|
||||
// Resize the window to resemble a small mobile phone
|
||||
await page.setViewportSize({ width: 350, height: 660 });
|
||||
// We should still be able to send reactions at this screen size
|
||||
await expect(page.getByRole("button", { name: "Reactions" })).toBeVisible();
|
||||
|
||||
// leave the call
|
||||
await page.getByTestId("incall_leave").click();
|
||||
await expect(page.getByRole("heading")).toContainText(
|
||||
|
||||
@@ -72,3 +72,56 @@ test("Should automatically retry non fatal JWT errors", async ({
|
||||
await hasRetriedPromise;
|
||||
await expect(page.getByTestId("video").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("Should show error screen if call creation is restricted", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// We need the socket connection to fail, but this cannot be done by using the websocket route.
|
||||
// Instead, we will trick the app by returning a bad URL for the SFU that will not be reachable an error out.
|
||||
await page.route(
|
||||
"**/matrix-rtc.m.localhost/livekit/jwt/sfu/get",
|
||||
async (route) =>
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
url: "wss://badurltotricktest/livekit/sfu",
|
||||
jwt: "FAKE",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Then if the socket connection fails, livekit will try to validate the token!
|
||||
// Livekit will not auto_create anymore and will return a 404 error.
|
||||
await page.route(
|
||||
"**/badurltotricktest/livekit/sfu/rtc/validate?**",
|
||||
async (route) =>
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
contentType: "text/plain",
|
||||
body: "requested room does not exist",
|
||||
}),
|
||||
);
|
||||
|
||||
await page.pause();
|
||||
|
||||
await page.getByTestId("home_callName").click();
|
||||
await page.getByTestId("home_callName").fill("HelloCall");
|
||||
await page.getByTestId("home_displayName").click();
|
||||
await page.getByTestId("home_displayName").fill("John Doe");
|
||||
await page.getByTestId("home_go").click();
|
||||
|
||||
// Join the call
|
||||
await page.getByTestId("lobby_joinCall").click();
|
||||
|
||||
await page.pause();
|
||||
// Should fail
|
||||
await expect(page.getByText("Failed to create call")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
/Call creation might be restricted to authorized users only/,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -159,8 +159,8 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
} = await registerUser(browser, userB);
|
||||
|
||||
// Invite the second user
|
||||
await ewPage1.getByRole("button", { name: "Add room" }).click();
|
||||
await ewPage1.getByText("New room").click();
|
||||
await ewPage1.getByRole("button", { name: "Add", exact: true }).click();
|
||||
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
|
||||
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
|
||||
await ewPage1.getByRole("button", { name: "Create room" }).click();
|
||||
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
|
||||
@@ -184,9 +184,9 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
|
||||
// Accept the invite
|
||||
await expect(
|
||||
ewPage2.getByRole("treeitem", { name: "Welcome Room" }),
|
||||
ewPage2.getByRole("option", { name: "Welcome Room" }),
|
||||
).toBeVisible();
|
||||
await ewPage2.getByRole("treeitem", { name: "Welcome Room" }).click();
|
||||
await ewPage2.getByRole("option", { name: "Welcome Room" }).click();
|
||||
await ewPage2.getByRole("button", { name: "Accept" }).click();
|
||||
await expect(
|
||||
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
|
||||
|
||||
60
playwright/reconnect.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// Skip test for Firefox, due to page.keyboard.press("Tab") not reliable on headless mode
|
||||
test.skip(
|
||||
({ browserName }) => browserName === "firefox",
|
||||
'This test is not working on firefox, page.keyboard.press("Tab") not reliable in headless mode',
|
||||
);
|
||||
|
||||
test("can only interact with header and footer while reconnecting", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("home_callName").click();
|
||||
await page.getByTestId("home_callName").fill("Test call");
|
||||
await page.getByTestId("home_displayName").click();
|
||||
await page.getByTestId("home_displayName").fill("Test user");
|
||||
// If we do not call fastForward here, we end up with Date.now() returning an actual timestamp
|
||||
// but once we call `await page.clock.fastForward(20000);` later this will reset Date.now() to 0
|
||||
// and we will never get into probablyDisconnected state?
|
||||
await page.clock.fastForward(10);
|
||||
await page.getByTestId("home_go").click();
|
||||
|
||||
await expect(page.locator("video")).toBeVisible();
|
||||
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
||||
|
||||
// Join the call
|
||||
await page.getByTestId("lobby_joinCall").click();
|
||||
|
||||
// The media tile for the local user should become visible
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
await expect(page.getByTestId("name_tag")).toContainText("Test user");
|
||||
|
||||
// Now disconnect from the internet
|
||||
await page.route("https://synapse.m.localhost/**/*", async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
await route.continue();
|
||||
});
|
||||
await page.clock.fastForward(20000);
|
||||
|
||||
await expect(
|
||||
page.getByRole("dialog", { name: "Reconnecting…" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Tab order should jump directly from header to footer, skipping media tiles
|
||||
await page.getByRole("button", { name: "Mute microphone" }).focus();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Mute microphone" }),
|
||||
).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.getByRole("button", { name: "Stop video" })).toBeFocused();
|
||||
// Most critically, we should be able to press the hangup button
|
||||
await page.getByRole("button", { name: "End call" }).click();
|
||||
});
|
||||
75
playwright/restricted-sfu.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { sleep } from "matrix-js-sdk/lib/utils.js";
|
||||
|
||||
test("Should request JWT token before starting the call", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
let sfGetTimestamp = 0;
|
||||
let sendStateEventTimestamp = 0;
|
||||
await page.route(
|
||||
"**/matrix-rtc.m.localhost/livekit/jwt/sfu/get",
|
||||
async (route) => {
|
||||
await sleep(2000); // Simulate very slow request
|
||||
await route.continue();
|
||||
sfGetTimestamp = Date.now();
|
||||
},
|
||||
);
|
||||
|
||||
await page.route(
|
||||
"**/state/org.matrix.msc3401.call.member/**",
|
||||
async (route) => {
|
||||
await route.continue();
|
||||
sendStateEventTimestamp = Date.now();
|
||||
},
|
||||
);
|
||||
|
||||
await page.getByTestId("home_callName").click();
|
||||
await page.getByTestId("home_callName").fill("HelloCall");
|
||||
await page.getByTestId("home_displayName").click();
|
||||
await page.getByTestId("home_displayName").fill("John Doe");
|
||||
await page.getByTestId("home_go").click();
|
||||
|
||||
// Join the call
|
||||
await page.getByTestId("lobby_joinCall").click();
|
||||
await page.waitForTimeout(4000);
|
||||
// Ensure that the call is connected
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^HelloCall$/ })
|
||||
.click();
|
||||
|
||||
expect(sfGetTimestamp).toBeGreaterThan(0);
|
||||
expect(sendStateEventTimestamp).toBeGreaterThan(0);
|
||||
expect(sfGetTimestamp).toBeLessThan(sendStateEventTimestamp);
|
||||
});
|
||||
|
||||
test("Error when pre-warming the focus are caught by the ErrorBoundary", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.route("**/openid/request_token", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 418, // Simulate an error not retryable
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId("home_callName").click();
|
||||
await page.getByTestId("home_callName").fill("HelloCall");
|
||||
await page.getByTestId("home_displayName").click();
|
||||
await page.getByTestId("home_displayName").fill("John Doe");
|
||||
await page.getByTestId("home_go").click();
|
||||
|
||||
// Join the call
|
||||
await page.getByTestId("lobby_joinCall").click();
|
||||
|
||||
// Should fail
|
||||
await expect(page.getByText("Something went wrong")).toBeVisible();
|
||||
});
|
||||
@@ -100,5 +100,5 @@ test("When creator left, avoid reconnect to the same SFU", async ({
|
||||
// https://github.com/element-hq/element-call/issues/3344
|
||||
// The app used to request a new jwt token then to reconnect to the SFU
|
||||
expect(wsConnectionCount).toBe(1);
|
||||
expect(sfuGetCallCount).toBe(1);
|
||||
expect(sfuGetCallCount).toBe(2 /* the first one is for the warmup */);
|
||||
});
|
||||
|
||||
@@ -49,7 +49,10 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
||||
|
||||
// Check the join indicator on the room list
|
||||
await expect(
|
||||
brooks.page.locator("div").filter({ hasText: /^Joined • 1$/ }),
|
||||
brooks.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByRole("button", { name: "End call" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Join from the other side
|
||||
@@ -59,26 +62,28 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
||||
).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Join" }).click();
|
||||
|
||||
await expect(
|
||||
whistler.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall"),
|
||||
).toBeVisible();
|
||||
// Currently disabled due to recent Element Web is bypassing Lobby
|
||||
// await expect(
|
||||
// whistler.page
|
||||
// .locator('iframe[title="Element Call"]')
|
||||
// .contentFrame()
|
||||
// .getByTestId("lobby_joinCall"),
|
||||
// ).toBeVisible();
|
||||
//
|
||||
// await whistler.page
|
||||
// .locator('iframe[title="Element Call"]')
|
||||
// .contentFrame()
|
||||
// .getByTestId("lobby_joinCall")
|
||||
// .click();
|
||||
|
||||
await whistler.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall")
|
||||
.click();
|
||||
// Currrenty disabled due to recent Element Web not indicating the number of participants
|
||||
// await expect(
|
||||
// whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
// ).toBeVisible();
|
||||
|
||||
await expect(
|
||||
whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
).toBeVisible();
|
||||
// await expect(
|
||||
// brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }),
|
||||
// ).toBeVisible();
|
||||
|
||||
// Whistler leaves
|
||||
await whistler.page.waitForTimeout(1000);
|
||||
|
||||
@@ -24,7 +24,6 @@ import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||
import { Initializer } from "./initializer";
|
||||
import { widget } from "./widget";
|
||||
import { useTheme } from "./useTheme";
|
||||
@@ -86,7 +85,6 @@ export const App: FC<Props> = ({ vm }) => {
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={(error) => <ErrorPage error={error} widget={widget} />}
|
||||
>
|
||||
<DisconnectedBanner />
|
||||
<Routes>
|
||||
<SentryRoute path="/" element={<HomePage />} />
|
||||
<SentryRoute path="/login" element={<LoginPage />} />
|
||||
|
||||
@@ -61,7 +61,11 @@ export const AppBar: FC<Props> = ({ children }) => {
|
||||
style={{ display: hidden ? "none" : "block" }}
|
||||
className={styles.bar}
|
||||
>
|
||||
<Header>
|
||||
<Header
|
||||
// App bar is mainly seen in the call view, which has its own
|
||||
// 'reconnecting' toast
|
||||
disconnectedBanner={false}
|
||||
>
|
||||
<LeftNav>
|
||||
<Tooltip label={t("common.back")}>
|
||||
<IconButton onClick={onBackClick}>
|
||||
|
||||
@@ -17,27 +17,38 @@ import Logo from "./icons/Logo.svg?react";
|
||||
import { Avatar, Size } from "./Avatar";
|
||||
import { EncryptionLock } from "./room/EncryptionLock";
|
||||
import { useMediaQuery } from "./useMediaQuery";
|
||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||
|
||||
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||
ref?: Ref<HTMLElement>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/**
|
||||
* Whether the header should display an informational banner whenever the
|
||||
* client is disconnected from the homeserver.
|
||||
* @default true
|
||||
*/
|
||||
disconnectedBanner?: boolean;
|
||||
}
|
||||
|
||||
export const Header: FC<HeaderProps> = ({
|
||||
ref,
|
||||
children,
|
||||
className,
|
||||
disconnectedBanner = true,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<header
|
||||
ref={ref}
|
||||
className={classNames(styles.header, className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
<>
|
||||
<header
|
||||
ref={ref}
|
||||
className={classNames(styles.header, className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
{disconnectedBanner && <DisconnectedBanner />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
.bg.animate[data-state="closed"] {
|
||||
animation: fade-out 130ms;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
|
||||
@@ -45,6 +45,12 @@ interface Props {
|
||||
* A supporting icon to display within the toast.
|
||||
*/
|
||||
Icon?: ComponentType<SVGAttributes<SVGElement>>;
|
||||
/**
|
||||
* Whether the toast should be modal, making it fill the screen (by portalling
|
||||
* it into the root of the document) and trap focus until dismissed.
|
||||
* @default true
|
||||
*/
|
||||
modal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +62,7 @@ export const Toast: FC<Props> = ({
|
||||
autoDismiss,
|
||||
children,
|
||||
Icon,
|
||||
modal = true,
|
||||
}) => {
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
@@ -71,29 +78,33 @@ export const Toast: FC<Props> = ({
|
||||
}
|
||||
}, [open, autoDismiss, onDismiss]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<DialogOverlay
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
/>
|
||||
<DialogContent aria-describedby={undefined} asChild>
|
||||
<DialogClose
|
||||
className={classNames(
|
||||
overlayStyles.overlay,
|
||||
overlayStyles.animate,
|
||||
styles.toast,
|
||||
)}
|
||||
>
|
||||
<DialogTitle asChild>
|
||||
<Text as="h3" size="sm" weight="semibold">
|
||||
{children}
|
||||
</Text>
|
||||
</DialogTitle>
|
||||
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
/>
|
||||
<DialogContent aria-describedby={undefined} asChild>
|
||||
<DialogClose
|
||||
className={classNames(
|
||||
overlayStyles.overlay,
|
||||
overlayStyles.animate,
|
||||
styles.toast,
|
||||
)}
|
||||
>
|
||||
<DialogTitle asChild>
|
||||
<Text as="h3" size="sm" weight="semibold">
|
||||
{children}
|
||||
</Text>
|
||||
</DialogTitle>
|
||||
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange} modal={modal}>
|
||||
{modal ? <DialogPortal>{content}</DialogPortal> : content}
|
||||
</DialogRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import * as PlatformMod from "../src/Platform";
|
||||
import {
|
||||
getRoomIdentifierFromUrl,
|
||||
getUrlParams,
|
||||
computeUrlParams,
|
||||
HeaderStyle,
|
||||
getUrlParams,
|
||||
} from "../src/UrlParams";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
@@ -103,16 +106,16 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("preload", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(getUrlParams().preload).toBe(false);
|
||||
expect(computeUrlParams().preload).toBe(false);
|
||||
});
|
||||
|
||||
it("ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?preload=true").preload).toBe(false);
|
||||
expect(computeUrlParams("?preload=true").preload).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).preload,
|
||||
).toBe(true);
|
||||
@@ -121,19 +124,20 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("returnToLobby", () => {
|
||||
it("is false in SPA mode", () => {
|
||||
expect(getUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
||||
expect(computeUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to false in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo")
|
||||
.returnToLobby,
|
||||
computeUrlParams(
|
||||
"?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
).toBe(true);
|
||||
@@ -142,12 +146,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("userId", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?userId=asd").userId).toBe(null);
|
||||
expect(computeUrlParams("?userId=asd").userId).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).userId,
|
||||
).toBe("asd");
|
||||
@@ -156,12 +160,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("deviceId", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||
expect(computeUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).deviceId,
|
||||
).toBe("asd");
|
||||
@@ -170,12 +174,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("baseUrl", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(getUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||
expect(computeUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).baseUrl,
|
||||
).toBe("asd");
|
||||
@@ -185,28 +189,28 @@ describe("UrlParams", () => {
|
||||
describe("viaServers", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).viaServers,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
expect(computeUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("homeserver", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).homeserver,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
expect(computeUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,7 +232,7 @@ describe("UrlParams", () => {
|
||||
const startNewCallDefaults = (platform: string): object => ({
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: true,
|
||||
preload: false,
|
||||
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
@@ -237,12 +241,12 @@ describe("UrlParams", () => {
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
sendNotificationType: platform === "desktop" ? "notification" : "ring",
|
||||
});
|
||||
const joinExistingCallDefaults = (platform: string): object => ({
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: true,
|
||||
preload: false,
|
||||
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
@@ -252,24 +256,55 @@ describe("UrlParams", () => {
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
defaultAudioEnabled: true,
|
||||
defaultVideoEnabled: true,
|
||||
});
|
||||
it("use no-intent-defaults with unknown intent", () => {
|
||||
expect(getUrlParams()).toMatchObject(noIntentDefaults);
|
||||
expect(computeUrlParams()).toMatchObject(noIntentDefaults);
|
||||
});
|
||||
|
||||
it("ignores intent if it is not a valid value", () => {
|
||||
expect(getUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||
expect(computeUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||
});
|
||||
|
||||
it("accepts start_call", () => {
|
||||
expect(
|
||||
getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org"),
|
||||
).toMatchObject(startNewCallDefaults("desktop"));
|
||||
computeUrlParams(
|
||||
"?intent=start_call&widgetId=1234&parentUrl=parent.org",
|
||||
),
|
||||
).toMatchObject({ ...startNewCallDefaults("desktop"), skipLobby: false });
|
||||
});
|
||||
|
||||
it("accepts start_call_dm mobile", () => {
|
||||
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("android");
|
||||
onTestFinished(() => {
|
||||
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("desktop");
|
||||
});
|
||||
expect(
|
||||
computeUrlParams(
|
||||
"?intent=start_call_dm&widgetId=1234&parentUrl=parent.org",
|
||||
),
|
||||
).toMatchObject(startNewCallDefaults("android"));
|
||||
});
|
||||
|
||||
it("accepts start_call_dm mobile and prioritizes overwritten params", () => {
|
||||
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("android");
|
||||
onTestFinished(() => {
|
||||
vi.spyOn(PlatformMod, "platform", "get").mockReturnValue("desktop");
|
||||
});
|
||||
expect(
|
||||
computeUrlParams(
|
||||
"?intent=start_call_dm&widgetId=1234&parentUrl=parent.org&sendNotificationType=notification",
|
||||
),
|
||||
).toMatchObject({
|
||||
...startNewCallDefaults("android"),
|
||||
sendNotificationType: "notification",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts join_existing", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
||||
),
|
||||
).toMatchObject(joinExistingCallDefaults("desktop"));
|
||||
@@ -278,31 +313,55 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("skipLobby", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(getUrlParams().skipLobby).toBe(false);
|
||||
expect(computeUrlParams().skipLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to false if intent is start_call in SPA mode", () => {
|
||||
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||
expect(computeUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to true if intent is start_call in widget mode", () => {
|
||||
it("defaults to false if intent is start_call in widget mode", () => {
|
||||
expect(
|
||||
getUrlParams(
|
||||
computeUrlParams(
|
||||
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).skipLobby,
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("default to false if intent is join_existing", () => {
|
||||
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||
expect(computeUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||
});
|
||||
});
|
||||
describe("header", () => {
|
||||
it("uses header if provided", () => {
|
||||
expect(getUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||
expect(computeUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||
"app_bar",
|
||||
);
|
||||
expect(getUrlParams("?header=none&hideHeader=false").header).toBe("none");
|
||||
expect(computeUrlParams("?header=none&hideHeader=false").header).toBe(
|
||||
"none",
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("getUrlParams", () => {
|
||||
it("uses cached values", () => {
|
||||
const spy = vi.spyOn(logger, "info");
|
||||
// call get once
|
||||
const params = getUrlParams("?header=app_bar&hideHeader=true", "");
|
||||
// call get twice
|
||||
expect(getUrlParams("?header=app_bar&hideHeader=true", "")).toBe(params);
|
||||
// expect compute to only be called once
|
||||
// it will only log when it is computing the values
|
||||
expect(spy).toHaveBeenCalledExactlyOnceWith(
|
||||
"UrlParams: final set of url params\n",
|
||||
"intent:",
|
||||
"unknown",
|
||||
"\nproperties:",
|
||||
expect.any(Object),
|
||||
"configuration:",
|
||||
expect.any(Object),
|
||||
"intentAndPlatformDerivedConfiguration:",
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
161
src/UrlParams.ts
@@ -8,7 +8,10 @@ Please see LICENSE in the repository root for full details.
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type RTCNotificationType } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
type RTCCallIntent,
|
||||
type RTCNotificationType,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { pickBy } from "lodash-es";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
@@ -23,9 +26,12 @@ interface RoomIdentifier {
|
||||
}
|
||||
|
||||
export enum UserIntent {
|
||||
// TODO: add DM vs room call
|
||||
StartNewCall = "start_call",
|
||||
JoinExistingCall = "join_existing",
|
||||
StartNewCallDM = "start_call_dm",
|
||||
StartNewCallDMVoice = "start_call_dm_voice",
|
||||
JoinExistingCallDM = "join_existing_dm",
|
||||
JoinExistingCallDMVoice = "join_existing_dm_voice",
|
||||
Unknown = "unknown",
|
||||
}
|
||||
|
||||
@@ -209,6 +215,33 @@ export interface UrlConfiguration {
|
||||
* Whether and what type of notification EC should send, when the user joins the call.
|
||||
*/
|
||||
sendNotificationType?: RTCNotificationType;
|
||||
/**
|
||||
* Whether the app should automatically leave the call when there
|
||||
* is no one left in the call.
|
||||
* This is one part to make the call matrixRTC session behave like a telephone call.
|
||||
*/
|
||||
autoLeaveWhenOthersLeft: boolean;
|
||||
|
||||
/**
|
||||
* If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up).
|
||||
* This is a no-op if not combined with sendNotificationType.
|
||||
*
|
||||
* This entails:
|
||||
* - show ui that it is awaiting an answer
|
||||
* - play a sound that indicates that it is awaiting an answer
|
||||
* - auto-dismiss the call widget once the notification lifetime expires on the receivers side.
|
||||
*/
|
||||
waitForCallPickup: boolean;
|
||||
|
||||
callIntent?: RTCCallIntent;
|
||||
}
|
||||
interface IntentAndPlatformDerivedConfiguration {
|
||||
defaultAudioEnabled?: boolean;
|
||||
defaultVideoEnabled?: boolean;
|
||||
}
|
||||
interface IntentAndPlatformDerivedConfiguration {
|
||||
defaultAudioEnabled?: boolean;
|
||||
defaultVideoEnabled?: boolean;
|
||||
}
|
||||
|
||||
// If you need to add a new flag to this interface, prefer a name that describes
|
||||
@@ -216,7 +249,10 @@ export interface UrlConfiguration {
|
||||
// the situations that call for this behavior ('isEmbedded'). This makes it
|
||||
// clearer what each flag means, and helps us avoid coupling Element Call's
|
||||
// behavior to the needs of specific consumers.
|
||||
export interface UrlParams extends UrlProperties, UrlConfiguration {}
|
||||
export interface UrlParams
|
||||
extends UrlProperties,
|
||||
UrlConfiguration,
|
||||
IntentAndPlatformDerivedConfiguration {}
|
||||
|
||||
// This is here as a stopgap, but what would be far nicer is a function that
|
||||
// takes a UrlParams and returns a query string. That would enable us to
|
||||
@@ -276,18 +312,30 @@ class ParamParser {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the flag exists and is not "false".
|
||||
*/
|
||||
public getFlagParam(name: string, defaultValue = false): boolean {
|
||||
const param = this.getParam(name);
|
||||
return param === null ? defaultValue : param !== "false";
|
||||
}
|
||||
/**
|
||||
* Returns the value of the flag if it exists, or undefined if it does not.
|
||||
*/
|
||||
public getFlag(name: string): boolean | undefined {
|
||||
const param = this.getParam(name);
|
||||
return param !== null ? param !== "false" : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let urlParamCache: {
|
||||
search?: string;
|
||||
hash?: string;
|
||||
params?: UrlParams;
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* Gets the app parameters for the current URL.
|
||||
* Gets the url params and loads them from a cache if already computed.
|
||||
* @param search The URL search string
|
||||
* @param hash The URL hash
|
||||
* @returns The app parameters encoded in the URL
|
||||
@@ -296,6 +344,26 @@ export const getUrlParams = (
|
||||
search = window.location.search,
|
||||
hash = window.location.hash,
|
||||
): UrlParams => {
|
||||
if (
|
||||
urlParamCache.search === search &&
|
||||
urlParamCache.hash === hash &&
|
||||
urlParamCache.params
|
||||
) {
|
||||
return urlParamCache.params;
|
||||
}
|
||||
const params = computeUrlParams(search, hash);
|
||||
urlParamCache = { search, hash, params };
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the app parameters for the current URL.
|
||||
* @param search The URL search string
|
||||
* @param hash The URL hash
|
||||
* @returns The app parameters encoded in the URL
|
||||
*/
|
||||
export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
const parser = new ParamParser(search, hash);
|
||||
|
||||
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||
@@ -319,11 +387,10 @@ export const getUrlParams = (
|
||||
? UserIntent.Unknown
|
||||
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
||||
// Here we only use constants and `platform` to determine the intent preset.
|
||||
let intentPreset: UrlConfiguration;
|
||||
const inAppDefault = {
|
||||
let intentPreset: UrlConfiguration = {
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: true,
|
||||
preload: false,
|
||||
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
|
||||
showControls: true,
|
||||
hideScreensharing: false,
|
||||
@@ -332,20 +399,38 @@ export const getUrlParams = (
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification" as RTCNotificationType,
|
||||
sendNotificationType: "notification",
|
||||
autoLeaveWhenOthersLeft: false,
|
||||
waitForCallPickup: false,
|
||||
};
|
||||
switch (intent) {
|
||||
case UserIntent.StartNewCall:
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: true,
|
||||
};
|
||||
intentPreset.skipLobby = false;
|
||||
intentPreset.callIntent = "video";
|
||||
break;
|
||||
case UserIntent.JoinExistingCall:
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: false,
|
||||
};
|
||||
// On desktop this will be overridden based on which button was used to join the call
|
||||
intentPreset.skipLobby = false;
|
||||
intentPreset.callIntent = "video";
|
||||
break;
|
||||
case UserIntent.StartNewCallDMVoice:
|
||||
intentPreset.callIntent = "audio";
|
||||
// Fall through
|
||||
case UserIntent.StartNewCallDM:
|
||||
intentPreset.skipLobby = true;
|
||||
intentPreset.sendNotificationType = "ring";
|
||||
intentPreset.autoLeaveWhenOthersLeft = true;
|
||||
intentPreset.waitForCallPickup = true;
|
||||
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
||||
break;
|
||||
case UserIntent.JoinExistingCallDMVoice:
|
||||
intentPreset.callIntent = "audio";
|
||||
// Fall through
|
||||
case UserIntent.JoinExistingCallDM:
|
||||
// On desktop this will be overridden based on which button was used to join the call
|
||||
intentPreset.skipLobby = true;
|
||||
intentPreset.autoLeaveWhenOthersLeft = true;
|
||||
intentPreset.callIntent = intentPreset.callIntent ?? "video";
|
||||
break;
|
||||
// Non widget usecase defaults
|
||||
default:
|
||||
@@ -362,9 +447,34 @@ export const getUrlParams = (
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: undefined,
|
||||
autoLeaveWhenOthersLeft: false,
|
||||
waitForCallPickup: false,
|
||||
};
|
||||
}
|
||||
|
||||
const intentAndPlatformDerivedConfiguration: IntentAndPlatformDerivedConfiguration =
|
||||
{};
|
||||
// Desktop also includes web. Its anything that is not mobile.
|
||||
const desktopMobile = platform === "desktop" ? "desktop" : "mobile";
|
||||
switch (desktopMobile) {
|
||||
case "desktop":
|
||||
case "mobile":
|
||||
switch (intent) {
|
||||
case UserIntent.StartNewCall:
|
||||
case UserIntent.JoinExistingCall:
|
||||
case UserIntent.StartNewCallDM:
|
||||
case UserIntent.JoinExistingCallDM:
|
||||
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
||||
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = true;
|
||||
break;
|
||||
case UserIntent.StartNewCallDMVoice:
|
||||
case UserIntent.JoinExistingCallDMVoice:
|
||||
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
||||
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const properties: UrlProperties = {
|
||||
widgetId,
|
||||
parentUrl,
|
||||
@@ -413,12 +523,29 @@ export const getUrlParams = (
|
||||
"ring",
|
||||
"notification",
|
||||
]),
|
||||
waitForCallPickup: parser.getFlag("waitForCallPickup"),
|
||||
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
||||
};
|
||||
|
||||
// Log the final configuration for debugging purposes.
|
||||
// This will only log when the cache is not yet set.
|
||||
logger.info(
|
||||
"UrlParams: final set of url params\n",
|
||||
"intent:",
|
||||
intent,
|
||||
"\nproperties:",
|
||||
properties,
|
||||
"configuration:",
|
||||
configuration,
|
||||
"intentAndPlatformDerivedConfiguration:",
|
||||
intentAndPlatformDerivedConfiguration,
|
||||
);
|
||||
|
||||
return {
|
||||
...properties,
|
||||
...intentPreset,
|
||||
...pickBy(configuration, (v) => v !== undefined),
|
||||
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
||||
...intentAndPlatformDerivedConfiguration,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
EndCallIcon,
|
||||
ShareScreenSolidIcon,
|
||||
SettingsSolidIcon,
|
||||
SwitchCameraSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import styles from "./Button.module.css";
|
||||
@@ -67,23 +66,6 @@ export const VideoButton: FC<VideoButtonProps> = ({ muted, ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SwitchCameraButton: FC<ComponentPropsWithoutRef<"button">> = (
|
||||
props,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip label={t("switch_camera")}>
|
||||
<CpdButton
|
||||
iconOnly
|
||||
Icon={SwitchCameraSolidIcon}
|
||||
kind="secondary"
|
||||
{...props}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`Can close reaction dialog 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby="«rb5»"
|
||||
aria-labelledby="«rbb»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
|
||||
@@ -114,16 +114,12 @@ export interface ConfigOptions {
|
||||
* when someone leaves a call.
|
||||
*/
|
||||
wait_for_key_rotation_ms?: number;
|
||||
/** @deprecated use wait_for_key_rotation_ms instead */
|
||||
key_rotation_on_leave_delay?: number;
|
||||
|
||||
/**
|
||||
* The duration (in milliseconds) after the most recent keep-alive (delayed leave event restart)
|
||||
* that the server waits before sending the leave MatrixRTC membership event.
|
||||
*/
|
||||
delayed_leave_event_delay_ms?: number;
|
||||
/** @deprecated use delayed_leave_event_delay_ms instead */
|
||||
membership_server_side_expiry_timeout?: number;
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) after which a we consider a delayed event restart http request to have failed.
|
||||
@@ -141,8 +137,6 @@ export interface ConfigOptions {
|
||||
* messages to the server by restarting the timer for the delayed leave event.
|
||||
*/
|
||||
delayed_leave_event_restart_ms?: number;
|
||||
/** @deprecated use delayed_leave_event_restart_ms instead */
|
||||
membership_keep_alive_period?: number;
|
||||
|
||||
/**
|
||||
* How long we wait before retrying after a network error on any of the requests.
|
||||
|
||||
@@ -6,9 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Subject } from "rxjs";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
const logger = rootLogger.getChild("[controlled-output]");
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
export interface Controls {
|
||||
canEnterPip(): boolean;
|
||||
|
||||
@@ -61,7 +61,12 @@ const TileWrapper_ = memo(
|
||||
useDrag((state) => onDrag?.current!(id, state), {
|
||||
target: ref,
|
||||
filterTaps: true,
|
||||
preventScroll: true,
|
||||
// Previous designs, which allowed tiles to be dragged and dropped around
|
||||
// the scrolling grid, required us to set preventScroll to true here. But
|
||||
// our designs no longer call for this, and meanwhile there's a bug in
|
||||
// use-gesture that causes filterTaps + preventScroll to break buttons
|
||||
// within tiles (like the 'switch camera' button) on mobile.
|
||||
// https://github.com/pmndrs/use-gesture/issues/593
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
6
src/icons/FullScreenMaximise.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 14C4.55228 14 5 14.4477 5 15V19H9C9.55228 19 10 19.4477 10 20C10 20.5523 9.55228 21 9 21H3V15C3 14.4477 3.44772 14 4 14Z"/>
|
||||
<path d="M20 14C20.5523 14 21 14.4477 21 15V21H15C14.4477 21 14 20.5523 14 20C14 19.4477 14.4477 19 15 19H19V15C19 14.4477 19.4477 14 20 14Z" />
|
||||
<path d="M9 3C9.55228 3 10 3.44772 10 4C10 4.55228 9.55228 5 9 5H5V9C5 9.55228 4.55228 10 4 10C3.44772 10 3 9.55228 3 9V3H9Z" />
|
||||
<path d="M21 9C21 9.55228 20.5523 10 20 10C19.4477 10 19 9.55228 19 9V5H15C14.4477 5 14 4.55228 14 4C14 3.44772 14.4477 3 15 3H21V9Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 658 B |
6
src/icons/FullScreenMinimise.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 20C10 20.5523 9.55228 21 9 21C8.44772 21 8 20.5523 8 20V16H4C3.44772 16 3 15.5523 3 15C3 14.4477 3.44772 14 4 14H10V20Z" />
|
||||
<path d="M20 14C20.5523 14 21 14.4477 21 15C21 15.5523 20.5523 16 20 16H16V20C16 20.5523 15.5523 21 15 21C14.4477 21 14 20.5523 14 20V14H20Z" />
|
||||
<path d="M9 3C9.55228 3 10 3.44772 10 4V10H4C3.44772 10 3 9.55228 3 9C3 8.44772 3.44772 8 4 8H8V4C8 3.44772 8.44772 3 9 3Z" />
|
||||
<path d="M15 3C15.5523 3 16 3.44772 16 4V8H20C20.5523 8 21 8.44772 21 9C21 9.55228 20.5523 10 20 10H14V4C14 3.44772 14.4477 3 15 3Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 656 B |
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2024-2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
type AudioTrackProps,
|
||||
} from "@livekit/components-react";
|
||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { useEarpieceAudioConfig } from "../MediaDevicesContext";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import * as controls from "../controls";
|
||||
|
||||
const logger = rootLogger.getChild("[MatrixAudioRenderer]");
|
||||
export interface MatrixAudioRendererProps {
|
||||
/**
|
||||
* The list of participants to render audio for.
|
||||
@@ -72,7 +71,7 @@ export function MatrixAudioRenderer({
|
||||
const logInvalid = (identity: string, validIdentities: Set<string>): void => {
|
||||
if (loggedInvalidIdentities.current.has(identity)) return;
|
||||
logger.warn(
|
||||
`Audio track ${identity} has no matching matrix call member`,
|
||||
`[MatrixAudioRenderer] Audio track ${identity} has no matching matrix call member`,
|
||||
`current members: ${Array.from(validIdentities.values())}`,
|
||||
`track will not get rendered`,
|
||||
);
|
||||
@@ -102,7 +101,7 @@ export function MatrixAudioRenderer({
|
||||
useEffect(() => {
|
||||
if (!tracks.some((t) => !validIdentities.has(t.participant.identity))) {
|
||||
logger.debug(
|
||||
`All audio tracks have a matching matrix call member identity.`,
|
||||
`[MatrixAudioRenderer] All audio tracks have a matching matrix call member identity.`,
|
||||
);
|
||||
loggedInvalidIdentities.current.clear();
|
||||
}
|
||||
@@ -182,7 +181,7 @@ interface StereoPanAudioTrackProps {
|
||||
/**
|
||||
* This wraps `livekit.AudioTrack` to allow adding audio nodes to a track.
|
||||
* It main purpose is to remount the AudioTrack component when switching from
|
||||
* audiooContext to normal audio playback.
|
||||
* audioContext to normal audio playback.
|
||||
* As of now the AudioTrack component does not support adding audio nodes while being mounted.
|
||||
* @param param0
|
||||
* @returns
|
||||
@@ -202,7 +201,7 @@ function AudioTrackWithAudioNodes({
|
||||
const [trackReady, setTrackReady] = useReactiveState(
|
||||
() => false,
|
||||
// We only want the track to reset once both (audioNodes and audioContext) are set.
|
||||
// for unsetting the audioContext its enough if one of the the is undefined.
|
||||
// for unsetting the audioContext its enough if one of the two is undefined.
|
||||
[audioContext && audioNodes],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024-2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { defer, sleep } from "matrix-js-sdk/lib/utils";
|
||||
import { sleep } from "matrix-js-sdk/lib/utils";
|
||||
|
||||
import { useECConnectionState } from "./useECConnectionState";
|
||||
import { type SFUConfig } from "./openIDSFU";
|
||||
@@ -93,8 +93,8 @@ describe("Leaking connection prevention", () => {
|
||||
|
||||
test("Should cancel pending connections when the component is unmounted", async () => {
|
||||
const connectCall = vi.fn();
|
||||
const pendingConnection = defer<void>();
|
||||
// let pendingDisconnection = defer<void>()
|
||||
const pendingConnection = Promise.withResolvers<void>();
|
||||
// let pendingDisconnection = Promise.withResolvers<void>()
|
||||
const disconnectMock = vi.fn();
|
||||
|
||||
const mockRoom = {
|
||||
@@ -141,8 +141,8 @@ describe("Leaking connection prevention", () => {
|
||||
|
||||
test("Should cancel about to open but not yet opened connection", async () => {
|
||||
const createTracksCall = vi.fn();
|
||||
const pendingCreateTrack = defer<void>();
|
||||
// let pendingDisconnection = defer<void>()
|
||||
const pendingCreateTrack = Promise.withResolvers<void>();
|
||||
// let pendingDisconnection = Promise.withResolvers<void>()
|
||||
const disconnectMock = vi.fn();
|
||||
const connectMock = vi.fn();
|
||||
|
||||
|
||||
@@ -22,17 +22,11 @@ import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import {
|
||||
ElementCallError,
|
||||
InsufficientCapacityError,
|
||||
SFURoomCreationRestrictedError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { AbortHandle } from "../utils/abortHandle.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
peerConnectionTimeout?: number;
|
||||
websocketTimeout?: number;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Additional values for states that a call can be in, beyond what livekit
|
||||
* provides in ConnectionState. Also reconnects the call if the SFU Config
|
||||
@@ -169,12 +163,7 @@ async function connectAndPublish(
|
||||
|
||||
try {
|
||||
logger.info(`[Lifecycle] Connecting to livekit room ${sfuConfig!.url} ...`);
|
||||
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt, {
|
||||
// Due to stability issues on Firefox we are testing the effect of different
|
||||
// timeouts, and allow these values to be set through the console
|
||||
peerConnectionTimeout: window.peerConnectionTimeout ?? 45000,
|
||||
websocketTimeout: window.websocketTimeout ?? 45000,
|
||||
});
|
||||
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
|
||||
logger.info(`[Lifecycle] ... connected to livekit room`);
|
||||
} catch (e) {
|
||||
logger.error("[Lifecycle] Failed to connect", e);
|
||||
@@ -184,11 +173,19 @@ async function connectAndPublish(
|
||||
// participant limits.
|
||||
// LiveKit Cloud uses 429 for connection limits.
|
||||
// Either way, all these errors can be explained as "insufficient capacity".
|
||||
if (
|
||||
e instanceof ConnectionError &&
|
||||
(e.status === 503 || e.status === 200 || e.status === 429)
|
||||
)
|
||||
throw new InsufficientCapacityError();
|
||||
if (e instanceof ConnectionError) {
|
||||
if (e.status === 503 || e.status === 200 || e.status === 429) {
|
||||
throw new InsufficientCapacityError();
|
||||
}
|
||||
if (e.status === 404) {
|
||||
// error msg is "Could not establish signal connection: requested room does not exist"
|
||||
// The room does not exist. There are two different modes of operation for the SFU:
|
||||
// - the room is created on the fly when connecting (livekit `auto_create` option)
|
||||
// - Only authorized users can create rooms, so the room must exist before connecting (done by the auth jwt service)
|
||||
// In the first case there will not be a 404, so we are in the second case.
|
||||
throw new SFURoomCreationRestrictedError();
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ConnectionState,
|
||||
type E2EEManagerOptions,
|
||||
ExternalE2EEKeyProvider,
|
||||
type LocalTrackPublication,
|
||||
LocalVideoTrack,
|
||||
Room,
|
||||
type RoomOptions,
|
||||
@@ -181,6 +182,33 @@ export function useLivekit(
|
||||
sfuConfig,
|
||||
);
|
||||
|
||||
// Log errors when local participant has issues publishing a track.
|
||||
useEffect(() => {
|
||||
const localTrackUnpublishedFn = (
|
||||
publication: LocalTrackPublication,
|
||||
): void => {
|
||||
logger.info(
|
||||
"Local track unpublished",
|
||||
publication.trackName,
|
||||
publication.trackInfo,
|
||||
);
|
||||
};
|
||||
const mediaDevicesErrorFn = (error: Error): void => {
|
||||
logger.warn("Media devices error when publishing a track", error);
|
||||
};
|
||||
|
||||
room.localParticipant.on("localTrackUnpublished", localTrackUnpublishedFn);
|
||||
room.localParticipant.on("mediaDevicesError", mediaDevicesErrorFn);
|
||||
|
||||
return (): void => {
|
||||
room.localParticipant.off(
|
||||
"localTrackUnpublished",
|
||||
localTrackUnpublishedFn,
|
||||
);
|
||||
room.localParticipant.off("mediaDevicesError", mediaDevicesErrorFn);
|
||||
};
|
||||
}, [room.localParticipant]);
|
||||
|
||||
useEffect(() => {
|
||||
// Sync the requested mute states with LiveKit's mute states. We do it this
|
||||
// way around rather than using LiveKit as the source of truth, so that the
|
||||
|
||||
@@ -22,11 +22,13 @@ export function RaisedHandIndicator({
|
||||
miniature,
|
||||
showTimer,
|
||||
onClick,
|
||||
tabIndex,
|
||||
}: {
|
||||
raisedHandTime?: Date;
|
||||
miniature?: boolean;
|
||||
showTimer?: boolean;
|
||||
onClick?: () => void;
|
||||
tabIndex?: number;
|
||||
}): ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const [raisedHandDuration, setRaisedHandDuration] = useState("");
|
||||
@@ -94,6 +96,7 @@ export function RaisedHandIndicator({
|
||||
background: "none",
|
||||
}}
|
||||
onClick={clickCallback}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
|
||||
@@ -27,6 +27,8 @@ import rockSoundOgg from "../sound/reactions/rock.ogg?url";
|
||||
import rockSoundMp3 from "../sound/reactions/rock.mp3?url";
|
||||
import waveSoundOgg from "../sound/reactions/wave.ogg?url";
|
||||
import waveSoundMp3 from "../sound/reactions/wave.mp3?url";
|
||||
import baduntssSoundOgg from "../sound/reactions/baduntss.ogg?url";
|
||||
import baduntssSoundMp3 from "../sound/reactions/baduntss.mp3?url";
|
||||
|
||||
export const ElementCallReactionEventType = "io.element.call.reaction";
|
||||
|
||||
@@ -191,6 +193,15 @@ export const ReactionSet: ReactionOption[] = [
|
||||
mp3: waveSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "🥁",
|
||||
name: "drum",
|
||||
alias: ["joke"],
|
||||
sound: {
|
||||
ogg: baduntssSoundOgg,
|
||||
mp3: baduntssSoundMp3,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export interface RaisedHandInfo {
|
||||
|
||||
@@ -19,10 +19,7 @@ import { act } from "react";
|
||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||
|
||||
import { mockRtcMembership } from "../utils/test";
|
||||
import {
|
||||
CallEventAudioRenderer,
|
||||
MAX_PARTICIPANT_COUNT_FOR_SOUND,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||
@@ -31,7 +28,9 @@ import {
|
||||
aliceRtcMember,
|
||||
bobRtcMember,
|
||||
local,
|
||||
localRtcMember,
|
||||
} from "../utils/test-fixtures";
|
||||
import { MAX_PARTICIPANT_COUNT_FOR_SOUND } from "../state/CallViewModel";
|
||||
|
||||
vitest.mock("../useAudioContext");
|
||||
vitest.mock("../soundUtils");
|
||||
@@ -55,6 +54,8 @@ beforeEach(() => {
|
||||
playSound = vitest.fn();
|
||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||
playSound,
|
||||
playSoundLooping: vitest.fn(),
|
||||
soundDuration: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +67,7 @@ beforeEach(() => {
|
||||
* a noise every time.
|
||||
*/
|
||||
test("plays one sound when entering a call", () => {
|
||||
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([
|
||||
const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
@@ -74,56 +75,72 @@ test("plays one sound when entering a call", () => {
|
||||
|
||||
// Joining a call usually means remote participants are added later.
|
||||
act(() => {
|
||||
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
|
||||
rtcMemberships$.next([localRtcMember, aliceRtcMember, bobRtcMember]);
|
||||
});
|
||||
expect(playSound).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("plays a sound when a user joins", () => {
|
||||
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([
|
||||
const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
|
||||
rtcMemberships$.next([localRtcMember, aliceRtcMember, bobRtcMember]);
|
||||
});
|
||||
// Play a sound when joining a call.
|
||||
expect(playSound).toBeCalledWith("join");
|
||||
});
|
||||
|
||||
test("plays a sound when a user leaves", () => {
|
||||
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([
|
||||
const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([
|
||||
local,
|
||||
alice,
|
||||
]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
remoteRtcMemberships$.next([]);
|
||||
rtcMemberships$.next([localRtcMember]);
|
||||
});
|
||||
expect(playSound).toBeCalledWith("left");
|
||||
});
|
||||
|
||||
test("does not play a sound before the call is successful", () => {
|
||||
const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment(
|
||||
[local, alice],
|
||||
[localRtcMember],
|
||||
{ waitForCallPickup: true },
|
||||
);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
rtcMemberships$.next([localRtcMember]);
|
||||
});
|
||||
expect(playSound).not.toBeCalledWith("left");
|
||||
});
|
||||
|
||||
test("plays no sound when the participant list is more than the maximum size", () => {
|
||||
const mockRtcMemberships: CallMembership[] = [];
|
||||
const mockRtcMemberships: CallMembership[] = [localRtcMember];
|
||||
for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) {
|
||||
mockRtcMemberships.push(
|
||||
mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`),
|
||||
);
|
||||
}
|
||||
|
||||
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment(
|
||||
const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment(
|
||||
[local, alice],
|
||||
mockRtcMemberships,
|
||||
);
|
||||
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
expect(playSound).not.toBeCalled();
|
||||
// Remove the last membership in the array to test the leaving sound
|
||||
// (The array has length MAX_PARTICIPANT_COUNT_FOR_SOUND + 1)
|
||||
act(() => {
|
||||
remoteRtcMemberships$.next(
|
||||
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1),
|
||||
rtcMemberships$.next(
|
||||
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND),
|
||||
);
|
||||
});
|
||||
expect(playSound).toBeCalledWith("left");
|
||||
@@ -155,6 +172,7 @@ test("should not play a sound when a hand raise is retracted", () => {
|
||||
]);
|
||||
render(<CallEventAudioRenderer vm={vm} />);
|
||||
|
||||
playSound.mockClear();
|
||||
act(() => {
|
||||
handRaisedSubject$.next({
|
||||
["foo"]: {
|
||||
@@ -169,7 +187,7 @@ test("should not play a sound when a hand raise is retracted", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(playSound).toHaveBeenCalledTimes(2);
|
||||
expect(playSound).toHaveBeenCalledExactlyOnceWith("raiseHand");
|
||||
act(() => {
|
||||
handRaisedSubject$.next({
|
||||
["foo"]: {
|
||||
@@ -179,5 +197,5 @@ test("should not play a sound when a hand raise is retracted", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(playSound).toHaveBeenCalledTimes(2);
|
||||
expect(playSound).toHaveBeenCalledExactlyOnceWith("raiseHand");
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import { filter, interval, throttle } from "rxjs";
|
||||
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import joinCallSoundMp3 from "../sound/join_call.mp3";
|
||||
@@ -17,15 +16,14 @@ import handSoundOgg from "../sound/raise_hand.ogg";
|
||||
import handSoundMp3 from "../sound/raise_hand.mp3";
|
||||
import screenShareStartedOgg from "../sound/screen_share_started.ogg";
|
||||
import screenShareStartedMp3 from "../sound/screen_share_started.mp3";
|
||||
import declineMp3 from "../sound/call_declined.mp3?url";
|
||||
import declineOgg from "../sound/call_declined.ogg?url";
|
||||
import timeoutMp3 from "../sound/call_timeout.mp3?url";
|
||||
import timeoutOgg from "../sound/call_timeout.ogg?url";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
import { useLatest } from "../useLatest";
|
||||
|
||||
// Do not play any sounds if the participant count has exceeded this
|
||||
// number.
|
||||
export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;
|
||||
export const THROTTLE_SOUND_EFFECT_MS = 500;
|
||||
|
||||
export const callEventAudioSounds = prefetchSounds({
|
||||
join: {
|
||||
mp3: joinCallSoundMp3,
|
||||
@@ -43,8 +41,18 @@ export const callEventAudioSounds = prefetchSounds({
|
||||
mp3: screenShareStartedMp3,
|
||||
ogg: screenShareStartedOgg,
|
||||
},
|
||||
decline: {
|
||||
mp3: declineMp3,
|
||||
ogg: declineOgg,
|
||||
},
|
||||
timeout: {
|
||||
mp3: timeoutMp3,
|
||||
ogg: timeoutOgg,
|
||||
},
|
||||
});
|
||||
|
||||
export type CallEventSounds = keyof Awaited<typeof callEventAudioSounds>;
|
||||
|
||||
export function CallEventAudioRenderer({
|
||||
vm,
|
||||
muted,
|
||||
@@ -60,37 +68,18 @@ export function CallEventAudioRenderer({
|
||||
const audioEngineRef = useLatest(audioEngineCtx);
|
||||
|
||||
useEffect(() => {
|
||||
const joinSub = vm.memberChanges$
|
||||
.pipe(
|
||||
filter(
|
||||
({ joined, ids }) =>
|
||||
ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && joined.length > 0,
|
||||
),
|
||||
throttle(() => interval(THROTTLE_SOUND_EFFECT_MS)),
|
||||
)
|
||||
.subscribe(() => {
|
||||
void audioEngineRef.current?.playSound("join");
|
||||
});
|
||||
|
||||
const leftSub = vm.memberChanges$
|
||||
.pipe(
|
||||
filter(
|
||||
({ ids, left }) =>
|
||||
ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && left.length > 0,
|
||||
),
|
||||
throttle(() => interval(THROTTLE_SOUND_EFFECT_MS)),
|
||||
)
|
||||
.subscribe(() => {
|
||||
void audioEngineRef.current?.playSound("left");
|
||||
});
|
||||
|
||||
const handRaisedSub = vm.newHandRaised$.subscribe(() => {
|
||||
void audioEngineRef.current?.playSound("raiseHand");
|
||||
});
|
||||
|
||||
const screenshareSub = vm.newScreenShare$.subscribe(() => {
|
||||
void audioEngineRef.current?.playSound("screenshareStarted");
|
||||
});
|
||||
const joinSub = vm.joinSoundEffect$.subscribe(
|
||||
() => void audioEngineRef.current?.playSound("join"),
|
||||
);
|
||||
const leftSub = vm.leaveSoundEffect$.subscribe(
|
||||
() => void audioEngineRef.current?.playSound("left"),
|
||||
);
|
||||
const handRaisedSub = vm.newHandRaised$.subscribe(
|
||||
() => void audioEngineRef.current?.playSound("raiseHand"),
|
||||
);
|
||||
const screenshareSub = vm.newScreenShare$.subscribe(
|
||||
() => void audioEngineRef.current?.playSound("screenshareStarted"),
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
joinSub.unsubscribe();
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
.overlay[data-show="false"] {
|
||||
animation: fade-out 130ms forwards;
|
||||
content-visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
export const EarpieceOverlay: FC<Props> = ({ show, onBackToVideoPressed }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={styles.overlay} data-show={show}>
|
||||
<div className={styles.overlay} data-show={show} aria-hidden={!show}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<VoiceCallIcon aria-hidden />
|
||||
</BigIcon>
|
||||
|
||||
@@ -132,9 +132,10 @@ test("ConnectionLostError: Action handling should reset error state", async () =
|
||||
const WrapComponent = (): ReactNode => {
|
||||
const [failState, setFailState] = useState(true);
|
||||
const reconnectCallback = useCallback(
|
||||
(action: CallErrorRecoveryAction) => {
|
||||
async (action: CallErrorRecoveryAction) => {
|
||||
reconnectCallbackSpy(action);
|
||||
setFailState(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
[setFailState],
|
||||
);
|
||||
|
||||
@@ -36,7 +36,9 @@ import { type WidgetHelpers } from "../widget.ts";
|
||||
|
||||
export type CallErrorRecoveryAction = "reconnect"; // | "retry" ;
|
||||
|
||||
export type RecoveryActionHandler = (action: CallErrorRecoveryAction) => void;
|
||||
export type RecoveryActionHandler = (
|
||||
action: CallErrorRecoveryAction,
|
||||
) => Promise<void>;
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: ElementCallError;
|
||||
@@ -71,7 +73,7 @@ const ErrorPage: FC<ErrorPageProps> = ({
|
||||
if (error instanceof ConnectionLostError) {
|
||||
actions.push({
|
||||
label: t("call_ended_view.reconnect_button"),
|
||||
onClick: () => recoveryActionHandler("reconnect"),
|
||||
onClick: () => void recoveryActionHandler("reconnect"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,9 +133,9 @@ export const GroupCallErrorBoundary = ({
|
||||
widget={widget ?? null}
|
||||
error={callError}
|
||||
resetError={resetError}
|
||||
recoveryActionHandler={(action: CallErrorRecoveryAction) => {
|
||||
recoveryActionHandler={async (action: CallErrorRecoveryAction) => {
|
||||
await recoveryActionHandler(action);
|
||||
resetError();
|
||||
recoveryActionHandler(action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,10 +12,14 @@ import {
|
||||
onTestFinished,
|
||||
test,
|
||||
vi,
|
||||
vitest,
|
||||
} from "vitest";
|
||||
import { render, waitFor, screen } from "@testing-library/react";
|
||||
import { render, waitFor, screen, act } from "@testing-library/react";
|
||||
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
MatrixRTCSessionEvent,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
||||
@@ -94,13 +98,15 @@ beforeEach(() => {
|
||||
playSound = vi.fn();
|
||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||
playSound,
|
||||
playSoundLooping: vi.fn(),
|
||||
soundDuration: {},
|
||||
});
|
||||
// A trivial implementation of Active call to ensure we are testing GroupCallView exclusively here.
|
||||
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(
|
||||
({ onLeave }) => {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => onLeave()}>Leave</button>
|
||||
<button onClick={() => onLeave("user")}>Leave</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -137,11 +143,9 @@ function createGroupCallView(
|
||||
getJoinRule: () => JoinRule.Invite,
|
||||
} as Partial<RoomState> as RoomState,
|
||||
});
|
||||
const rtcSession = new MockRTCSession(
|
||||
room,
|
||||
localRtcMember,
|
||||
[],
|
||||
).withMemberships(constant([]));
|
||||
const rtcSession = new MockRTCSession(room, []).withMemberships(
|
||||
constant([localRtcMember]),
|
||||
);
|
||||
rtcSession.joined = joined;
|
||||
const muteState = {
|
||||
audio: { enabled: false },
|
||||
@@ -208,6 +212,8 @@ test("GroupCallView plays a leave sound synchronously in widget mode", async ()
|
||||
);
|
||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||
playSound,
|
||||
playSoundLooping: vitest.fn(),
|
||||
soundDuration: {},
|
||||
});
|
||||
|
||||
const { getByText, rtcSession } = createGroupCallView(
|
||||
@@ -260,3 +266,19 @@ test("GroupCallView shows errors that occur during joining", async () => {
|
||||
await user.click(screen.getByRole("button", { name: "Join call" }));
|
||||
screen.getByText("Call is not supported");
|
||||
});
|
||||
|
||||
test("user can reconnect after a membership manager error", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rtcSession } = createGroupCallView(null, true);
|
||||
await act(() =>
|
||||
rtcSession.emit(MatrixRTCSessionEvent.MembershipManagerError, undefined),
|
||||
);
|
||||
// XXX: Wrapping the following click in act() shouldn't be necessary (the
|
||||
// async state update should be processed automatically by the waitFor call),
|
||||
// and yet here we are.
|
||||
await act(async () =>
|
||||
user.click(screen.getByRole("button", { name: "Reconnect" })),
|
||||
);
|
||||
// In-call controls should be visible again
|
||||
await waitFor(() => screen.getByRole("button", { name: "Leave" }));
|
||||
});
|
||||
|
||||
@@ -50,10 +50,13 @@ import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { HeaderStyle, useUrlParams } from "../UrlParams";
|
||||
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { callEventAudioSounds } from "./CallEventAudioRenderer";
|
||||
import {
|
||||
callEventAudioSounds,
|
||||
type CallEventSounds,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import {
|
||||
@@ -83,8 +86,8 @@ interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
confineToRoom: boolean;
|
||||
preload: boolean;
|
||||
skipLobby: boolean;
|
||||
preload: UrlParams["preload"];
|
||||
skipLobby: UrlParams["skipLobby"];
|
||||
header: HeaderStyle;
|
||||
rtcSession: MatrixRTCSession;
|
||||
isJoined: boolean;
|
||||
@@ -125,6 +128,16 @@ export const GroupCallView: FC<Props> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Update our member event when our mute state changes.
|
||||
useEffect(() => {
|
||||
if (!isJoined) {
|
||||
return;
|
||||
}
|
||||
void rtcSession.updateCallIntent(
|
||||
muteStates.video.enabled ? "video" : "audio",
|
||||
);
|
||||
}, [rtcSession, isJoined, muteStates.video.enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
logger.info("[Lifecycle] GroupCallView Component mounted");
|
||||
return (): void => {
|
||||
@@ -166,7 +179,11 @@ export const GroupCallView: FC<Props> = ({
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const roomName = useRoomName(room);
|
||||
const roomAvatar = useRoomAvatar(room);
|
||||
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
||||
const {
|
||||
perParticipantE2EE,
|
||||
returnToLobby,
|
||||
password: passwordFromUrl,
|
||||
} = useUrlParams();
|
||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
||||
const [useExperimentalToDeviceTransport] = useSetting(
|
||||
@@ -174,7 +191,6 @@ export const GroupCallView: FC<Props> = ({
|
||||
);
|
||||
|
||||
// Save the password once we start the groupCallView
|
||||
const { password: passwordFromUrl } = useUrlParams();
|
||||
useEffect(() => {
|
||||
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
|
||||
}, [passwordFromUrl, room.roomId]);
|
||||
@@ -273,34 +289,28 @@ export const GroupCallView: FC<Props> = ({
|
||||
};
|
||||
|
||||
if (skipLobby) {
|
||||
if (widget) {
|
||||
if (preload) {
|
||||
// In preload mode without lobby we wait for a join action before entering
|
||||
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup(
|
||||
ev.detail.data as unknown as JoinCallData,
|
||||
);
|
||||
await enterRTCSessionOrError(rtcSession);
|
||||
widget.api.transport.reply(ev.detail, {});
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
});
|
||||
};
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return (): void => {
|
||||
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
} else {
|
||||
// No lobby and no preload: we enter the rtc session right away
|
||||
if (widget && preload) {
|
||||
// In preload mode without lobby we wait for a join action before entering
|
||||
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
|
||||
await enterRTCSessionOrError(rtcSession);
|
||||
widget.api.transport.reply(ev.detail, {});
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
logger.error("Error joining RTC session on preload", e);
|
||||
});
|
||||
}
|
||||
};
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return (): void => {
|
||||
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
} else {
|
||||
void enterRTCSessionOrError(rtcSession);
|
||||
// No lobby and no preload: we enter the rtc session right away
|
||||
(async (): Promise<void> => {
|
||||
await enterRTCSessionOrError(rtcSession);
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session immediately", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
@@ -320,8 +330,11 @@ export const GroupCallView: FC<Props> = ({
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onLeave = useCallback(
|
||||
(cause: "user" | "error" = "user"): void => {
|
||||
const audioPromise = leaveSoundContext.current?.playSound("left");
|
||||
(
|
||||
cause: "user" | "error" = "user",
|
||||
playSound: CallEventSounds = "left",
|
||||
): void => {
|
||||
const audioPromise = leaveSoundContext.current?.playSound(playSound);
|
||||
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
||||
// therefore we want the event to be sent instantly without getting queued/batched.
|
||||
const sendInstantly = !!widget;
|
||||
@@ -423,7 +436,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
client={client}
|
||||
matrixInfo={matrixInfo}
|
||||
muteStates={muteStates}
|
||||
onEnter={() => void enterRTCSessionOrError(rtcSession)}
|
||||
onEnter={async () => enterRTCSessionOrError(rtcSession)}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={header === HeaderStyle.None}
|
||||
participantCount={participantCount}
|
||||
@@ -449,7 +462,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
client={client}
|
||||
matrixInfo={matrixInfo}
|
||||
rtcSession={rtcSession as MatrixRTCSession}
|
||||
participantCount={participantCount}
|
||||
matrixRoom={room}
|
||||
onLeave={onLeave}
|
||||
header={header}
|
||||
muteStates={muteStates}
|
||||
@@ -491,6 +504,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
// Left in widget mode:
|
||||
body = returnToLobby ? lobbyView : null;
|
||||
} else if (preload || skipLobby) {
|
||||
// The RTC session is not joined to yet (`isJoined`), but enterRTCSessionOrError should have been called.
|
||||
body = null;
|
||||
} else {
|
||||
body = lobbyView;
|
||||
@@ -499,10 +513,11 @@ export const GroupCallView: FC<Props> = ({
|
||||
return (
|
||||
<GroupCallErrorBoundary
|
||||
widget={widget}
|
||||
recoveryActionHandler={(action) => {
|
||||
recoveryActionHandler={async (action) => {
|
||||
setExternalError(null);
|
||||
if (action == "reconnect") {
|
||||
setLeft(false);
|
||||
enterRTCSessionOrError(rtcSession).catch((e) => {
|
||||
await enterRTCSessionOrError(rtcSession).catch((e) => {
|
||||
logger.error("Error re-entering RTC session", e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,14 +108,6 @@ Please see LICENSE in the repository root for full details.
|
||||
}
|
||||
|
||||
@media (max-width: 370px) {
|
||||
.raiseHand {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 340px) {
|
||||
.invite,
|
||||
.switchCamera,
|
||||
.shareScreen {
|
||||
display: none;
|
||||
}
|
||||
@@ -127,6 +119,13 @@ Please see LICENSE in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.invite,
|
||||
.raiseHand {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 400px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-4x);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { act, render, type RenderResult } from "@testing-library/react";
|
||||
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
||||
import { ConnectionState, type LocalParticipant } from "livekit-client";
|
||||
import { type LocalParticipant } from "livekit-client";
|
||||
import { of } from "rxjs";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -175,12 +175,11 @@ function createInCallView(): RenderResult & {
|
||||
kind: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
matrixRoom={room}
|
||||
livekitRoom={livekitRoom}
|
||||
participantCount={0}
|
||||
onLeave={function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
connState={ConnectionState.Connected}
|
||||
onShareClick={null}
|
||||
/>
|
||||
</RoomContext>
|
||||
|
||||
@@ -7,8 +7,8 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
||||
import { IconButton, Text, Tooltip } from "@vector-im/compound-web";
|
||||
import { ConnectionState, type Room } from "livekit-client";
|
||||
import { type MatrixClient } from "matrix-js-sdk";
|
||||
import { ConnectionState, type Room as LivekitRoom } from "livekit-client";
|
||||
import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk";
|
||||
import {
|
||||
type FC,
|
||||
type PointerEvent,
|
||||
@@ -25,7 +25,11 @@ import useMeasure from "react-use-measure";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import classNames from "classnames";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
import { useObservable } from "observable-hooks";
|
||||
import {
|
||||
useObservable,
|
||||
useObservableEagerState,
|
||||
useSubscription,
|
||||
} from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
import {
|
||||
@@ -44,7 +48,6 @@ import {
|
||||
ShareScreenButton,
|
||||
SettingsButton,
|
||||
ReactionToggleButton,
|
||||
SwitchCameraButton,
|
||||
} from "../button";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { type HeaderStyle, useUrlParams } from "../UrlParams";
|
||||
@@ -63,7 +66,6 @@ import { type MuteStates } from "./MuteStates";
|
||||
import { type MatrixInfo } from "./VideoPreview";
|
||||
import { InviteButton } from "../button/InviteButton";
|
||||
import { LayoutToggle } from "./LayoutToggle";
|
||||
import { type ECConnectionState } from "../livekit/useECConnectionState";
|
||||
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||
import {
|
||||
CallViewModel,
|
||||
@@ -94,9 +96,11 @@ import {
|
||||
useReactionsSender,
|
||||
} from "../reactions/useReactionsSender";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { useSwitchCamera } from "./useSwitchCamera";
|
||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||
import {
|
||||
CallEventAudioRenderer,
|
||||
type CallEventSounds,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import {
|
||||
debugTileLayout as debugTileLayoutSetting,
|
||||
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
|
||||
@@ -113,6 +117,15 @@ import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||
import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
import { Toast } from "../Toast.tsx";
|
||||
import overlayStyles from "../Overlay.module.css";
|
||||
import { Avatar, Size as AvatarSize } from "../Avatar";
|
||||
import waitingStyles from "./WaitingForJoin.module.css";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
||||
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
||||
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
|
||||
@@ -132,25 +145,30 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
sfuConfig,
|
||||
props.e2eeSystem,
|
||||
);
|
||||
const connStateObservable$ = useObservable(
|
||||
(inputs$) => inputs$.pipe(map(([connState]) => connState)),
|
||||
const observableScope = useInitial(() => new ObservableScope());
|
||||
const connStateBehavior$ = useObservable(
|
||||
(inputs$) =>
|
||||
observableScope.behavior(
|
||||
inputs$.pipe(map(([connState]) => connState)),
|
||||
connState,
|
||||
),
|
||||
[connState],
|
||||
);
|
||||
const [vm, setVm] = useState<CallViewModel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
logger.info(
|
||||
`[Lifecycle] InCallView Component mounted, livekitroom state ${livekitRoom?.state}`,
|
||||
`[Lifecycle] InCallView Component mounted, livekit room state ${livekitRoom?.state}`,
|
||||
);
|
||||
return (): void => {
|
||||
logger.info(
|
||||
`[Lifecycle] InCallView Component unmounted, livekitroom state ${livekitRoom?.state}`,
|
||||
`[Lifecycle] InCallView Component unmounted, livekit room state ${livekitRoom?.state}`,
|
||||
);
|
||||
livekitRoom
|
||||
?.disconnect()
|
||||
.then(() => {
|
||||
logger.info(
|
||||
`[Lifecycle] Disconnected from livekite room, state:${livekitRoom?.state}`,
|
||||
`[Lifecycle] Disconnected from livekit room, state:${livekitRoom?.state}`,
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -159,15 +177,24 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
};
|
||||
}, [livekitRoom]);
|
||||
|
||||
const { autoLeaveWhenOthersLeft, sendNotificationType, waitForCallPickup } =
|
||||
useUrlParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (livekitRoom !== undefined) {
|
||||
const reactionsReader = new ReactionsReader(props.rtcSession);
|
||||
const vm = new CallViewModel(
|
||||
props.rtcSession,
|
||||
props.matrixRoom,
|
||||
livekitRoom,
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
connStateObservable$,
|
||||
{
|
||||
encryptionSystem: props.e2eeSystem,
|
||||
autoLeaveWhenOthersLeft,
|
||||
waitForCallPickup:
|
||||
waitForCallPickup && sendNotificationType === "ring",
|
||||
},
|
||||
connStateBehavior$,
|
||||
reactionsReader.raisedHands$,
|
||||
reactionsReader.reactions$,
|
||||
);
|
||||
@@ -179,10 +206,14 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
}
|
||||
}, [
|
||||
props.rtcSession,
|
||||
props.matrixRoom,
|
||||
livekitRoom,
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
connStateObservable$,
|
||||
connStateBehavior$,
|
||||
autoLeaveWhenOthersLeft,
|
||||
sendNotificationType,
|
||||
waitForCallPickup,
|
||||
]);
|
||||
|
||||
if (livekitRoom === undefined || vm === null) return null;
|
||||
@@ -190,12 +221,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
return (
|
||||
<RoomContext value={livekitRoom}>
|
||||
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
|
||||
<InCallView
|
||||
{...props}
|
||||
vm={vm}
|
||||
livekitRoom={livekitRoom}
|
||||
connState={connState}
|
||||
/>
|
||||
<InCallView {...props} vm={vm} livekitRoom={livekitRoom} />
|
||||
</ReactionsSenderProvider>
|
||||
</RoomContext>
|
||||
);
|
||||
@@ -206,14 +232,13 @@ export interface InCallViewProps {
|
||||
vm: CallViewModel;
|
||||
matrixInfo: MatrixInfo;
|
||||
rtcSession: MatrixRTCSession;
|
||||
livekitRoom: Room;
|
||||
matrixRoom: MatrixRoom;
|
||||
livekitRoom: LivekitRoom;
|
||||
muteStates: MuteStates;
|
||||
participantCount: number;
|
||||
/** Function to call when the user explicitly ends the call */
|
||||
onLeave: () => void;
|
||||
onLeave: (cause: "user", soundFile?: CallEventSounds) => void;
|
||||
header: HeaderStyle;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
connState: ECConnectionState;
|
||||
onShareClick: (() => void) | null;
|
||||
}
|
||||
|
||||
@@ -222,12 +247,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
vm,
|
||||
matrixInfo,
|
||||
rtcSession,
|
||||
matrixRoom,
|
||||
livekitRoom,
|
||||
muteStates,
|
||||
participantCount,
|
||||
onLeave,
|
||||
header: headerStyle,
|
||||
connState,
|
||||
onShareClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -235,10 +259,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
useReactionsSender();
|
||||
|
||||
useWakeLock();
|
||||
const connectionState = useObservableEagerState(vm.livekitConnectionState$);
|
||||
|
||||
// annoyingly we don't get the disconnection reason this way,
|
||||
// only by listening for the emitted event
|
||||
if (connState === ConnectionState.Disconnected)
|
||||
if (connectionState === ConnectionState.Disconnected)
|
||||
throw new ConnectionLostError();
|
||||
|
||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||
@@ -253,6 +278,21 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
});
|
||||
|
||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||
// Call pickup state and display names are needed for waiting overlay/sounds
|
||||
const callPickupState = useBehavior(vm.callPickupState$);
|
||||
|
||||
// Preload a waiting and decline sounds
|
||||
const pickupPhaseSoundCache = useInitial(async () => {
|
||||
return prefetchSounds({
|
||||
waiting: { mp3: ringtoneMp3, ogg: ringtoneOgg },
|
||||
});
|
||||
});
|
||||
|
||||
const pickupPhaseAudio = useAudioContext({
|
||||
sounds: pickupPhaseSoundCache,
|
||||
latencyHint: "interactive",
|
||||
muted: muteAllAudio,
|
||||
});
|
||||
|
||||
// This seems like it might be enough logic to use move it into the call view model?
|
||||
const [didFallbackToRoomKey, setDidFallbackToRoomKey] = useState(false);
|
||||
@@ -266,7 +306,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const [useExperimentalToDeviceTransport] = useSetting(
|
||||
useExperimentalToDeviceTransportSetting,
|
||||
);
|
||||
const encryptionSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
|
||||
const encryptionSystem = useRoomEncryptionSystem(matrixRoom.roomId);
|
||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||
|
||||
const showToDeviceEncryption = useMemo(
|
||||
@@ -303,6 +343,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => void toggleRaisedHand(),
|
||||
);
|
||||
|
||||
const participantCount = useBehavior(vm.participantCount$);
|
||||
const reconnecting = useBehavior(vm.reconnecting$);
|
||||
const windowMode = useBehavior(vm.windowMode$);
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$);
|
||||
@@ -312,7 +354,90 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||
const switchCamera = useSwitchCamera(vm.localVideo$);
|
||||
useSubscription(vm.autoLeave$, () => onLeave("user"));
|
||||
|
||||
// We need to set the proper timings on the animation based upon the sound length.
|
||||
const ringDuration = pickupPhaseAudio?.soundDuration["waiting"] ?? 1;
|
||||
useEffect((): (() => void) => {
|
||||
// The CSS animation includes the delay, so we must double the length of the sound.
|
||||
window.document.body.style.setProperty(
|
||||
"--call-ring-duration-s",
|
||||
`${ringDuration * 2}s`,
|
||||
);
|
||||
window.document.body.style.setProperty(
|
||||
"--call-ring-delay-s",
|
||||
`${ringDuration}s`,
|
||||
);
|
||||
// Remove properties when we unload.
|
||||
return () => {
|
||||
window.document.body.style.removeProperty("--call-ring-duration-s");
|
||||
window.document.body.style.removeProperty("--call-ring-delay-s");
|
||||
};
|
||||
}, [pickupPhaseAudio?.soundDuration, ringDuration]);
|
||||
|
||||
// When we enter timeout or decline we will leave the call.
|
||||
useEffect((): void | (() => void) => {
|
||||
if (callPickupState === "timeout") {
|
||||
onLeave("user", "timeout");
|
||||
}
|
||||
if (callPickupState === "decline") {
|
||||
onLeave("user", "decline");
|
||||
}
|
||||
}, [callPickupState, onLeave, pickupPhaseAudio]);
|
||||
|
||||
// When waiting for pickup, loop a waiting sound
|
||||
useEffect((): void | (() => void) => {
|
||||
if (callPickupState !== "ringing" || !pickupPhaseAudio) return;
|
||||
const endSound = pickupPhaseAudio.playSoundLooping("waiting", ringDuration);
|
||||
return () => {
|
||||
void endSound().catch((e) => {
|
||||
logger.error("Failed to stop ringing sound", e);
|
||||
});
|
||||
};
|
||||
}, [callPickupState, pickupPhaseAudio, ringDuration]);
|
||||
|
||||
// Waiting UI overlay
|
||||
const waitingOverlay: JSX.Element | null = useMemo(() => {
|
||||
// No overlay if not in ringing state
|
||||
if (callPickupState !== "ringing") return null;
|
||||
|
||||
// Use room state for other participants data (the one that we likely want to reach)
|
||||
const roomOthers = [
|
||||
...matrixRoom.getMembersWithMembership("join"),
|
||||
...matrixRoom.getMembersWithMembership("invite"),
|
||||
].filter((m) => m.userId !== client.getUserId());
|
||||
// Yield if there are not other members in the room.
|
||||
if (roomOthers.length === 0) return null;
|
||||
|
||||
const otherMember = roomOthers.length > 0 ? roomOthers[0] : undefined;
|
||||
const isOneOnOne = roomOthers.length === 1 && otherMember;
|
||||
const text = isOneOnOne
|
||||
? `Waiting for ${otherMember.name ?? otherMember.userId} to join…`
|
||||
: "Waiting for other participants…";
|
||||
const avatarMxc = isOneOnOne
|
||||
? (otherMember.getMxcAvatarUrl?.() ?? undefined)
|
||||
: (matrixRoom.getMxcAvatarUrl() ?? undefined);
|
||||
|
||||
return (
|
||||
<div className={classNames(overlayStyles.bg, waitingStyles.overlay)}>
|
||||
<div
|
||||
className={classNames(overlayStyles.content, waitingStyles.content)}
|
||||
>
|
||||
<div className={waitingStyles.pulse}>
|
||||
<Avatar
|
||||
id={isOneOnOne ? otherMember.userId : matrixRoom.roomId}
|
||||
name={isOneOnOne ? otherMember.name : matrixRoom.name}
|
||||
src={avatarMxc}
|
||||
size={AvatarSize.XL}
|
||||
/>
|
||||
</div>
|
||||
<Text size="md" className={waitingStyles.text}>
|
||||
{text}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [callPickupState, client, matrixRoom]);
|
||||
|
||||
// Ideally we could detect taps by listening for click events and checking
|
||||
// that the pointerType of the event is "touch", but this isn't yet supported
|
||||
@@ -495,7 +620,11 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
break;
|
||||
case "standard":
|
||||
header = (
|
||||
<Header className={styles.header} ref={headerRef}>
|
||||
<Header
|
||||
className={styles.header}
|
||||
ref={headerRef}
|
||||
disconnectedBanner={false} // This screen has its own 'reconnecting' toast
|
||||
>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo
|
||||
id={matrixInfo.roomId}
|
||||
@@ -518,6 +647,38 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// The reconnecting toast cannot be dismissed
|
||||
const onDismissReconnectingToast = useCallback(() => {}, []);
|
||||
// We need to use a non-modal toast to avoid trapping focus within the toast.
|
||||
// However, a non-modal toast will not render any background overlay on its
|
||||
// own, so we must render one manually.
|
||||
const reconnectingToast = (
|
||||
<>
|
||||
<div
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
data-state={reconnecting ? "open" : "closed"}
|
||||
/>
|
||||
<Toast
|
||||
onDismiss={onDismissReconnectingToast}
|
||||
open={reconnecting}
|
||||
modal={false}
|
||||
>
|
||||
{t("common.reconnecting")}
|
||||
</Toast>
|
||||
</>
|
||||
);
|
||||
|
||||
const earpieceOverlay = (
|
||||
<EarpieceOverlay
|
||||
show={earpieceMode && !reconnecting}
|
||||
onBackToVideoPressed={audioOutputSwitcher?.switch}
|
||||
/>
|
||||
);
|
||||
|
||||
// If the reconnecting toast or earpiece overlay obscures the media tiles, we
|
||||
// need to remove them from the accessibility tree and block focus.
|
||||
const contentObscured = reconnecting || earpieceMode;
|
||||
|
||||
const Tile = useMemo(
|
||||
() =>
|
||||
function Tile({
|
||||
@@ -547,6 +708,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicatorsValue}
|
||||
focusable={!contentObscured}
|
||||
/>
|
||||
) : (
|
||||
<SpotlightTile
|
||||
@@ -557,12 +719,13 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
showIndicators={showSpotlightIndicatorsValue}
|
||||
focusable={!contentObscured}
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[vm, openProfile],
|
||||
[vm, openProfile, contentObscured],
|
||||
);
|
||||
|
||||
const layouts = useMemo(() => {
|
||||
@@ -591,6 +754,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetWidth={gridBounds.height}
|
||||
targetHeight={gridBounds.width}
|
||||
showIndicators={false}
|
||||
focusable={!contentObscured}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -608,6 +773,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
model={layout}
|
||||
Layout={layers.fixed}
|
||||
Tile={Tile}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
);
|
||||
const scrollingGrid = (
|
||||
@@ -617,6 +783,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
model={layout}
|
||||
Layout={layers.scrolling}
|
||||
Tile={Tile}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
);
|
||||
// The grid tiles go *under* the spotlight in the portrait layout, but
|
||||
@@ -635,7 +802,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
};
|
||||
|
||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||
rtcSession.room.roomId,
|
||||
matrixRoom.roomId,
|
||||
);
|
||||
|
||||
const toggleScreensharing = useCallback(() => {
|
||||
@@ -669,15 +836,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
if (switchCamera !== null)
|
||||
buttons.push(
|
||||
<SwitchCameraButton
|
||||
key="switch_camera"
|
||||
className={styles.switchCamera}
|
||||
onClick={switchCamera}
|
||||
onTouchEnd={onControlsTouchEnd}
|
||||
/>,
|
||||
);
|
||||
if (canScreenshare && !hideScreensharing) {
|
||||
buttons.push(
|
||||
<ShareScreenButton
|
||||
@@ -714,7 +872,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
<EndCallButton
|
||||
key="end_call"
|
||||
onClick={function (): void {
|
||||
onLeave();
|
||||
onLeave("user");
|
||||
}}
|
||||
onTouchEnd={onControlsTouchEnd}
|
||||
data-testid="incall_leave"
|
||||
@@ -782,18 +940,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{renderContent()}
|
||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<EarpieceOverlay
|
||||
show={earpieceMode}
|
||||
onBackToVideoPressed={audioOutputSwitcher?.switch}
|
||||
/>
|
||||
{reconnectingToast}
|
||||
{earpieceOverlay}
|
||||
<ReactionsOverlay vm={vm} />
|
||||
{waitingOverlay}
|
||||
{footer}
|
||||
{layout.type !== "pip" && (
|
||||
<>
|
||||
<RageshakeRequestModal {...rageshakeRequestModalProps} />
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
roomId={matrixRoom.roomId}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
tab={settingsTab}
|
||||
|
||||
@@ -24,8 +24,7 @@ import {
|
||||
type LocalVideoTrack,
|
||||
Track,
|
||||
} from "livekit-client";
|
||||
import { useObservable, useObservableEagerState } from "observable-hooks";
|
||||
import { map } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import inCallStyles from "./InCallView.module.css";
|
||||
@@ -38,7 +37,6 @@ import {
|
||||
EndCallButton,
|
||||
MicButton,
|
||||
SettingsButton,
|
||||
SwitchCameraButton,
|
||||
VideoButton,
|
||||
} from "../button/Button";
|
||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||
@@ -47,7 +45,6 @@ import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { Link } from "../button/Link";
|
||||
import { useMediaDevices } from "../MediaDevicesContext";
|
||||
import { useInitial } from "../useInitial";
|
||||
import { useSwitchCamera as useShowSwitchCamera } from "./useSwitchCamera";
|
||||
import {
|
||||
useTrackProcessor,
|
||||
useTrackProcessorSync,
|
||||
@@ -60,7 +57,7 @@ interface Props {
|
||||
client: MatrixClient;
|
||||
matrixInfo: MatrixInfo;
|
||||
muteStates: MuteStates;
|
||||
onEnter: () => void;
|
||||
onEnter: () => Promise<void>;
|
||||
enterLabel?: JSX.Element | string;
|
||||
confineToRoom: boolean;
|
||||
hideHeader: boolean;
|
||||
@@ -195,12 +192,14 @@ export const LobbyView: FC<Props> = ({
|
||||
}, [devices, videoInputId, videoTrack]);
|
||||
|
||||
useTrackProcessorSync(videoTrack);
|
||||
const showSwitchCamera = useShowSwitchCamera(
|
||||
useObservable(
|
||||
(inputs$) => inputs$.pipe(map(([video]) => video)),
|
||||
[videoTrack],
|
||||
),
|
||||
);
|
||||
|
||||
const [waitingToEnter, setWaitingToEnter] = useState(false);
|
||||
const onEnterCall = useCallback(() => {
|
||||
setWaitingToEnter(true);
|
||||
void onEnter().finally(() => setWaitingToEnter(false));
|
||||
}, [onEnter]);
|
||||
|
||||
const waiting = waitingForInvite || waitingToEnter;
|
||||
|
||||
// TODO: Unify this component with InCallView, so we can get slick joining
|
||||
// animations and don't have to feel bad about reusing its CSS
|
||||
@@ -231,11 +230,12 @@ export const LobbyView: FC<Props> = ({
|
||||
>
|
||||
<Button
|
||||
className={classNames(styles.join, {
|
||||
[styles.wait]: waitingForInvite,
|
||||
[styles.wait]: waiting,
|
||||
})}
|
||||
size={waitingForInvite ? "sm" : "lg"}
|
||||
size={waiting ? "sm" : "lg"}
|
||||
disabled={waiting}
|
||||
onClick={() => {
|
||||
if (!waitingForInvite) onEnter();
|
||||
if (!waiting) onEnterCall();
|
||||
}}
|
||||
data-testid="lobby_joinCall"
|
||||
>
|
||||
@@ -257,9 +257,6 @@ export const LobbyView: FC<Props> = ({
|
||||
onClick={onVideoPress}
|
||||
disabled={muteStates.video.setEnabled === null}
|
||||
/>
|
||||
{showSwitchCamera && (
|
||||
<SwitchCameraButton onClick={showSwitchCamera} />
|
||||
)}
|
||||
<SettingsButton onClick={openSettings} />
|
||||
{!confineToRoom && <EndCallButton onClick={onLeaveClick} />}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
@@ -26,7 +27,6 @@ import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { mockConfig } from "../utils/test";
|
||||
import { MediaDevices } from "../state/MediaDevices";
|
||||
import { ObservableScope } from "../state/ObservableScope";
|
||||
|
||||
vi.mock("@livekit/components-core");
|
||||
|
||||
interface TestComponentProps {
|
||||
@@ -110,9 +110,10 @@ function mockMediaDevices(
|
||||
return new MediaDevices(scope);
|
||||
}
|
||||
|
||||
describe("useMuteStates", () => {
|
||||
describe("useMuteStates VITE_PACKAGE='full' (SPA) mode", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -256,3 +257,67 @@ describe("useMuteStates", () => {
|
||||
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("useMuteStates in VITE_PACKAGE='embedded' (widget) mode", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "embedded");
|
||||
});
|
||||
|
||||
it("uses defaults from config", () => {
|
||||
mockConfig({
|
||||
media_devices: {
|
||||
enable_audio: false,
|
||||
enable_video: false,
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MediaDevicesContext value={mockMediaDevices()}>
|
||||
<TestComponent />
|
||||
</MediaDevicesContext>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
|
||||
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
|
||||
});
|
||||
|
||||
it("skipLobby does not mute inputs", () => {
|
||||
mockConfig();
|
||||
|
||||
render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
"/room/?skipLobby=true&widgetId=1234&parentUrl=www.parent.org",
|
||||
]}
|
||||
>
|
||||
<MediaDevicesContext value={mockMediaDevices()}>
|
||||
<TestComponent />
|
||||
</MediaDevicesContext>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
|
||||
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
|
||||
});
|
||||
|
||||
it("url params win over config", () => {
|
||||
// The config sets audio and video to disabled
|
||||
mockConfig({ media_devices: { enable_audio: false, enable_video: false } });
|
||||
|
||||
render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
// The Intent sets both audio and video enabled to true via the url param configuration
|
||||
"/room/?intent=start_call_dm&widgetId=1234&parentUrl=www.parent.org",
|
||||
]}
|
||||
>
|
||||
<MediaDevicesContext value={mockMediaDevices()}>
|
||||
<TestComponent />
|
||||
</MediaDevicesContext>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
// At the end we expect the url param to take precedence, resulting in true
|
||||
expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
|
||||
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,11 +81,15 @@ function useMuteState(
|
||||
export function useMuteStates(isJoined: boolean): MuteStates {
|
||||
const devices = useMediaDevices();
|
||||
|
||||
const { skipLobby } = useUrlParams();
|
||||
const { skipLobby, defaultAudioEnabled, defaultVideoEnabled } =
|
||||
useUrlParams();
|
||||
|
||||
const audio = useMuteState(devices.audioInput, () => {
|
||||
return Config.get().media_devices.enable_audio && !skipLobby && !isJoined;
|
||||
});
|
||||
const audio = useMuteState(
|
||||
devices.audioInput,
|
||||
() =>
|
||||
(defaultAudioEnabled ?? Config.get().media_devices.enable_audio) &&
|
||||
allowJoinUnmuted(skipLobby, isJoined),
|
||||
);
|
||||
useEffect(() => {
|
||||
// If audio is enabled, we need to request the device names again,
|
||||
// because iOS will not be able to switch to the correct device after un-muting.
|
||||
@@ -97,7 +101,9 @@ export function useMuteStates(isJoined: boolean): MuteStates {
|
||||
const isEarpiece = useIsEarpiece();
|
||||
const video = useMuteState(
|
||||
devices.videoInput,
|
||||
() => Config.get().media_devices.enable_video && !skipLobby && !isJoined,
|
||||
() =>
|
||||
(defaultVideoEnabled ?? Config.get().media_devices.enable_video) &&
|
||||
allowJoinUnmuted(skipLobby, isJoined),
|
||||
isEarpiece, // Force video to be unavailable if using earpiece
|
||||
);
|
||||
|
||||
@@ -164,3 +170,9 @@ export function useMuteStates(isJoined: boolean): MuteStates {
|
||||
|
||||
return useMemo(() => ({ audio, video }), [audio, video]);
|
||||
}
|
||||
|
||||
function allowJoinUnmuted(skipLobby: boolean, isJoined: boolean): boolean {
|
||||
return (
|
||||
(!skipLobby && !isJoined) || import.meta.env.VITE_PACKAGE === "embedded"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ beforeEach(() => {
|
||||
playSound = vitest.fn();
|
||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||
playSound,
|
||||
playSoundLooping: vitest.fn(),
|
||||
soundDuration: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -151,7 +151,10 @@ export const RoomPage: FC = () => {
|
||||
: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
onEnter={(): void => knock?.()}
|
||||
onEnter={async (): Promise<void> => {
|
||||
knock?.();
|
||||
return Promise.resolve();
|
||||
}}
|
||||
enterLabel={label}
|
||||
waitingForInvite={groupCallState.kind === "waitForInvite"}
|
||||
confineToRoom={confineToRoom}
|
||||
|
||||
61
src/room/WaitingForJoin.module.css
Normal file
@@ -0,0 +1,61 @@
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
position: relative;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.pulse::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -12px;
|
||||
border-radius: 9999px;
|
||||
border: 12px solid rgba(255, 255, 255, 0.6);
|
||||
animation: pulse var(--call-ring-duration-s) ease-out infinite;
|
||||
animation-delay: 1s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--cpd-color-text-on-solid-primary);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.7;
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
35% {
|
||||
transform: scale(1.15);
|
||||
opacity: 0.15;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0;
|
||||
}
|
||||
50.01% {
|
||||
transform: scale(0);
|
||||
}
|
||||
85% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,34 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="participantsLine"
|
||||
>
|
||||
<svg
|
||||
aria-label="Participants"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.175 13.825Q10.35 15 12 15t2.825-1.175T16 11t-1.175-2.825T12 7 9.175 8.175 8 11t1.175 2.825m4.237-1.412A1.93 1.93 0 0 1 12 13q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 11q0-.825.588-1.412A1.93 1.93 0 0 1 12 9q.825 0 1.412.588Q14 10.175 14 11t-.588 1.412"
|
||||
/>
|
||||
<path
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0"
|
||||
/>
|
||||
<path
|
||||
d="M16.23 18.792a13 13 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0q-.73.18-1.455.455a8 8 0 0 1-1.729-1.454q1.336-.618 2.709-.95A13.8 13.8 0 0 1 12 16q1.65 0 3.25.387 1.373.333 2.709.95a8 8 0 0 1-1.73 1.455"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41"
|
||||
data-testid="roomHeader_participants_count"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -83,6 +111,11 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="bg animate"
|
||||
data-state="closed"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="overlay"
|
||||
data-show="false"
|
||||
>
|
||||
@@ -256,7 +289,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-labelledby="«r5»"
|
||||
aria-labelledby="«r8»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -279,7 +312,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-labelledby="«ra»"
|
||||
aria-labelledby="«rd»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -301,7 +334,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="«rf»"
|
||||
aria-labelledby="«ri»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
@@ -322,7 +355,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="«rk»"
|
||||
aria-labelledby="«rn»"
|
||||
class="_button_vczzf_8 endCall _has-icon_vczzf_57 _icon-only_vczzf_50 _destructive_vczzf_107"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -348,7 +381,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
class="toggle layout"
|
||||
>
|
||||
<input
|
||||
aria-labelledby="«rp»"
|
||||
aria-labelledby="«rs»"
|
||||
name="layout"
|
||||
type="radio"
|
||||
value="spotlight"
|
||||
@@ -366,7 +399,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
aria-labelledby="«ru»"
|
||||
aria-labelledby="«r11»"
|
||||
checked=""
|
||||
name="layout"
|
||||
type="radio"
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
fromEvent,
|
||||
map,
|
||||
merge,
|
||||
type Observable,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import {
|
||||
facingModeFromLocalTrack,
|
||||
type LocalVideoTrack,
|
||||
TrackEvent,
|
||||
} from "livekit-client";
|
||||
import { useObservable, useObservableEagerState } from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { useMediaDevices } from "../MediaDevicesContext";
|
||||
import { platform } from "../Platform";
|
||||
import { useLatest } from "../useLatest";
|
||||
|
||||
/**
|
||||
* Determines whether the user should be shown a button to switch their camera,
|
||||
* producing a callback if so.
|
||||
*/
|
||||
export function useSwitchCamera(
|
||||
video$: Observable<LocalVideoTrack | null>,
|
||||
): (() => void) | null {
|
||||
const mediaDevices = useMediaDevices();
|
||||
const setVideoInput = useLatest(mediaDevices.videoInput.select);
|
||||
|
||||
// Produce an observable like the input 'video' observable, except make it
|
||||
// emit whenever the track is muted or the device changes
|
||||
const videoTrack$: Observable<LocalVideoTrack | null> = useObservable(
|
||||
(inputs$) =>
|
||||
inputs$.pipe(
|
||||
switchMap(([video$]) => video$),
|
||||
switchMap((video) => {
|
||||
if (video === null) return of(null);
|
||||
return merge(
|
||||
fromEvent(video, TrackEvent.Restarted).pipe(
|
||||
startWith(null),
|
||||
map(() => video),
|
||||
),
|
||||
fromEvent(video, TrackEvent.Muted).pipe(map(() => null)),
|
||||
);
|
||||
}),
|
||||
),
|
||||
[video$],
|
||||
);
|
||||
|
||||
const switchCamera$: Observable<(() => void) | null> = useObservable(
|
||||
(inputs$) =>
|
||||
platform === "desktop"
|
||||
? of(null)
|
||||
: inputs$.pipe(
|
||||
switchMap(([track$]) => track$),
|
||||
map((track) => {
|
||||
if (track === null) return null;
|
||||
const facingMode = facingModeFromLocalTrack(track).facingMode;
|
||||
// If the camera isn't front or back-facing, don't provide a switch
|
||||
// camera shortcut at all
|
||||
if (facingMode !== "user" && facingMode !== "environment")
|
||||
return null;
|
||||
// Restart the track with a camera facing the opposite direction
|
||||
return (): void =>
|
||||
void track
|
||||
.restartTrack({
|
||||
facingMode: facingMode === "user" ? "environment" : "user",
|
||||
})
|
||||
.then(() => {
|
||||
// Inform the MediaDeviceContext which camera was chosen
|
||||
const deviceId =
|
||||
track.mediaStreamTrack.getSettings().deviceId;
|
||||
if (deviceId !== undefined) setVideoInput.current(deviceId);
|
||||
})
|
||||
.catch((e) =>
|
||||
logger.error("Failed to switch camera", facingMode, e),
|
||||
);
|
||||
}),
|
||||
),
|
||||
[videoTrack$],
|
||||
);
|
||||
|
||||
return useObservableEagerState(switchCamera$);
|
||||
}
|
||||
@@ -70,6 +70,12 @@ test("It joins the correct Session", async () => {
|
||||
roomId: "roomId",
|
||||
client: {
|
||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||
access_token: "ACCCESS_TOKEN",
|
||||
token_type: "Bearer",
|
||||
matrix_server_name: "localhost",
|
||||
expires_in: 10000,
|
||||
}),
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
@@ -195,6 +201,12 @@ test("It should not fail with configuration error if homeserver config has livek
|
||||
roomId: "roomId",
|
||||
client: {
|
||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||
access_token: "ACCCESS_TOKEN",
|
||||
token_type: "Bearer",
|
||||
matrix_server_name: "localhost",
|
||||
expires_in: 10000,
|
||||
}),
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
|
||||
@@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import {
|
||||
isLivekitFocus,
|
||||
isLivekitFocusConfig,
|
||||
type LivekitFocus,
|
||||
type LivekitFocusActive,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
||||
|
||||
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||
@@ -20,6 +20,7 @@ import { Config } from "./config/Config";
|
||||
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
|
||||
import { MatrixRTCFocusMissingError } from "./utils/errors";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { getSFUConfigWithOpenID } from "./livekit/openIDSFU.ts";
|
||||
|
||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
||||
|
||||
@@ -46,6 +47,9 @@ async function makePreferredLivekitFoci(
|
||||
preferredFoci.push(focusInUse);
|
||||
}
|
||||
|
||||
// Warm up the first focus we owned, to ensure livekit room is created before any state event sent.
|
||||
let toWarmUp: LivekitFocus | undefined;
|
||||
|
||||
// Prioritize the .well-known/matrix/client, if available, over the configured SFU
|
||||
const domain = rtcSession.room.client.getDomain();
|
||||
if (domain) {
|
||||
@@ -55,18 +59,17 @@ async function makePreferredLivekitFoci(
|
||||
FOCI_WK_KEY
|
||||
];
|
||||
if (Array.isArray(wellKnownFoci)) {
|
||||
preferredFoci.push(
|
||||
...wellKnownFoci
|
||||
.filter((f) => !!f)
|
||||
.filter(isLivekitFocusConfig)
|
||||
.map((wellKnownFocus) => {
|
||||
logger.log(
|
||||
"Adding livekit focus from well known: ",
|
||||
wellKnownFocus,
|
||||
);
|
||||
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
||||
}),
|
||||
);
|
||||
const validWellKnownFoci = wellKnownFoci
|
||||
.filter((f) => !!f)
|
||||
.filter(isLivekitFocusConfig)
|
||||
.map((wellKnownFocus) => {
|
||||
logger.log("Adding livekit focus from well known: ", wellKnownFocus);
|
||||
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
||||
});
|
||||
if (validWellKnownFoci.length > 0) {
|
||||
toWarmUp = validWellKnownFoci[0];
|
||||
}
|
||||
preferredFoci.push(...validWellKnownFoci);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +80,15 @@ async function makePreferredLivekitFoci(
|
||||
livekit_service_url: urlFromConf,
|
||||
livekit_alias: livekitAlias,
|
||||
};
|
||||
toWarmUp = toWarmUp ?? focusFormConf;
|
||||
logger.log("Adding livekit focus from config: ", focusFormConf);
|
||||
preferredFoci.push(focusFormConf);
|
||||
}
|
||||
|
||||
if (toWarmUp) {
|
||||
// this will call the jwt/sfu/get endpoint to pre create the livekit room.
|
||||
await getSFUConfigWithOpenID(rtcSession.room.client, toWarmUp);
|
||||
}
|
||||
if (preferredFoci.length === 0)
|
||||
throw new MatrixRTCFocusMissingError(domain ?? "");
|
||||
return Promise.resolve(preferredFoci);
|
||||
@@ -112,28 +120,26 @@ export async function enterRTCSession(
|
||||
const { features, matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
||||
const useDeviceSessionMemberEvents =
|
||||
features?.feature_use_device_session_member_events;
|
||||
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
||||
rtcSession.joinRoomSession(
|
||||
await makePreferredLivekitFoci(rtcSession, livekitAlias),
|
||||
makeActiveFocus(),
|
||||
{
|
||||
notificationType: getUrlParams().sendNotificationType,
|
||||
notificationType,
|
||||
callIntent,
|
||||
useNewMembershipManager,
|
||||
manageMediaKeys: encryptMedia,
|
||||
...(useDeviceSessionMemberEvents !== undefined && {
|
||||
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
||||
}),
|
||||
delayedLeaveEventRestartMs:
|
||||
matrixRtcSessionConfig?.delayed_leave_event_restart_ms ??
|
||||
matrixRtcSessionConfig?.membership_keep_alive_period,
|
||||
matrixRtcSessionConfig?.delayed_leave_event_restart_ms,
|
||||
delayedLeaveEventDelayMs:
|
||||
matrixRtcSessionConfig?.delayed_leave_event_delay_ms ??
|
||||
matrixRtcSessionConfig?.membership_server_side_expiry_timeout,
|
||||
matrixRtcSessionConfig?.delayed_leave_event_delay_ms,
|
||||
delayedLeaveEventRestartLocalTimeoutMs:
|
||||
matrixRtcSessionConfig?.delayed_leave_event_restart_local_timeout_ms,
|
||||
networkErrorRetryMs: matrixRtcSessionConfig?.network_error_retry_ms,
|
||||
makeKeyDelay:
|
||||
matrixRtcSessionConfig?.wait_for_key_rotation_ms ??
|
||||
matrixRtcSessionConfig?.key_rotation_on_leave_delay,
|
||||
makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms,
|
||||
membershipEventExpiryMs:
|
||||
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
||||
useExperimentalToDeviceTransport,
|
||||
|
||||
@@ -23,3 +23,4 @@ The following sound effects have been originally created by Element.
|
||||
- `end_call`
|
||||
- `reactions/rock`
|
||||
- `reactions/wave`
|
||||
- `reactions/baduntss`
|
||||
|
||||