mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-26 21:09:21 +00:00
Compare commits
4 Commits
hs/apply-h
...
hs/fix-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fccd8010bb | ||
|
|
63ebb1d868 | ||
|
|
c0576741ec | ||
|
|
489399c9e2 |
@@ -44,8 +44,6 @@ 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
|
||||
@@ -165,8 +163,6 @@ 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
3
.gitignore
vendored
@@ -8,7 +8,6 @@ dist-ssr
|
||||
.idea/
|
||||
public/config.json
|
||||
backend/synapse_tmp/*
|
||||
backend/synapse_tmp_othersite/*
|
||||
/coverage
|
||||
config.json
|
||||
|
||||
@@ -29,4 +28,4 @@ yarn-error.log
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.cache/
|
||||
18
README.md
18
README.md
@@ -206,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
|
||||
including federation:
|
||||
whole stack of components which is required for a local development environment:
|
||||
|
||||
- Minimum Synapse Setup (servernameis: `synapse.m.localhost`, `synapse.othersite.m.localhost`)
|
||||
- Minimum Synapse Setup (servername: `synapse.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 LiveKit SFU Setup using dev defaults for config
|
||||
- Redis db for completeness
|
||||
- Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS)
|
||||
- Hostnames: `m.localhost`, `*.m.localhost`, `*.othersite.m.localhost`
|
||||
- Hostnames: `m.localhost`, `*.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` and `synapse.othersite.m.localhost`
|
||||
- MatrixRTC backend: `matrix-rtc.m.localhost` and `matrix-rtc.othersite.m.localhost`
|
||||
- Synapse homeserver: `synapse.m.localhost`
|
||||
- MatrixRTC backend: `matrix-rtc.m.localhost`
|
||||
- Local Element Call development `call.m.localhost` via `yarn dev --host `
|
||||
- Element Web `app.m.localhost` and `app.othersite.m.localhost`
|
||||
- Note certificates will expire on Thr, 20 September 2035 14:27:35 CEST
|
||||
- Element Web `app.m.localhost`
|
||||
- Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT
|
||||
|
||||
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._**
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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,18 +19,8 @@ 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: "synapse.othersite.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
- server_name: "matrix.org"
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
@@ -44,13 +34,6 @@ 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:
|
||||
@@ -62,3 +45,10 @@ 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
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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,6 +6,11 @@ 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,5 +1,4 @@
|
||||
# Synapse reverse proxy including .well-known/matrix/client
|
||||
# domain synapse.m.localhost
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
@@ -27,53 +26,14 @@ 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 ~ ^(/_matrix|/_synapse/client) {
|
||||
proxy_pass "http://homeserver:8008";
|
||||
location / {
|
||||
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;
|
||||
@@ -81,7 +41,6 @@ server {
|
||||
}
|
||||
|
||||
# MatrixRTC reverse proxy
|
||||
# domain matrix-rtc.m.localhost
|
||||
# - MatrixRTC Authorization Service
|
||||
# - LiveKit SFU websocket signaling connection
|
||||
upstream jwt-auth-services {
|
||||
@@ -90,14 +49,16 @@ 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/ {
|
||||
|
||||
@@ -133,54 +94,6 @@ 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;
|
||||
@@ -246,36 +159,3 @@ 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-----
|
||||
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
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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==
|
||||
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=
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
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=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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=
|
||||
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=
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -34,6 +34,5 @@ subjectAltName = @alt_names
|
||||
DNS.1 = localhost
|
||||
DNS.2 = m.localhost
|
||||
DNS.3 = *.m.localhost
|
||||
DNS.4 = *.othersite.m.localhost
|
||||
EOF
|
||||
)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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,18 +19,8 @@ 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: "synapse.othersite.m.localhost"
|
||||
accept_keys_insecurely: true
|
||||
- server_name: "matrix.org"
|
||||
|
||||
experimental_features:
|
||||
# MSC3266: Room summary API. Used for knocking over federation
|
||||
@@ -44,13 +34,6 @@ 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:
|
||||
@@ -61,10 +44,6 @@ rc_message:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_delayed_event_mgmt:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
@@ -79,3 +58,10 @@ 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
|
||||
|
||||
5
backend/redis.conf
Normal file
5
backend/redis.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
bind 0.0.0.0
|
||||
protected-mode yes
|
||||
port 6379
|
||||
timeout 0
|
||||
tcp-keepalive 300
|
||||
@@ -24,28 +24,6 @@ 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
|
||||
@@ -66,23 +44,15 @@ services:
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
livekit-1:
|
||||
image: livekit/livekit-server:latest
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
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…)
|
||||
command: redis-server /etc/redis.conf
|
||||
ports:
|
||||
# HOST_PORT:CONTAINER_PORT
|
||||
- 17880:17880/tcp
|
||||
- 17881:17881/tcp
|
||||
- 17882:17882/tcp
|
||||
- 50300-50400:50300-50400/udp
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z
|
||||
- ./backend/redis.conf:/etc/redis.conf:Z
|
||||
networks:
|
||||
- ecbackend
|
||||
|
||||
@@ -104,24 +74,6 @@ 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
|
||||
@@ -134,19 +86,6 @@ 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
|
||||
@@ -169,7 +108,4 @@ services:
|
||||
networks:
|
||||
ecbackend:
|
||||
aliases:
|
||||
- synapse.m.localhost
|
||||
- synapse.othersite.m.localhost
|
||||
- matrix-rtc.m.localhost
|
||||
- matrix-rtc.othersite.m.localhost
|
||||
|
||||
@@ -2,6 +2,3 @@ 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
|
||||
|
||||
@@ -159,8 +159,8 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
} = await registerUser(browser, userB);
|
||||
|
||||
// Invite the second user
|
||||
await ewPage1.getByRole("button", { name: "Add", exact: true }).click();
|
||||
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
|
||||
await ewPage1.getByRole("button", { name: "Add room" }).click();
|
||||
await ewPage1.getByText("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("option", { name: "Welcome Room" }),
|
||||
ewPage2.getByRole("treeitem", { name: "Welcome Room" }),
|
||||
).toBeVisible();
|
||||
await ewPage2.getByRole("option", { name: "Welcome Room" }).click();
|
||||
await ewPage2.getByRole("treeitem", { name: "Welcome Room" }).click();
|
||||
await ewPage2.getByRole("button", { name: "Accept" }).click();
|
||||
await expect(
|
||||
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
});
|
||||
@@ -49,10 +49,7 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
||||
|
||||
// Check the join indicator on the room list
|
||||
await expect(
|
||||
brooks.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByRole("button", { name: "End call" }),
|
||||
brooks.page.locator("div").filter({ hasText: /^Joined • 1$/ }),
|
||||
).toBeVisible();
|
||||
|
||||
// Join from the other side
|
||||
@@ -62,28 +59,26 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
|
||||
).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Join" }).click();
|
||||
|
||||
// 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 expect(
|
||||
whistler.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall"),
|
||||
).toBeVisible();
|
||||
|
||||
// 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 whistler.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall")
|
||||
.click();
|
||||
|
||||
// await expect(
|
||||
// brooks.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();
|
||||
|
||||
// Whistler leaves
|
||||
await whistler.page.waitForTimeout(1000);
|
||||
|
||||
@@ -35,8 +35,6 @@ 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 {
|
||||
|
||||
@@ -46,11 +46,11 @@ interface Props {
|
||||
*/
|
||||
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.
|
||||
* Whether the toast should be portaled into the root of the document (rather
|
||||
* than rendered in-place within the component tree).
|
||||
* @default true
|
||||
*/
|
||||
modal?: boolean;
|
||||
portal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +62,7 @@ export const Toast: FC<Props> = ({
|
||||
autoDismiss,
|
||||
children,
|
||||
Icon,
|
||||
modal = true,
|
||||
portal = true,
|
||||
}) => {
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
@@ -103,8 +103,8 @@ export const Toast: FC<Props> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange} modal={modal}>
|
||||
{modal ? <DialogPortal>{content}</DialogPortal> : content}
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
{portal ? <DialogPortal>{content}</DialogPortal> : content}
|
||||
</DialogRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,15 +5,12 @@ 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, onTestFinished, vi } from "vitest";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import * as PlatformMod from "../src/Platform";
|
||||
import {
|
||||
getRoomIdentifierFromUrl,
|
||||
computeUrlParams,
|
||||
HeaderStyle,
|
||||
getUrlParams,
|
||||
HeaderStyle,
|
||||
} from "../src/UrlParams";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
@@ -106,16 +103,16 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("preload", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(computeUrlParams().preload).toBe(false);
|
||||
expect(getUrlParams().preload).toBe(false);
|
||||
});
|
||||
|
||||
it("ignored in SPA mode", () => {
|
||||
expect(computeUrlParams("?preload=true").preload).toBe(false);
|
||||
expect(getUrlParams("?preload=true").preload).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).preload,
|
||||
).toBe(true);
|
||||
@@ -124,20 +121,19 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("returnToLobby", () => {
|
||||
it("is false in SPA mode", () => {
|
||||
expect(computeUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
||||
expect(getUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to false in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
"?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo")
|
||||
.returnToLobby,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("respected in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).returnToLobby,
|
||||
).toBe(true);
|
||||
@@ -146,12 +142,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("userId", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(computeUrlParams("?userId=asd").userId).toBe(null);
|
||||
expect(getUrlParams("?userId=asd").userId).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).userId,
|
||||
).toBe("asd");
|
||||
@@ -160,12 +156,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("deviceId", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(computeUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||
expect(getUrlParams("?deviceId=asd").deviceId).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).deviceId,
|
||||
).toBe("asd");
|
||||
@@ -174,12 +170,12 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("baseUrl", () => {
|
||||
it("is ignored in SPA mode", () => {
|
||||
expect(computeUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||
expect(getUrlParams("?baseUrl=asd").baseUrl).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).baseUrl,
|
||||
).toBe("asd");
|
||||
@@ -189,28 +185,28 @@ describe("UrlParams", () => {
|
||||
describe("viaServers", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).viaServers,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(computeUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("homeserver", () => {
|
||||
it("is ignored in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).homeserver,
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("is parsed in SPA mode", () => {
|
||||
expect(computeUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,7 +237,7 @@ describe("UrlParams", () => {
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: platform === "desktop" ? "notification" : "ring",
|
||||
sendNotificationType: "notification",
|
||||
});
|
||||
const joinExistingCallDefaults = (platform: string): object => ({
|
||||
confineToRoom: true,
|
||||
@@ -256,55 +252,24 @@ describe("UrlParams", () => {
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
defaultAudioEnabled: true,
|
||||
defaultVideoEnabled: true,
|
||||
});
|
||||
it("use no-intent-defaults with unknown intent", () => {
|
||||
expect(computeUrlParams()).toMatchObject(noIntentDefaults);
|
||||
expect(getUrlParams()).toMatchObject(noIntentDefaults);
|
||||
});
|
||||
|
||||
it("ignores intent if it is not a valid value", () => {
|
||||
expect(computeUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||
expect(getUrlParams("?intent=foo")).toMatchObject(noIntentDefaults);
|
||||
});
|
||||
|
||||
it("accepts start_call", () => {
|
||||
expect(
|
||||
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",
|
||||
});
|
||||
getUrlParams("?intent=start_call&widgetId=1234&parentUrl=parent.org"),
|
||||
).toMatchObject(startNewCallDefaults("desktop"));
|
||||
});
|
||||
|
||||
it("accepts join_existing", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?intent=join_existing&widgetId=1234&parentUrl=parent.org",
|
||||
),
|
||||
).toMatchObject(joinExistingCallDefaults("desktop"));
|
||||
@@ -313,55 +278,31 @@ describe("UrlParams", () => {
|
||||
|
||||
describe("skipLobby", () => {
|
||||
it("defaults to false", () => {
|
||||
expect(computeUrlParams().skipLobby).toBe(false);
|
||||
expect(getUrlParams().skipLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to false if intent is start_call in SPA mode", () => {
|
||||
expect(computeUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to false if intent is start_call in widget mode", () => {
|
||||
it("defaults to true if intent is start_call in widget mode", () => {
|
||||
expect(
|
||||
computeUrlParams(
|
||||
getUrlParams(
|
||||
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
|
||||
).skipLobby,
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("default to false if intent is join_existing", () => {
|
||||
expect(computeUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||
});
|
||||
});
|
||||
describe("header", () => {
|
||||
it("uses header if provided", () => {
|
||||
expect(computeUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||
expect(getUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||
"app_bar",
|
||||
);
|
||||
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:",
|
||||
{},
|
||||
);
|
||||
expect(getUrlParams("?header=none&hideHeader=false").header).toBe("none");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
132
src/UrlParams.ts
132
src/UrlParams.ts
@@ -8,10 +8,7 @@ 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 RTCCallIntent,
|
||||
type RTCNotificationType,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type RTCNotificationType } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { pickBy } from "lodash-es";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
@@ -29,9 +26,7 @@ export enum UserIntent {
|
||||
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",
|
||||
}
|
||||
|
||||
@@ -232,16 +227,6 @@ export interface UrlConfiguration {
|
||||
* - 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
|
||||
@@ -249,10 +234,7 @@ interface IntentAndPlatformDerivedConfiguration {
|
||||
// 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,
|
||||
IntentAndPlatformDerivedConfiguration {}
|
||||
export interface UrlParams extends UrlProperties, UrlConfiguration {}
|
||||
|
||||
// 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
|
||||
@@ -328,14 +310,8 @@ class ParamParser {
|
||||
}
|
||||
}
|
||||
|
||||
let urlParamCache: {
|
||||
search?: string;
|
||||
hash?: string;
|
||||
params?: UrlParams;
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* Gets the url params and loads them from a cache if already computed.
|
||||
* 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
|
||||
@@ -344,26 +320,6 @@ 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") ?? "");
|
||||
@@ -387,7 +343,8 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
? UserIntent.Unknown
|
||||
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
|
||||
// Here we only use constants and `platform` to determine the intent preset.
|
||||
let intentPreset: UrlConfiguration = {
|
||||
let intentPreset: UrlConfiguration;
|
||||
const inAppDefault = {
|
||||
confineToRoom: true,
|
||||
appPrompt: false,
|
||||
preload: false,
|
||||
@@ -399,38 +356,37 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
sendNotificationType: "notification" as RTCNotificationType,
|
||||
autoLeaveWhenOthersLeft: false,
|
||||
waitForCallPickup: false,
|
||||
};
|
||||
switch (intent) {
|
||||
case UserIntent.StartNewCall:
|
||||
intentPreset.skipLobby = false;
|
||||
intentPreset.callIntent = "video";
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: true,
|
||||
};
|
||||
break;
|
||||
case UserIntent.JoinExistingCall:
|
||||
// On desktop this will be overridden based on which button was used to join the call
|
||||
intentPreset.skipLobby = false;
|
||||
intentPreset.callIntent = "video";
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: false,
|
||||
};
|
||||
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";
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: true,
|
||||
autoLeaveWhenOthersLeft: true,
|
||||
waitForCallPickup: true,
|
||||
};
|
||||
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";
|
||||
intentPreset = {
|
||||
...inAppDefault,
|
||||
skipLobby: true,
|
||||
autoLeaveWhenOthersLeft: true,
|
||||
};
|
||||
break;
|
||||
// Non widget usecase defaults
|
||||
default:
|
||||
@@ -452,29 +408,6 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -527,25 +460,10 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
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?: unknown) => v !== undefined),
|
||||
...intentAndPlatformDerivedConfiguration,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`Can close reaction dialog 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby="«rbb»"
|
||||
aria-labelledby="«rb5»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ConnectionState,
|
||||
type E2EEManagerOptions,
|
||||
ExternalE2EEKeyProvider,
|
||||
type LocalTrackPublication,
|
||||
LocalVideoTrack,
|
||||
Room,
|
||||
type RoomOptions,
|
||||
@@ -182,33 +181,6 @@ 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,13 +22,11 @@ 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("");
|
||||
@@ -96,7 +94,6 @@ export function RaisedHandIndicator({
|
||||
background: "none",
|
||||
}}
|
||||
onClick={clickCallback}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
|
||||
@@ -27,8 +27,6 @@ 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";
|
||||
|
||||
@@ -193,15 +191,6 @@ export const ReactionSet: ReactionOption[] = [
|
||||
mp3: waveSoundMp3,
|
||||
},
|
||||
},
|
||||
{
|
||||
emoji: "🥁",
|
||||
name: "drum",
|
||||
alias: ["joke"],
|
||||
sound: {
|
||||
ogg: baduntssSoundOgg,
|
||||
mp3: baduntssSoundMp3,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export interface RaisedHandInfo {
|
||||
|
||||
@@ -54,8 +54,6 @@ beforeEach(() => {
|
||||
playSound = vitest.fn();
|
||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||
playSound,
|
||||
playSoundLooping: vitest.fn(),
|
||||
soundDuration: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,20 +105,6 @@ test("plays a sound when a user leaves", () => {
|
||||
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[] = [localRtcMember];
|
||||
for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) {
|
||||
|
||||
@@ -16,10 +16,6 @@ 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";
|
||||
@@ -41,18 +37,8 @@ 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,
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
.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} aria-hidden={!show}>
|
||||
<div className={styles.overlay} data-show={show}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<VoiceCallIcon aria-hidden />
|
||||
</BigIcon>
|
||||
|
||||
@@ -132,10 +132,9 @@ test("ConnectionLostError: Action handling should reset error state", async () =
|
||||
const WrapComponent = (): ReactNode => {
|
||||
const [failState, setFailState] = useState(true);
|
||||
const reconnectCallback = useCallback(
|
||||
async (action: CallErrorRecoveryAction) => {
|
||||
(action: CallErrorRecoveryAction) => {
|
||||
reconnectCallbackSpy(action);
|
||||
setFailState(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
[setFailState],
|
||||
);
|
||||
|
||||
@@ -36,9 +36,7 @@ import { type WidgetHelpers } from "../widget.ts";
|
||||
|
||||
export type CallErrorRecoveryAction = "reconnect"; // | "retry" ;
|
||||
|
||||
export type RecoveryActionHandler = (
|
||||
action: CallErrorRecoveryAction,
|
||||
) => Promise<void>;
|
||||
export type RecoveryActionHandler = (action: CallErrorRecoveryAction) => void;
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: ElementCallError;
|
||||
@@ -73,7 +71,7 @@ const ErrorPage: FC<ErrorPageProps> = ({
|
||||
if (error instanceof ConnectionLostError) {
|
||||
actions.push({
|
||||
label: t("call_ended_view.reconnect_button"),
|
||||
onClick: () => void recoveryActionHandler("reconnect"),
|
||||
onClick: () => recoveryActionHandler("reconnect"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,9 +131,9 @@ export const GroupCallErrorBoundary = ({
|
||||
widget={widget ?? null}
|
||||
error={callError}
|
||||
resetError={resetError}
|
||||
recoveryActionHandler={async (action: CallErrorRecoveryAction) => {
|
||||
await recoveryActionHandler(action);
|
||||
recoveryActionHandler={(action: CallErrorRecoveryAction) => {
|
||||
resetError();
|
||||
recoveryActionHandler(action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,14 +12,10 @@ import {
|
||||
onTestFinished,
|
||||
test,
|
||||
vi,
|
||||
vitest,
|
||||
} from "vitest";
|
||||
import { render, waitFor, screen, act } from "@testing-library/react";
|
||||
import { render, waitFor, screen } from "@testing-library/react";
|
||||
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
||||
import {
|
||||
MatrixRTCSessionEvent,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { 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";
|
||||
@@ -98,15 +94,13 @@ 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("user")}>Leave</button>
|
||||
<button onClick={() => onLeave()}>Leave</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -212,8 +206,6 @@ 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(
|
||||
@@ -266,19 +258,3 @@ 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" }));
|
||||
});
|
||||
|
||||
@@ -53,10 +53,7 @@ import { InviteModal } from "./InviteModal";
|
||||
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import {
|
||||
callEventAudioSounds,
|
||||
type CallEventSounds,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { callEventAudioSounds } from "./CallEventAudioRenderer";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import {
|
||||
@@ -128,16 +125,6 @@ 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 => {
|
||||
@@ -330,11 +317,8 @@ export const GroupCallView: FC<Props> = ({
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onLeave = useCallback(
|
||||
(
|
||||
cause: "user" | "error" = "user",
|
||||
playSound: CallEventSounds = "left",
|
||||
): void => {
|
||||
const audioPromise = leaveSoundContext.current?.playSound(playSound);
|
||||
(cause: "user" | "error" = "user"): void => {
|
||||
const audioPromise = leaveSoundContext.current?.playSound("left");
|
||||
// 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;
|
||||
@@ -513,11 +497,10 @@ export const GroupCallView: FC<Props> = ({
|
||||
return (
|
||||
<GroupCallErrorBoundary
|
||||
widget={widget}
|
||||
recoveryActionHandler={async (action) => {
|
||||
setExternalError(null);
|
||||
recoveryActionHandler={(action) => {
|
||||
if (action == "reconnect") {
|
||||
setLeft(false);
|
||||
await enterRTCSessionOrError(rtcSession).catch((e) => {
|
||||
enterRTCSessionOrError(rtcSession).catch((e) => {
|
||||
logger.error("Error re-entering RTC session", e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 { type LocalParticipant } from "livekit-client";
|
||||
import { ConnectionState, type LocalParticipant } from "livekit-client";
|
||||
import { of } from "rxjs";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -180,6 +180,7 @@ function createInCallView(): RenderResult & {
|
||||
onLeave={function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
connState={ConnectionState.Connected}
|
||||
onShareClick={null}
|
||||
/>
|
||||
</RoomContext>
|
||||
|
||||
@@ -25,11 +25,7 @@ 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,
|
||||
useObservableEagerState,
|
||||
useSubscription,
|
||||
} from "observable-hooks";
|
||||
import { useObservable, useSubscription } from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
import {
|
||||
@@ -66,6 +62,7 @@ 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,
|
||||
@@ -97,10 +94,7 @@ import {
|
||||
} from "../reactions/useReactionsSender";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
import {
|
||||
CallEventAudioRenderer,
|
||||
type CallEventSounds,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||
import {
|
||||
debugTileLayout as debugTileLayoutSetting,
|
||||
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
|
||||
@@ -118,14 +112,6 @@ 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 ?? {});
|
||||
|
||||
@@ -145,13 +131,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
sfuConfig,
|
||||
props.e2eeSystem,
|
||||
);
|
||||
const observableScope = useInitial(() => new ObservableScope());
|
||||
const connStateBehavior$ = useObservable(
|
||||
(inputs$) =>
|
||||
observableScope.behavior(
|
||||
inputs$.pipe(map(([connState]) => connState)),
|
||||
connState,
|
||||
),
|
||||
const connStateObservable$ = useObservable(
|
||||
(inputs$) => inputs$.pipe(map(([connState]) => connState)),
|
||||
[connState],
|
||||
);
|
||||
const [vm, setVm] = useState<CallViewModel | null>(null);
|
||||
@@ -194,7 +175,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
waitForCallPickup:
|
||||
waitForCallPickup && sendNotificationType === "ring",
|
||||
},
|
||||
connStateBehavior$,
|
||||
connStateObservable$,
|
||||
reactionsReader.raisedHands$,
|
||||
reactionsReader.reactions$,
|
||||
);
|
||||
@@ -210,7 +191,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
livekitRoom,
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
connStateBehavior$,
|
||||
connStateObservable$,
|
||||
autoLeaveWhenOthersLeft,
|
||||
sendNotificationType,
|
||||
waitForCallPickup,
|
||||
@@ -221,7 +202,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
return (
|
||||
<RoomContext value={livekitRoom}>
|
||||
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
|
||||
<InCallView {...props} vm={vm} livekitRoom={livekitRoom} />
|
||||
<InCallView
|
||||
{...props}
|
||||
vm={vm}
|
||||
livekitRoom={livekitRoom}
|
||||
connState={connState}
|
||||
/>
|
||||
</ReactionsSenderProvider>
|
||||
</RoomContext>
|
||||
);
|
||||
@@ -236,9 +222,10 @@ export interface InCallViewProps {
|
||||
livekitRoom: LivekitRoom;
|
||||
muteStates: MuteStates;
|
||||
/** Function to call when the user explicitly ends the call */
|
||||
onLeave: (cause: "user", soundFile?: CallEventSounds) => void;
|
||||
onLeave: () => void;
|
||||
header: HeaderStyle;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
connState: ECConnectionState;
|
||||
onShareClick: (() => void) | null;
|
||||
}
|
||||
|
||||
@@ -252,6 +239,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
muteStates,
|
||||
onLeave,
|
||||
header: headerStyle,
|
||||
connState,
|
||||
onShareClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -259,11 +247,10 @@ 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 (connectionState === ConnectionState.Disconnected)
|
||||
if (connState === ConnectionState.Disconnected)
|
||||
throw new ConnectionLostError();
|
||||
|
||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||
@@ -278,21 +265,6 @@ 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);
|
||||
@@ -354,90 +326,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||
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]);
|
||||
useSubscription(vm.autoLeave$, onLeave);
|
||||
|
||||
// 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
|
||||
@@ -647,38 +536,6 @@ 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({
|
||||
@@ -708,7 +565,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicatorsValue}
|
||||
focusable={!contentObscured}
|
||||
/>
|
||||
) : (
|
||||
<SpotlightTile
|
||||
@@ -719,13 +575,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
showIndicators={showSpotlightIndicatorsValue}
|
||||
focusable={!contentObscured}
|
||||
className={classNames(className, styles.tile)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[vm, openProfile, contentObscured],
|
||||
[vm, openProfile],
|
||||
);
|
||||
|
||||
const layouts = useMemo(() => {
|
||||
@@ -754,8 +609,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetWidth={gridBounds.height}
|
||||
targetHeight={gridBounds.width}
|
||||
showIndicators={false}
|
||||
focusable={!contentObscured}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -773,7 +626,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
model={layout}
|
||||
Layout={layers.fixed}
|
||||
Tile={Tile}
|
||||
aria-hidden={contentObscured}
|
||||
/>
|
||||
);
|
||||
const scrollingGrid = (
|
||||
@@ -783,7 +635,6 @@ 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
|
||||
@@ -872,7 +723,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
<EndCallButton
|
||||
key="end_call"
|
||||
onClick={function (): void {
|
||||
onLeave("user");
|
||||
onLeave();
|
||||
}}
|
||||
onTouchEnd={onControlsTouchEnd}
|
||||
data-testid="incall_leave"
|
||||
@@ -913,6 +764,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
// The reconnecting toast cannot be dismissed
|
||||
const onDismissReconnectingToast = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.inRoom}
|
||||
@@ -940,10 +794,18 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{renderContent()}
|
||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
{reconnectingToast}
|
||||
{earpieceOverlay}
|
||||
<Toast
|
||||
onDismiss={onDismissReconnectingToast}
|
||||
open={reconnecting}
|
||||
portal={false}
|
||||
>
|
||||
{t("common.reconnecting")}
|
||||
</Toast>
|
||||
<EarpieceOverlay
|
||||
show={earpieceMode && !reconnecting}
|
||||
onBackToVideoPressed={audioOutputSwitcher?.switch}
|
||||
/>
|
||||
<ReactionsOverlay vm={vm} />
|
||||
{waitingOverlay}
|
||||
{footer}
|
||||
{layout.type !== "pip" && (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
@@ -27,6 +26,7 @@ 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,10 +110,9 @@ function mockMediaDevices(
|
||||
return new MediaDevices(scope);
|
||||
}
|
||||
|
||||
describe("useMuteStates VITE_PACKAGE='full' (SPA) mode", () => {
|
||||
describe("useMuteStates", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -257,67 +256,3 @@ describe("useMuteStates VITE_PACKAGE='full' (SPA) mode", () => {
|
||||
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,15 +81,11 @@ function useMuteState(
|
||||
export function useMuteStates(isJoined: boolean): MuteStates {
|
||||
const devices = useMediaDevices();
|
||||
|
||||
const { skipLobby, defaultAudioEnabled, defaultVideoEnabled } =
|
||||
useUrlParams();
|
||||
const { skipLobby } = useUrlParams();
|
||||
|
||||
const audio = useMuteState(
|
||||
devices.audioInput,
|
||||
() =>
|
||||
(defaultAudioEnabled ?? Config.get().media_devices.enable_audio) &&
|
||||
allowJoinUnmuted(skipLobby, isJoined),
|
||||
);
|
||||
const audio = useMuteState(devices.audioInput, () => {
|
||||
return Config.get().media_devices.enable_audio && !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.
|
||||
@@ -101,9 +97,7 @@ export function useMuteStates(isJoined: boolean): MuteStates {
|
||||
const isEarpiece = useIsEarpiece();
|
||||
const video = useMuteState(
|
||||
devices.videoInput,
|
||||
() =>
|
||||
(defaultVideoEnabled ?? Config.get().media_devices.enable_video) &&
|
||||
allowJoinUnmuted(skipLobby, isJoined),
|
||||
() => Config.get().media_devices.enable_video && !skipLobby && !isJoined,
|
||||
isEarpiece, // Force video to be unavailable if using earpiece
|
||||
);
|
||||
|
||||
@@ -170,9 +164,3 @@ 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,8 +69,6 @@ beforeEach(() => {
|
||||
playSound = vitest.fn();
|
||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||
playSound,
|
||||
playSoundLooping: vitest.fn(),
|
||||
soundDuration: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -111,11 +111,6 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="bg animate"
|
||||
data-state="closed"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="overlay"
|
||||
data-show="false"
|
||||
>
|
||||
|
||||
@@ -120,13 +120,11 @@ 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,
|
||||
callIntent,
|
||||
notificationType: getUrlParams().sendNotificationType,
|
||||
useNewMembershipManager,
|
||||
manageMediaKeys: encryptMedia,
|
||||
...(useDeviceSessionMemberEvents !== undefined && {
|
||||
|
||||
@@ -23,4 +23,3 @@ The following sound effects have been originally created by Element.
|
||||
- `end_call`
|
||||
- `reactions/rock`
|
||||
- `reactions/wave`
|
||||
- `reactions/baduntss`
|
||||
|
||||
BIN
src/sound/blocked.mp3
Normal file
BIN
src/sound/blocked.mp3
Normal file
Binary file not shown.
BIN
src/sound/blocked.ogg
Normal file
BIN
src/sound/blocked.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -266,7 +266,7 @@ const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent;
|
||||
interface CallViewModelInputs {
|
||||
remoteParticipants$: Behavior<RemoteParticipant[]>;
|
||||
rtcMembers$: Behavior<Partial<CallMembership>[]>;
|
||||
livekitConnectionState$: Behavior<ECConnectionState>;
|
||||
connectionState$: Observable<ECConnectionState>;
|
||||
speaking: Map<Participant, Observable<boolean>>;
|
||||
mediaDevices: MediaDevices;
|
||||
initialSyncState: SyncState;
|
||||
@@ -276,9 +276,7 @@ function withCallViewModel(
|
||||
{
|
||||
remoteParticipants$ = constant([]),
|
||||
rtcMembers$ = constant([localRtcMember]),
|
||||
livekitConnectionState$: connectionState$ = constant(
|
||||
ConnectionState.Connected,
|
||||
),
|
||||
connectionState$ = of(ConnectionState.Connected),
|
||||
speaking = new Map(),
|
||||
mediaDevices = mockMediaDevices({}),
|
||||
initialSyncState = SyncState.Syncing,
|
||||
@@ -386,7 +384,7 @@ test("participants are retained during a focus switch", () => {
|
||||
b: [],
|
||||
}),
|
||||
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||
livekitConnectionState$: behavior(connectionInputMarbles, {
|
||||
connectionState$: behavior(connectionInputMarbles, {
|
||||
c: ConnectionState.Connected,
|
||||
s: ECAddonConnectionState.ECSwitchingFocus,
|
||||
}),
|
||||
@@ -1253,41 +1251,6 @@ describe("waitForCallPickup$", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("regression test: does stop ringing in case livekitConnectionState$ emits after didSendCallNotification$ has already emitted", () => {
|
||||
withTestScheduler(({ schedule, expectObservable, behavior }) => {
|
||||
withCallViewModel(
|
||||
{
|
||||
livekitConnectionState$: behavior("d 9ms c", {
|
||||
d: ConnectionState.Disconnected,
|
||||
c: ConnectionState.Connected,
|
||||
}),
|
||||
},
|
||||
(vm, rtcSession) => {
|
||||
// Fire a call notification IMMEDIATELY (its important for this test, that this happens before the livekitConnectionState$ emits)
|
||||
schedule("n", {
|
||||
n: () => {
|
||||
rtcSession.emit(
|
||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||
mockRingEvent("$notif1", 30),
|
||||
mockLegacyRingEvent,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expectObservable(vm.callPickupState$).toBe("a 9ms b 19ms c", {
|
||||
a: "unknown",
|
||||
b: "ringing",
|
||||
c: "timeout",
|
||||
});
|
||||
},
|
||||
{
|
||||
waitForCallPickup: true,
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("ringing -> success if someone joins before timeout", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
|
||||
@@ -1328,51 +1291,6 @@ describe("waitForCallPickup$", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("ringing -> unknown if we get disconnected", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
const connectionState$ = new BehaviorSubject(ConnectionState.Connected);
|
||||
// Someone joins at 20ms (both LiveKit participant and MatrixRTC member)
|
||||
withCallViewModel(
|
||||
{
|
||||
remoteParticipants$: behavior("a 19ms b", {
|
||||
a: [],
|
||||
b: [aliceParticipant],
|
||||
}),
|
||||
rtcMembers$: behavior("a 19ms b", {
|
||||
a: [localRtcMember],
|
||||
b: [localRtcMember, aliceRtcMember],
|
||||
}),
|
||||
livekitConnectionState$: connectionState$,
|
||||
},
|
||||
(vm, rtcSession) => {
|
||||
// Notify at 5ms so we enter ringing, then get disconnected 5ms later
|
||||
schedule(" 5ms r 5ms d", {
|
||||
r: () => {
|
||||
rtcSession.emit(
|
||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||
mockRingEvent("$notif2", 100),
|
||||
mockLegacyRingEvent,
|
||||
);
|
||||
},
|
||||
d: () => {
|
||||
connectionState$.next(ConnectionState.Disconnected);
|
||||
},
|
||||
});
|
||||
|
||||
expectObservable(vm.callPickupState$).toBe("a 4ms b 5ms c", {
|
||||
a: "unknown",
|
||||
b: "ringing",
|
||||
c: "unknown",
|
||||
});
|
||||
},
|
||||
{
|
||||
waitForCallPickup: true,
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("success when someone joins before we notify", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
// Join at 10ms, notify later at 20ms (state should stay success)
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
race,
|
||||
scan,
|
||||
skip,
|
||||
skipWhile,
|
||||
startWith,
|
||||
switchAll,
|
||||
switchMap,
|
||||
@@ -854,6 +853,17 @@ export class CallViewModel extends ViewModel {
|
||||
throttleTime(THROTTLE_SOUND_EFFECT_MS),
|
||||
);
|
||||
|
||||
public readonly leaveSoundEffect$ = this.userMedia$.pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, current]) =>
|
||||
current.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND &&
|
||||
current.length < prev.length,
|
||||
),
|
||||
map(() => {}),
|
||||
throttleTime(THROTTLE_SOUND_EFFECT_MS),
|
||||
);
|
||||
|
||||
/**
|
||||
* The number of participants currently in the call.
|
||||
*
|
||||
@@ -880,130 +890,99 @@ export class CallViewModel extends ViewModel {
|
||||
? this.allOthersLeft$
|
||||
: NEVER;
|
||||
|
||||
private readonly didSendCallNotification$ = fromEvent(
|
||||
this.matrixRTCSession,
|
||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||
) as Observable<
|
||||
Parameters<
|
||||
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
||||
>
|
||||
>;
|
||||
/**
|
||||
* Whenever the RTC session tells us that it intends to ring the remote
|
||||
* participant's devices, this emits an Observable tracking the current state of
|
||||
* that ringing process.
|
||||
*/
|
||||
// This is a behavior since we need to store the latest state for when we subscribe to this after `didSendCallNotification$`
|
||||
// has already emitted but we still need the latest observable with a timeout timer that only gets created on after receiving `notificationEvent`.
|
||||
// A behavior will emit the latest observable with the running timer to new subscribers.
|
||||
// see also: callPickupState$ and in particular the line: `return this.ring$.pipe(mergeAll());` here we otherwise might get an EMPTY observable if
|
||||
// `ring$` would not be a behavior.
|
||||
private readonly ring$: Behavior<"ringing" | "timeout" | "decline" | null> =
|
||||
this.scope.behavior(
|
||||
this.didSendCallNotification$.pipe(
|
||||
filter(
|
||||
([notificationEvent]) =>
|
||||
notificationEvent.notification_type === "ring",
|
||||
),
|
||||
switchMap(([notificationEvent]) => {
|
||||
const lifetimeMs = notificationEvent?.lifetime ?? 0;
|
||||
return concat(
|
||||
lifetimeMs === 0
|
||||
? // If no lifetime, skip the ring state
|
||||
of(null)
|
||||
: // Ring until lifetime ms have passed
|
||||
timer(lifetimeMs).pipe(
|
||||
ignoreElements(),
|
||||
startWith("ringing" as const),
|
||||
),
|
||||
// The notification lifetime has timed out, meaning ringing has likely
|
||||
// stopped on all receiving clients.
|
||||
of("timeout" as const),
|
||||
// This makes sure we will not drop into the `endWith("decline" as const)` state
|
||||
NEVER,
|
||||
).pipe(
|
||||
takeUntil(
|
||||
(
|
||||
fromEvent(this.matrixRoom, RoomEvent.Timeline) as Observable<
|
||||
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
|
||||
>
|
||||
).pipe(
|
||||
filter(
|
||||
([event]) =>
|
||||
event.getType() === EventType.RTCDecline &&
|
||||
event.getRelation()?.rel_type === "m.reference" &&
|
||||
event.getRelation()?.event_id ===
|
||||
notificationEvent.event_id &&
|
||||
event.getSender() !== this.userId,
|
||||
),
|
||||
),
|
||||
private readonly ring$: Observable<
|
||||
Observable<"ringing" | "timeout" | "decline">
|
||||
> = (
|
||||
fromEvent(
|
||||
this.matrixRTCSession,
|
||||
MatrixRTCSessionEvent.DidSendCallNotification,
|
||||
) as Observable<
|
||||
Parameters<
|
||||
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
|
||||
>
|
||||
>
|
||||
).pipe(
|
||||
filter(
|
||||
([notificationEvent]) => notificationEvent.notification_type === "ring",
|
||||
),
|
||||
map(([notificationEvent]) => {
|
||||
const lifetimeMs = notificationEvent?.lifetime ?? 0;
|
||||
return concat(
|
||||
lifetimeMs === 0
|
||||
? // If no lifetime, skip the ring state
|
||||
EMPTY
|
||||
: // Ring until lifetime ms have passed
|
||||
timer(lifetimeMs).pipe(
|
||||
ignoreElements(),
|
||||
startWith("ringing" as const),
|
||||
),
|
||||
endWith("decline" as const),
|
||||
);
|
||||
}),
|
||||
),
|
||||
null,
|
||||
);
|
||||
// The notification lifetime has timed out, meaning ringing has likely
|
||||
// stopped on all receiving clients.
|
||||
of("timeout" as const),
|
||||
NEVER,
|
||||
).pipe(
|
||||
takeUntil(
|
||||
(
|
||||
fromEvent(this.matrixRoom, RoomEvent.Timeline) as Observable<
|
||||
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
|
||||
>
|
||||
).pipe(
|
||||
filter(
|
||||
([event]) =>
|
||||
event.getType() === EventType.RTCDecline &&
|
||||
event.getRelation()?.rel_type === "m.reference" &&
|
||||
event.getRelation()?.event_id === notificationEvent.event_id &&
|
||||
event.getSender() !== this.userId,
|
||||
),
|
||||
),
|
||||
),
|
||||
endWith("decline" as const),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether some Matrix user other than ourself is joined to the call.
|
||||
*/
|
||||
private readonly someoneElseJoined$ = this.memberships$.pipe(
|
||||
map((ms) => ms.some((m) => m.sender !== this.userId)),
|
||||
) as Behavior<boolean>;
|
||||
);
|
||||
|
||||
/**
|
||||
* The current call pickup state of the call.
|
||||
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
|
||||
* Then we can conclude if we were the first one to join or not.
|
||||
* This may also be set if we are disconnected.
|
||||
* - "ringing": The call is ringing on other devices in this room (This client should give audiovisual feedback that this is happening).
|
||||
* - "timeout": No-one picked up in the defined time this call should be ringing on others devices.
|
||||
* The call failed. If desired this can be used as a trigger to exit the call.
|
||||
* - "success": Someone else joined. The call is in a normal state. No audiovisual feedback.
|
||||
* - null: EC is configured to never show any waiting for answer state.
|
||||
*/
|
||||
public readonly callPickupState$: Behavior<
|
||||
"unknown" | "ringing" | "timeout" | "decline" | "success" | null
|
||||
> = this.options.waitForCallPickup
|
||||
public readonly callPickupState$ = this.options.waitForCallPickup
|
||||
? this.scope.behavior<
|
||||
"unknown" | "ringing" | "timeout" | "decline" | "success"
|
||||
>(
|
||||
combineLatest(
|
||||
[this.livekitConnectionState$, this.someoneElseJoined$, this.ring$],
|
||||
(livekitConnectionState, someoneElseJoined, ring) => {
|
||||
if (livekitConnectionState === ConnectionState.Disconnected) {
|
||||
// Do not ring until we're connected.
|
||||
return "unknown" as const;
|
||||
} else if (someoneElseJoined) {
|
||||
return "success" as const;
|
||||
}
|
||||
// Show the ringing state of the most recent ringing attempt.
|
||||
// as long as we have not yet sent an RTC notification event, ring will be null -> callPickupState$ = unknown.
|
||||
return ring ?? ("unknown" as const);
|
||||
},
|
||||
this.someoneElseJoined$.pipe(
|
||||
switchMap((someoneElseJoined) =>
|
||||
someoneElseJoined
|
||||
? of("success" as const)
|
||||
: // Show the ringing state of the most recent ringing attempt.
|
||||
this.ring$.pipe(switchAll()),
|
||||
),
|
||||
// The state starts as 'unknown' because we don't know if the RTC
|
||||
// session will actually send a notify event yet. It will only be
|
||||
// known once we send our own membership and see that we were the
|
||||
// first one to join.
|
||||
startWith("unknown" as const),
|
||||
),
|
||||
)
|
||||
: constant(null);
|
||||
|
||||
public readonly leaveSoundEffect$ = combineLatest([
|
||||
this.callPickupState$,
|
||||
this.userMedia$,
|
||||
]).pipe(
|
||||
// Until the call is successful, do not play a leave sound.
|
||||
// If callPickupState$ is null, then we always play the sound as it will not conflict with a decline sound.
|
||||
skipWhile(([c]) => c !== null && c !== "success"),
|
||||
map(([, userMedia]) => userMedia),
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, current]) =>
|
||||
current.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND &&
|
||||
current.length < prev.length,
|
||||
),
|
||||
map(() => {}),
|
||||
throttleTime(THROTTLE_SOUND_EFFECT_MS),
|
||||
);
|
||||
|
||||
/**
|
||||
* List of MediaItems that we want to display, that are of type ScreenShare
|
||||
*/
|
||||
@@ -1693,7 +1672,7 @@ export class CallViewModel extends ViewModel {
|
||||
private readonly livekitRoom: LivekitRoom,
|
||||
private readonly mediaDevices: MediaDevices,
|
||||
private readonly options: CallViewModelOptions,
|
||||
public readonly livekitConnectionState$: Behavior<ECConnectionState>,
|
||||
private readonly livekitConnectionState$: Observable<ECConnectionState>,
|
||||
private readonly handsRaisedSubject$: Observable<
|
||||
Record<string, RaisedHandInfo>
|
||||
>,
|
||||
|
||||
@@ -64,7 +64,6 @@ test("GridTile is accessible", async () => {
|
||||
targetWidth={300}
|
||||
targetHeight={200}
|
||||
showSpeakingIndicators
|
||||
focusable={true}
|
||||
/>
|
||||
</ReactionsSenderProvider>,
|
||||
);
|
||||
|
||||
@@ -60,7 +60,6 @@ interface TileProps {
|
||||
targetHeight: number;
|
||||
displayName: string;
|
||||
showSpeakingIndicators: boolean;
|
||||
focusable: boolean;
|
||||
}
|
||||
|
||||
interface UserMediaTileProps extends TileProps {
|
||||
@@ -82,7 +81,6 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
menuEnd,
|
||||
className,
|
||||
displayName,
|
||||
focusable,
|
||||
...props
|
||||
}) => {
|
||||
const { toggleRaisedHand } = useReactionsSender();
|
||||
@@ -164,7 +162,6 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
/>
|
||||
}
|
||||
displayName={displayName}
|
||||
focusable={focusable}
|
||||
primaryButton={
|
||||
primaryButton ?? (
|
||||
<Menu
|
||||
@@ -172,10 +169,7 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
onOpenChange={setMenuOpen}
|
||||
title={displayName}
|
||||
trigger={
|
||||
<button
|
||||
aria-label={t("common.options")}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<button aria-label={t("common.options")}>
|
||||
<OverflowHorizontalIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
}
|
||||
@@ -214,7 +208,6 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
ref,
|
||||
vm,
|
||||
onOpenProfile,
|
||||
focusable,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -243,7 +236,6 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
className={styles.switchCamera}
|
||||
aria-label={t("switch_camera")}
|
||||
onClick={switchCamera}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<SwitchCameraSolidIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
@@ -266,7 +258,6 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
focusable={focusable}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -346,7 +337,6 @@ interface GridTileProps {
|
||||
className?: string;
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
showSpeakingIndicators: boolean;
|
||||
focusable: boolean;
|
||||
}
|
||||
|
||||
export const GridTile: FC<GridTileProps> = ({
|
||||
|
||||
@@ -47,7 +47,6 @@ describe("MediaView", () => {
|
||||
video: trackReference,
|
||||
member: undefined,
|
||||
localParticipant: false,
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
test("is accessible", async () => {
|
||||
|
||||
@@ -38,7 +38,6 @@ interface Props extends ComponentProps<typeof animated.div> {
|
||||
encryptionStatus: EncryptionStatus;
|
||||
nameTagLeadingIcon?: ReactNode;
|
||||
displayName: string;
|
||||
focusable: boolean;
|
||||
primaryButton?: ReactNode;
|
||||
raisedHandTime?: Date;
|
||||
currentReaction?: ReactionOption;
|
||||
@@ -62,7 +61,6 @@ export const MediaView: FC<Props> = ({
|
||||
unencryptedWarning,
|
||||
nameTagLeadingIcon,
|
||||
displayName,
|
||||
focusable,
|
||||
primaryButton,
|
||||
encryptionStatus,
|
||||
raisedHandTime,
|
||||
@@ -116,7 +114,6 @@ export const MediaView: FC<Props> = ({
|
||||
miniature={avatarSize < 96}
|
||||
showTimer={handRaiseTimerVisible}
|
||||
onClick={raisedHandOnClick}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
/>
|
||||
{currentReaction && (
|
||||
<ReactionIndicator
|
||||
@@ -167,7 +164,6 @@ export const MediaView: FC<Props> = ({
|
||||
label={t("common.unencrypted")}
|
||||
placement="bottom"
|
||||
isTriggerInteractive={false}
|
||||
nonInteractiveTriggerTabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<ErrorSolidIcon
|
||||
width={20}
|
||||
|
||||
@@ -59,7 +59,6 @@ test("SpotlightTile is accessible", async () => {
|
||||
expanded={false}
|
||||
onToggleExpanded={toggleExpanded}
|
||||
showIndicators
|
||||
focusable={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ interface SpotlightItemBaseProps {
|
||||
unencryptedWarning: boolean;
|
||||
encryptionStatus: EncryptionStatus;
|
||||
displayName: string;
|
||||
focusable: boolean;
|
||||
"aria-hidden"?: boolean;
|
||||
localParticipant: boolean;
|
||||
}
|
||||
@@ -113,7 +112,6 @@ interface SpotlightItemProps {
|
||||
vm: MediaViewModel;
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
focusable: boolean;
|
||||
intersectionObserver$: Observable<IntersectionObserver>;
|
||||
/**
|
||||
* Whether this item should act as a scroll snapping point.
|
||||
@@ -127,7 +125,6 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
||||
vm,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
focusable,
|
||||
intersectionObserver$,
|
||||
snap,
|
||||
"aria-hidden": ariaHidden,
|
||||
@@ -166,7 +163,6 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
|
||||
member: vm.member,
|
||||
unencryptedWarning,
|
||||
displayName,
|
||||
focusable,
|
||||
encryptionStatus,
|
||||
"aria-hidden": ariaHidden,
|
||||
localParticipant: vm.local,
|
||||
@@ -189,7 +185,6 @@ interface Props {
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
showIndicators: boolean;
|
||||
focusable: boolean;
|
||||
className?: string;
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
}
|
||||
@@ -202,7 +197,6 @@ export const SpotlightTile: FC<Props> = ({
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
showIndicators,
|
||||
focusable = true,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
@@ -299,7 +293,6 @@ export const SpotlightTile: FC<Props> = ({
|
||||
className={classNames(styles.advance, styles.back)}
|
||||
aria-label={t("common.back")}
|
||||
onClick={onBackClick}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<ChevronLeftIcon aria-hidden width={24} height={24} />
|
||||
</button>
|
||||
@@ -311,7 +304,6 @@ export const SpotlightTile: FC<Props> = ({
|
||||
vm={vm}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
focusable={focusable}
|
||||
intersectionObserver$={intersectionObserver$}
|
||||
// This is how we get the container to scroll to the right media
|
||||
// when the previous/next buttons are clicked: we temporarily
|
||||
@@ -327,7 +319,6 @@ export const SpotlightTile: FC<Props> = ({
|
||||
className={classNames(styles.expand)}
|
||||
aria-label={"maximise"}
|
||||
onClick={onToggleFullscreen}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<FullScreenIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
@@ -339,7 +330,6 @@ export const SpotlightTile: FC<Props> = ({
|
||||
expanded ? t("video_tile.collapse") : t("video_tile.expand")
|
||||
}
|
||||
onClick={onToggleExpanded}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<ToggleExpandIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
@@ -351,7 +341,6 @@ export const SpotlightTile: FC<Props> = ({
|
||||
className={classNames(styles.advance, styles.next)}
|
||||
aria-label={t("common.next")}
|
||||
onClick={onNextClick}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<ChevronRightIcon aria-hidden width={24} height={24} />
|
||||
</button>
|
||||
|
||||
@@ -32,8 +32,6 @@ async function playSound(
|
||||
buffer: AudioBuffer,
|
||||
volume: number,
|
||||
stereoPan: number,
|
||||
delayS = 0,
|
||||
abort?: AbortController,
|
||||
): Promise<void> {
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.setValueAtTime(volume, 0);
|
||||
@@ -41,62 +39,13 @@ async function playSound(
|
||||
pan.pan.setValueAtTime(stereoPan, 0);
|
||||
const src = ctx.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
abort?.signal.addEventListener("abort", () => {
|
||||
src.disconnect();
|
||||
});
|
||||
const p = new Promise<void>((r) => src.addEventListener("ended", () => r()));
|
||||
src.connect(gain).connect(pan).connect(ctx.destination);
|
||||
const p = new Promise<void>((r) => src.addEventListener("ended", () => r()));
|
||||
controls.setPlaybackStarted();
|
||||
src.start(ctx.currentTime + delayS);
|
||||
src.start();
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound though a given AudioContext, looping until stopped. Will take
|
||||
* care of connecting the correct buffer and gating
|
||||
* through gain.
|
||||
* @param volume The volume to play at.
|
||||
* @param ctx The context to play through.
|
||||
* @param buffer The buffer to play.
|
||||
* @returns A function used to end the sound. This function will return a promise when the sound has stopped.
|
||||
*/
|
||||
function playSoundLooping(
|
||||
ctx: AudioContext,
|
||||
buffer: AudioBuffer,
|
||||
volume: number,
|
||||
stereoPan: number,
|
||||
delayS?: number,
|
||||
): () => Promise<void> {
|
||||
if (delayS === 0) {
|
||||
throw Error("Looping sounds must have a delay");
|
||||
}
|
||||
|
||||
// Our audio loop
|
||||
let lastSoundPromise: Promise<void>;
|
||||
let nextSoundPromise: Promise<void>;
|
||||
let ac: AbortController | undefined;
|
||||
void (async (): Promise<void> => {
|
||||
ac = new AbortController();
|
||||
// Play a sound immediately
|
||||
lastSoundPromise = Promise.resolve();
|
||||
do {
|
||||
// Queue up the next sound.
|
||||
nextSoundPromise = playSound(ctx, buffer, volume, stereoPan, delayS, ac);
|
||||
// Await the previous sound.
|
||||
await lastSoundPromise;
|
||||
// Swap the promises over, and loop round to play the next sound.
|
||||
lastSoundPromise = nextSoundPromise;
|
||||
} while (!ac.signal.aborted);
|
||||
})();
|
||||
|
||||
return async () => {
|
||||
ac?.abort();
|
||||
// Wait for sounds to finish.
|
||||
await lastSoundPromise;
|
||||
await nextSoundPromise;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props<S extends string> {
|
||||
/**
|
||||
* The sounds to play. If no sounds should be played then
|
||||
@@ -108,13 +57,8 @@ interface Props<S extends string> {
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
interface UseAudioContext<S extends string> {
|
||||
interface UseAudioContext<S> {
|
||||
playSound(soundName: S): Promise<void>;
|
||||
playSoundLooping(soundName: S, delayS?: number): () => Promise<void>;
|
||||
/**
|
||||
* Map of sound name to duration in seconds.
|
||||
*/
|
||||
soundDuration: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,23 +146,5 @@ export function useAudioContext<S extends string>(
|
||||
earpiecePan,
|
||||
);
|
||||
},
|
||||
playSoundLooping: (name, delayS: number): (() => Promise<void>) => {
|
||||
if (!audioBuffers[name]) {
|
||||
throw Error(`Tried to play a sound that wasn't buffered (${name})`);
|
||||
}
|
||||
return playSoundLooping(
|
||||
audioContext,
|
||||
audioBuffers[name],
|
||||
soundEffectVolume * earpieceVolume,
|
||||
earpiecePan,
|
||||
delayS,
|
||||
);
|
||||
},
|
||||
soundDuration: Object.fromEntries(
|
||||
Object.entries(audioBuffers).map(([k, v]) => [
|
||||
k,
|
||||
(v as AudioBuffer).duration,
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
} from "matrix-js-sdk";
|
||||
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import {
|
||||
CallViewModel,
|
||||
type CallViewModelOptions,
|
||||
} from "../state/CallViewModel";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import {
|
||||
mockLivekitRoom,
|
||||
mockMatrixRoom,
|
||||
@@ -39,7 +36,6 @@ import {
|
||||
localRtcMember,
|
||||
} from "./test-fixtures";
|
||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||
import { constant } from "../state/Behavior";
|
||||
|
||||
export function getBasicRTCSession(
|
||||
members: RoomMember[],
|
||||
@@ -126,7 +122,6 @@ export function getBasicRTCSession(
|
||||
export function getBasicCallViewModelEnvironment(
|
||||
members: RoomMember[],
|
||||
initialRtcMemberships: CallMembership[] = [localRtcMember, aliceRtcMember],
|
||||
callViewModelOptions: Partial<CallViewModelOptions> = {},
|
||||
): {
|
||||
vm: CallViewModel;
|
||||
rtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||
@@ -153,9 +148,8 @@ export function getBasicCallViewModelEnvironment(
|
||||
mockMediaDevices({}),
|
||||
{
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
...callViewModelOptions,
|
||||
},
|
||||
constant(ConnectionState.Connected),
|
||||
of(ConnectionState.Connected),
|
||||
handRaisedSubject$,
|
||||
reactionsSubject$,
|
||||
);
|
||||
|
||||
@@ -360,8 +360,6 @@ export class MockRTCSession extends TypedEventEmitter<
|
||||
return this;
|
||||
}
|
||||
|
||||
public updateCallIntent = vitest.fn();
|
||||
|
||||
private _membershipStatus = Status.Connected;
|
||||
public get membershipStatus(): Status {
|
||||
return this._membershipStatus;
|
||||
|
||||
@@ -116,7 +116,6 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
EventType.Reaction,
|
||||
EventType.RoomRedaction,
|
||||
ElementCallReactionEventType,
|
||||
EventType.RTCDecline,
|
||||
];
|
||||
|
||||
const sendState = [
|
||||
|
||||
68
yarn.lock
68
yarn.lock
@@ -2793,10 +2793,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@npm:^15.3.0":
|
||||
version: 15.3.0
|
||||
resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:15.3.0"
|
||||
checksum: 10c0/45628f36b7b0e54a8777ae67a7233dbdf3e3cf14e0d95d21f62f89a7ea7e3f907232f1eb7b1262193b1e227759fad47af829dcccc103ded89011f13c66f01d76
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@npm:^15.1.0":
|
||||
version: 15.1.0
|
||||
resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:15.1.0"
|
||||
checksum: 10c0/19edc6d0045ff49fad8d77b6e561cee994f7513f8c18a7176ae2d3f0116c1a91980e02d10300b09c2b72dea4da4a8c3392f2bf1752057f2d6b53030a056d76d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5290,6 +5290,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/retry@npm:0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "@types/retry@npm:0.12.0"
|
||||
checksum: 10c0/7c5c9086369826f569b83a4683661557cab1361bac0897a1cefa1a915ff739acd10ca0d62b01071046fe3f5a3f7f2aec80785fe283b75602dc6726781ea3e328
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/sdp-transform@npm:^2.4.5":
|
||||
version: 2.4.10
|
||||
resolution: "@types/sdp-transform@npm:2.4.10"
|
||||
@@ -9521,13 +9528,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-network-error@npm:^1.1.0":
|
||||
version: 1.3.0
|
||||
resolution: "is-network-error@npm:1.3.0"
|
||||
checksum: 10c0/3e85a69e957988db66d5af5412efdd531a5a63e150d1bdd5647cfd4dc54fd89b1dbdd472621f8915233c3176ba1e6922afa8a51a9e363ba4693edf96a294f898
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-number-object@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "is-number-object@npm:1.1.1"
|
||||
@@ -10065,8 +10065,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"livekit-client@npm:^2.13.0":
|
||||
version: 2.15.7
|
||||
resolution: "livekit-client@npm:2.15.7"
|
||||
version: 2.15.6
|
||||
resolution: "livekit-client@npm:2.15.6"
|
||||
dependencies:
|
||||
"@livekit/mutex": "npm:1.1.1"
|
||||
"@livekit/protocol": "npm:1.39.3"
|
||||
@@ -10079,7 +10079,7 @@ __metadata:
|
||||
webrtc-adapter: "npm:^9.0.1"
|
||||
peerDependencies:
|
||||
"@types/dom-mediacapture-record": ^1
|
||||
checksum: 10c0/17ea084565a4fef31038af4ce1aa6951125dc80b78d6d320789d67bfc544d95b6b661032848221758c9ece6dab6ab0e867da683759f11b0bc65a1d94ab27d185
|
||||
checksum: 10c0/f1ab6cdf2b85647036e9de906734c1394dac497da0bd879a29d0c587c437ada262021478fcef24df99b3489a39d97fe67ab33de0785ed0a63335da2fef577192
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10298,11 +10298,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=develop":
|
||||
version: 38.3.0
|
||||
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=41d70d0b5d3f0eba92686f8089cb329d875b26b5"
|
||||
version: 37.13.0
|
||||
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=2f1d654f14be8dd03896e9e76f12017b6f9eec1c"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.3.0"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.1.0"
|
||||
another-json: "npm:^0.2.0"
|
||||
bs58: "npm:^6.0.0"
|
||||
content-type: "npm:^1.0.4"
|
||||
@@ -10311,11 +10311,11 @@ __metadata:
|
||||
matrix-events-sdk: "npm:0.0.1"
|
||||
matrix-widget-api: "npm:^1.10.0"
|
||||
oidc-client-ts: "npm:^3.0.1"
|
||||
p-retry: "npm:7"
|
||||
p-retry: "npm:4"
|
||||
sdp-transform: "npm:^2.14.1"
|
||||
unhomoglyph: "npm:^1.0.6"
|
||||
uuid: "npm:13"
|
||||
checksum: 10c0/b48528fec573f3e14d1297f360a56d52d7f313da0d4cf82ab51e4c29798b86995b8a6bd72409779746e7bcf02949bc2788bffa9aba276bfb1a76dbcbe89900a0
|
||||
uuid: "npm:11"
|
||||
checksum: 10c0/ecd019c677c272c5598617dcde407dbe4b1b11460863b2a577e33f3fd8732c9d9073ec0221b471ec1eb24e2839eec20728db7f92c9348be83126547286e50805
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10922,12 +10922,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-retry@npm:7":
|
||||
version: 7.0.0
|
||||
resolution: "p-retry@npm:7.0.0"
|
||||
"p-retry@npm:4":
|
||||
version: 4.6.2
|
||||
resolution: "p-retry@npm:4.6.2"
|
||||
dependencies:
|
||||
is-network-error: "npm:^1.1.0"
|
||||
checksum: 10c0/3c090ac72bbe00fd2f062ee6178c44f7302f298936ab2290a458575e73650e7834b556beb2b09fa9fbebedab2ec3358cb474c09710cf828972b670c3c0cb89e4
|
||||
"@types/retry": "npm:0.12.0"
|
||||
retry: "npm:^0.13.1"
|
||||
checksum: 10c0/d58512f120f1590cfedb4c2e0c42cb3fa66f3cea8a4646632fcb834c56055bb7a6f138aa57b20cc236fb207c9d694e362e0b5c2b14d9b062f67e8925580c73b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12229,6 +12230,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"retry@npm:^0.13.1":
|
||||
version: 0.13.1
|
||||
resolution: "retry@npm:0.13.1"
|
||||
checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reusify@npm:^1.0.4":
|
||||
version: 1.1.0
|
||||
resolution: "reusify@npm:1.1.0"
|
||||
@@ -13758,12 +13766,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:13":
|
||||
version: 13.0.0
|
||||
resolution: "uuid@npm:13.0.0"
|
||||
"uuid@npm:11":
|
||||
version: 11.0.5
|
||||
resolution: "uuid@npm:11.0.5"
|
||||
bin:
|
||||
uuid: dist-node/bin/uuid
|
||||
checksum: 10c0/950e4c18d57fef6c69675344f5700a08af21e26b9eff2bf2180427564297368c538ea11ac9fb2e6528b17fc3966a9fd2c5049361b0b63c7d654f3c550c9b3d67
|
||||
uuid: dist/esm/bin/uuid
|
||||
checksum: 10c0/6f59f0c605e02c14515401084ca124b9cb462b4dcac866916a49862bcf831874508a308588c23a7718269226ad11a92da29b39d761ad2b86e736623e3a33b6e7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user