mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-24 00:38:31 +08:00
Merge remote-tracking branch 'origin/livekit' into raise-hand-button
This commit is contained in:
commit
167caa32a3
6
.github/workflows/docker.yaml
vendored
6
.github/workflows/docker.yaml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check it out
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
||||
@ -48,10 +48,10 @@ jobs:
|
||||
tags: ${{ inputs.docker_tags}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out test private repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
repository: element-hq/static-call-participant
|
||||
ref: refs/heads/main
|
||||
|
6
.github/workflows/element-call.yaml
vendored
6
.github/workflows/element-call.yaml
vendored
@ -21,9 +21,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
with:
|
||||
name: build-output
|
||||
path: dist
|
||||
|
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@ -7,9 +7,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
2
.github/workflows/publish.yaml
vendored
2
.github/workflows/publish.yaml
vendored
@ -51,7 +51,7 @@ jobs:
|
||||
run: |
|
||||
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
|
6
.github/workflows/test.yaml
vendored
6
.github/workflows/test.yaml
vendored
@ -9,9 +9,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
- name: Vitest
|
||||
run: "yarn run test:coverage"
|
||||
- name: Upload to codecov
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
|
6
.github/workflows/translations-download.yaml
vendored
6
.github/workflows/translations-download.yaml
vendored
@ -13,9 +13,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
|
||||
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@d121e62763d8cc35b5fb1710e887d6e69a52d3a4 # v7.0.2
|
||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/localazy-download
|
||||
|
2
.github/workflows/translations-upload.yaml
vendored
2
.github/workflows/translations-upload.yaml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
|
||||
- name: Upload
|
||||
uses: localazy/upload@27e6b5c0fddf4551596b42226b1c24124335d24a # v1
|
||||
|
@ -44,7 +44,7 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
By default, the app expects you to have a Matrix homeserver (such as [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html)) installed locally and running on port 8008. If you wish to use a homeserver on a different URL or one that is hosted on a different server, you can add a config file as above, and include the homeserver URL that you'd like to use.
|
||||
By default, the app expects you to have a Matrix homeserver (such as [Synapse](https://element-hq.github.io/synapse/latest/setup/installation.html)) installed locally and running on port 8008. If you wish to use a homeserver on a different URL or one that is hosted on a different server, you can add a config file as above, and include the homeserver URL that you'd like to use.
|
||||
|
||||
Element Call requires a homeserver with registration enabled without any 3pid or token requirements, if you want it to be used by unregistered users. Furthermore, it is not recommended to use it with an existing homeserver where user accounts have joined normal rooms, as it may not be able to handle those yet and it may behave unreliably.
|
||||
|
||||
|
@ -7,12 +7,16 @@ services:
|
||||
auth-service:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||
hostname: auth-server
|
||||
ports:
|
||||
- 8881:8080
|
||||
# Use host network in case the configured homeserver runs on localhost
|
||||
network_mode: host
|
||||
environment:
|
||||
- LK_JWT_PORT=8881
|
||||
- LIVEKIT_URL=ws://localhost:7880
|
||||
- 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
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -23,11 +27,15 @@ services:
|
||||
image: livekit/livekit-server:latest
|
||||
command: --dev --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "7880:7880"
|
||||
- "7881:7881"
|
||||
- "7882:7882"
|
||||
- "50100-50200:50100-50200"
|
||||
# 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…)
|
||||
network_mode: host
|
||||
# ports:
|
||||
# - "7880:7880/tcp"
|
||||
# - "7881:7881/tcp"
|
||||
# - "7882:7882/tcp"
|
||||
# - "50100-50200:50100-50200/udp"
|
||||
volumes:
|
||||
- ./backend/livekit.yaml:/etc/livekit.yaml
|
||||
networks:
|
||||
|
@ -1,6 +1,10 @@
|
||||
# Don't post comments on PRs; they're noisy and the same information can be
|
||||
# gotten through the checks section at the bottom of the PR anyways
|
||||
comment: false
|
||||
github_checks:
|
||||
# Don't mark up the diffs on PRs with warnings about untested lines; we're not
|
||||
# aiming for 100% test coverage and they just get in the way of reviewing
|
||||
annotations: false
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
|
15
package.json
15
package.json
@ -29,7 +29,7 @@
|
||||
"@livekit/components-react": "^2.0.0",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/core": "^1.25.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.54.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
||||
@ -42,6 +42,7 @@
|
||||
"@sentry/vite-plugin": "^2.0.0",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
@ -71,9 +72,9 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "^1.2.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"global-jsdom": "^24.0.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"global-jsdom": "^25.0.0",
|
||||
"history": "^4.0.0",
|
||||
"i18next": "^23.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
@ -81,17 +82,17 @@
|
||||
"i18next-parser": "^9.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"knip": "^5.27.2",
|
||||
"livekit-client": "^2.0.2",
|
||||
"livekit-client": "^2.5.7",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#414ac9d8cc28330718236b90ad67a1507e146932",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#0a29063bc9e61ee70ca43820d4bb91f6a27f1237",
|
||||
"matrix-widget-api": "^1.8.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"pako": "^2.0.4",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"posthog-js": "^1.29.0",
|
||||
"posthog-js": "1.160.3",
|
||||
"prettier": "^3.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Abmelden",
|
||||
"submit": "Absenden"
|
||||
},
|
||||
"analytics_notice": "Mit der Teilnahme an der Beta akzeptierst du die Sammlung von anonymen Daten, die wir zur Verbesserung des Produkts verwenden. Weitere Informationen zu den von uns erhobenen Daten findest du in unserer <2>Datenschutzerklärung</2> und unseren <5>Cookie-Richtlinien</5>.",
|
||||
"analytics_notice": "Mit der Teilnahme an der Beta akzeptierst du die Sammlung von anonymen Daten, die wir zur Verbesserung des Produkts verwenden. Weitere Informationen zu den von uns erhobenen Daten findest du in unserer <2>Datenschutzerklärung</2> und unseren <6>Cookie-Richtlinien</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Weiter im Browser",
|
||||
"open_in_app": "In der App öffnen",
|
||||
|
@ -13,7 +13,7 @@
|
||||
"sign_out": "Αποσύνδεση",
|
||||
"submit": "Υποβολή"
|
||||
},
|
||||
"analytics_notice": "Συμμετέχοντας σε αυτή τη δοκιμαστική έκδοση, συναινείτε στη συλλογή ανώνυμων δεδομένων, τα οποία χρησιμοποιούμε για τη βελτίωση του προϊόντος. Μπορείτε να βρείτε περισσότερες πληροφορίες σχετικά με το ποια δεδομένα καταγράφουμε στην <2>Πολιτική απορρήτου</2> και στην <5>Πολιτική cookies</5>.",
|
||||
"analytics_notice": "Συμμετέχοντας σε αυτή τη δοκιμαστική έκδοση, συναινείτε στη συλλογή ανώνυμων δεδομένων, τα οποία χρησιμοποιούμε για τη βελτίωση του προϊόντος. Μπορείτε να βρείτε περισσότερες πληροφορίες σχετικά με το ποια δεδομένα καταγράφουμε στην <2>Πολιτική απορρήτου</2> και στην <6>Πολιτική cookies</6>.",
|
||||
"call_ended_view": {
|
||||
"create_account_button": "Δημιουργία λογαριασμού",
|
||||
"create_account_prompt": "<0>Γιατί να μην ολοκληρώσετε με τη δημιουργία ενός κωδικού πρόσβασης για τη διατήρηση του λογαριασμού σας;</0><1>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.</1>",
|
||||
|
@ -16,7 +16,7 @@
|
||||
"submit": "Submit",
|
||||
"upload_file": "Upload file"
|
||||
},
|
||||
"analytics_notice": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.",
|
||||
"analytics_notice": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <6>Cookie Policy</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Continue in browser",
|
||||
"open_in_app": "Open in the app",
|
||||
@ -93,6 +93,7 @@
|
||||
"layout_spotlight_label": "Spotlight",
|
||||
"lobby": {
|
||||
"ask_to_join": "Ask to join call",
|
||||
"join_as_guest": "Join as guest",
|
||||
"join_button": "Join call",
|
||||
"leave_button": "Back to recents",
|
||||
"waiting_for_invite": "Request sent"
|
||||
@ -130,8 +131,8 @@
|
||||
"register_confirm_password_label": "Confirm password",
|
||||
"register_heading": "Create your account",
|
||||
"return_home_button": "Return to home screen",
|
||||
"room_auth_view_eula_caption": "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
|
||||
"room_auth_view_join_button": "Join call now",
|
||||
"room_auth_view_continue_button": "Continue",
|
||||
"room_auth_view_eula_caption": "By clicking \"Continue\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
|
||||
"screenshare_button_label": "Share screen",
|
||||
"settings": {
|
||||
"developer_settings_label": "Developer Settings",
|
||||
|
@ -12,7 +12,7 @@
|
||||
"sign_out": "Cerrar sesión",
|
||||
"submit": "Enviar"
|
||||
},
|
||||
"analytics_notice": "Al participar en esta beta, consientes a la recogida de datos anónimos, los cuales usaremos para mejorar el producto. Puedes encontrar más información sobre que datos recogemos en nuestra <2>Política de privacidad</2> y en nuestra <5>Política sobre Cookies</5>.",
|
||||
"analytics_notice": "Al participar en esta beta, consientes a la recogida de datos anónimos, los cuales usaremos para mejorar el producto. Puedes encontrar más información sobre que datos recogemos en nuestra <2>Política de privacidad</2> y en nuestra <6>Política sobre Cookies</6>.",
|
||||
"call_ended_view": {
|
||||
"create_account_button": "Crear cuenta",
|
||||
"create_account_prompt": "<0>¿Por qué no mantienes tu cuenta estableciendo una contraseña?</0><1>Podrás mantener tu nombre y establecer un avatar para usarlo en futuras llamadas</1>",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Logi välja",
|
||||
"submit": "Saada"
|
||||
},
|
||||
"analytics_notice": "Nõustudes selle beetaversiooni kasutamisega sa nõustud ka toote arendamiseks kasutatavate anonüümsete andmete kogumisega. Täpsemat teavet kogutavate andmete kohta leiad meie <2>Privaatsuspoliitikast</2> ja meie <5>Küpsiste kasutamise reeglitest</5>.",
|
||||
"analytics_notice": "Nõustudes selle beetaversiooni kasutamisega sa nõustud ka toote arendamiseks kasutatavate anonüümsete andmete kogumisega. Täpsemat teavet kogutavate andmete kohta leiad meie <2>Privaatsuspoliitikast</2> ja meie <6>Küpsiste kasutamise reeglitest</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Jätka veebibrauseris",
|
||||
"open_in_app": "Ava rakenduses",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Déconnexion",
|
||||
"submit": "Envoyer"
|
||||
},
|
||||
"analytics_notice": "En participant à cette beta, vous consentez à la collecte de données anonymes, qui seront utilisées pour améliorer le produit. Vous trouverez plus d’informations sur les données collectées dans notre <2>Politique de vie privée</2> et notre <5>Politique de cookies</5>.",
|
||||
"analytics_notice": "En participant à cette beta, vous consentez à la collecte de données anonymes, qui seront utilisées pour améliorer le produit. Vous trouverez plus d’informations sur les données collectées dans notre <2>Politique de vie privée</2> et notre <6>Politique de cookies</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Continuer dans le navigateur",
|
||||
"open_in_app": "Ouvrir dans l’application",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Keluar",
|
||||
"submit": "Kirim"
|
||||
},
|
||||
"analytics_notice": "Dengan bergabung dalam beta ini, Anda mengizinkan kami untuk mengumpulkan data anonim, yang kami gunakan untuk meningkatkan produk ini. Anda dapat mempelajari lebih lanjut tentang data apa yang kami lacak dalam <2>Kebijakan Privasi</2> dan <5>Kebijakan Kuki</5> kami.",
|
||||
"analytics_notice": "Dengan bergabung dalam beta ini, Anda mengizinkan kami untuk mengumpulkan data anonim, yang kami gunakan untuk meningkatkan produk ini. Anda dapat mempelajari lebih lanjut tentang data apa yang kami lacak dalam <2>Kebijakan Privasi</2> dan <6>Kebijakan Kuki</6> kami.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Lanjutkan dalam peramban",
|
||||
"open_in_app": "Buka dalam aplikasi",
|
||||
|
@ -14,7 +14,7 @@
|
||||
"sign_out": "Disconnetti",
|
||||
"submit": "Invia"
|
||||
},
|
||||
"analytics_notice": "Partecipando a questa beta, acconsenti alla raccolta di dati anonimi che usiamo per migliorare il prodotto. Puoi trovare più informazioni su quali dati monitoriamo nella nostra <2>informativa sulla privacy</2> e nell'<5>informativa sui cookie</5>.",
|
||||
"analytics_notice": "Partecipando a questa beta, acconsenti alla raccolta di dati anonimi che usiamo per migliorare il prodotto. Puoi trovare più informazioni su quali dati monitoriamo nella nostra <2>informativa sulla privacy</2> e nell'<6>informativa sui cookie</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Continua nel browser",
|
||||
"open_in_app": "Apri nell'app",
|
||||
|
@ -13,7 +13,7 @@
|
||||
"sign_out": "Atteikties",
|
||||
"submit": "Iesniegt"
|
||||
},
|
||||
"analytics_notice": "Piedalīšanās šajā beta apliecina piekrišanu anonīmu datu ievākšanai, ko mēs izmantojam, lai uzlabotu izstrādājumu. Vairāk informācijas par datiem, ko mēs ievācam, var atrast mūsu <2>privātuma nosacījumos</2> un <5>sīkdatņu nosacījumos</5>.",
|
||||
"analytics_notice": "Piedalīšanās šajā beta apliecina piekrišanu anonīmu datu ievākšanai, ko mēs izmantojam, lai uzlabotu izstrādājumu. Vairāk informācijas par datiem, ko mēs ievācam, var atrast mūsu <2>privātuma nosacījumos</2> un <6>sīkdatņu nosacījumos</6>.",
|
||||
"call_ended_view": {
|
||||
"body": "Tu tiki atvienots no zvana",
|
||||
"create_account_button": "Izveidot kontu",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Wyloguj się",
|
||||
"submit": "Wyślij"
|
||||
},
|
||||
"analytics_notice": "Uczestnicząc w tej becie, upoważniasz nas do zbierania anonimowych danych, które wykorzystamy do ulepszenia produktu. Dowiedz się więcej na temat danych, które zbieramy w naszej <2>Polityce prywatności</2> i <5>Polityce ciasteczek</5>.",
|
||||
"analytics_notice": "Uczestnicząc w tej becie, upoważniasz nas do zbierania anonimowych danych, które wykorzystamy do ulepszenia produktu. Dowiedz się więcej na temat danych, które zbieramy w naszej <2>Polityce prywatności</2> i <6>Polityce ciasteczek</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Kontynuuj w przeglądarce",
|
||||
"open_in_app": "Otwórz w aplikacji",
|
||||
|
@ -13,7 +13,7 @@
|
||||
"sign_out": "Выйти",
|
||||
"submit": "Отправить"
|
||||
},
|
||||
"analytics_notice": "Участвуя в этой бета-версии, вы соглашаетесь на сбор анонимных данных, которые мы используем для улучшения продукта. Более подробную информацию о том, какие данные мы отслеживаем, вы можете найти в нашей <2> Политике конфиденциальности</2> и нашей <5> Политике использования файлов cookie</5>.",
|
||||
"analytics_notice": "Участвуя в этой бета-версии, вы соглашаетесь на сбор анонимных данных, которые мы используем для улучшения продукта. Более подробную информацию о том, какие данные мы отслеживаем, вы можете найти в нашей <2> Политике конфиденциальности</2> и нашей <6> Политике использования файлов cookie</6>.",
|
||||
"call_ended_view": {
|
||||
"create_account_button": "Создать аккаунт",
|
||||
"create_account_prompt": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Odhlásiť sa",
|
||||
"submit": "Odoslať"
|
||||
},
|
||||
"analytics_notice": "Účasťou v tejto beta verzii súhlasíte so zhromažďovaním anonymných údajov, ktoré použijeme na zlepšenie produktu. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich <2>Zásadách ochrany osobných údajov</2> a <5>Zásadách používania súborov cookie</5>.",
|
||||
"analytics_notice": "Účasťou v tejto beta verzii súhlasíte so zhromažďovaním anonymných údajov, ktoré použijeme na zlepšenie produktu. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich <2>Zásadách ochrany osobných údajov</2> a <6>Zásadách používania súborov cookie</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Pokračovať v prehliadači",
|
||||
"open_in_app": "Otvoriť v aplikácii",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "Вийти",
|
||||
"submit": "Надіслати"
|
||||
},
|
||||
"analytics_notice": "Користуючись дочасним доступом, ви даєте згоду на збір анонімних даних, які ми використовуємо для вдосконалення продукту. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашій <2>Політиці Приватності</2> і нашій <5>Політиці про куки</5>.",
|
||||
"analytics_notice": "Користуючись дочасним доступом, ви даєте згоду на збір анонімних даних, які ми використовуємо для вдосконалення продукту. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашій <2>Політиці Приватності</2> і нашій <6>Політиці про куки</6>.",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "Продовжити у браузері",
|
||||
"open_in_app": "Відкрити у застосунку",
|
||||
|
@ -13,7 +13,7 @@
|
||||
"sign_out": "登出",
|
||||
"submit": "提交"
|
||||
},
|
||||
"analytics_notice": "参与测试即表示您同意我们收集匿名数据,用于改进产品。您可以在我们的<2>隐私政策</2>和<5>Cookie政策</5>中找到有关我们跟踪哪些数据以及更多信息。",
|
||||
"analytics_notice": "参与测试即表示您同意我们收集匿名数据,用于改进产品。您可以在我们的<2>隐私政策</2>和<6>Cookie政策</6>中找到有关我们跟踪哪些数据以及更多信息。",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "在浏览器中继续",
|
||||
"open_in_app": "在应用中打开",
|
||||
|
@ -15,7 +15,7 @@
|
||||
"sign_out": "登出",
|
||||
"submit": "遞交"
|
||||
},
|
||||
"analytics_notice": "參與此測試版即表示您同意蒐集匿名資料,我們使用這些資料來改進產品。您可以在我們的<2>隱私政策</2>與我們的 <5>Cookie 政策</5> 中找到關於我們追蹤哪些資料的更多資訊。",
|
||||
"analytics_notice": "參與此測試版即表示您同意蒐集匿名資料,我們使用這些資料來改進產品。您可以在我們的<2>隱私政策</2>與我們的 <6>Cookie 政策</6> 中找到關於我們追蹤哪些資料的更多資訊。",
|
||||
"app_selection_modal": {
|
||||
"continue_in_browser": "在瀏覽器中繼續",
|
||||
"open_in_app": "在應用程式中開啟",
|
||||
|
@ -44,5 +44,6 @@
|
||||
"prHeader": "Please review modals on mobile for visual regressions."
|
||||
}
|
||||
],
|
||||
"semanticCommits": "disabled"
|
||||
"semanticCommits": "disabled",
|
||||
"ignoreDeps": ["posthog-js"]
|
||||
}
|
||||
|
@ -16,6 +16,13 @@ interface Props {
|
||||
label: string;
|
||||
value: number;
|
||||
onValueChange: (value: number) => void;
|
||||
/**
|
||||
* Event handler called when the value changes at the end of an interaction.
|
||||
* Useful when you only need to capture a final value to update a backend
|
||||
* service, or when you want to remember the last value that the user
|
||||
* "committed" to.
|
||||
*/
|
||||
onValueCommit?: (value: number) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
@ -30,6 +37,7 @@ export const Slider: FC<Props> = ({
|
||||
label,
|
||||
value,
|
||||
onValueChange: onValueChangeProp,
|
||||
onValueCommit: onValueCommitProp,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
@ -39,12 +47,17 @@ export const Slider: FC<Props> = ({
|
||||
([v]: number[]) => onValueChangeProp(v),
|
||||
[onValueChangeProp],
|
||||
);
|
||||
const onValueCommit = useCallback(
|
||||
([v]: number[]) => onValueCommitProp?.(v),
|
||||
[onValueCommitProp],
|
||||
);
|
||||
|
||||
return (
|
||||
<Root
|
||||
className={classNames(className, styles.slider)}
|
||||
value={[value]}
|
||||
onValueChange={onValueChange}
|
||||
onValueCommit={onValueCommit}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
|
@ -64,6 +64,7 @@ interface PlatformProperties {
|
||||
appVersion: string;
|
||||
matrixBackend: "embedded" | "jssdk";
|
||||
callBackend: "livekit" | "full-mesh";
|
||||
cryptoVersion?: string;
|
||||
}
|
||||
|
||||
interface PosthogSettings {
|
||||
@ -184,6 +185,9 @@ export class PosthogAnalytics {
|
||||
appVersion,
|
||||
matrixBackend: widget ? "embedded" : "jssdk",
|
||||
callBackend: "livekit",
|
||||
cryptoVersion: widget
|
||||
? undefined
|
||||
: window.matrixclient?.getCrypto()?.getVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { DisconnectReason } from "livekit-client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import {
|
||||
IPosthogEvent,
|
||||
@ -20,6 +21,9 @@ interface CallEnded extends IPosthogEvent {
|
||||
callParticipantsOnLeave: number;
|
||||
callParticipantsMax: number;
|
||||
callDuration: number;
|
||||
roomEventEncryptionKeysSent: number;
|
||||
roomEventEncryptionKeysReceived: number;
|
||||
roomEventEncryptionKeysReceivedAverageAge: number;
|
||||
}
|
||||
|
||||
export class CallEndedTracker {
|
||||
@ -43,6 +47,7 @@ export class CallEndedTracker {
|
||||
callId: string,
|
||||
callParticipantsNow: number,
|
||||
sendInstantly: boolean,
|
||||
rtcSession: MatrixRTCSession,
|
||||
): void {
|
||||
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
||||
{
|
||||
@ -51,6 +56,16 @@ export class CallEndedTracker {
|
||||
callParticipantsMax: this.cache.maxParticipantsCount,
|
||||
callParticipantsOnLeave: callParticipantsNow,
|
||||
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
|
||||
roomEventEncryptionKeysSent:
|
||||
rtcSession.statistics.counters.roomEventEncryptionKeysSent,
|
||||
roomEventEncryptionKeysReceived:
|
||||
rtcSession.statistics.counters.roomEventEncryptionKeysReceived,
|
||||
roomEventEncryptionKeysReceivedAverageAge:
|
||||
rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0
|
||||
? rtcSession.statistics.totals
|
||||
.roomEventEncryptionKeysReceivedTotalAge /
|
||||
rtcSession.statistics.counters.roomEventEncryptionKeysReceived
|
||||
: 0,
|
||||
},
|
||||
{ send_instantly: sendInstantly },
|
||||
);
|
||||
|
@ -64,15 +64,6 @@ Please see LICENSE in the repository root for full details.
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
margin-bottom: 100px;
|
||||
font-size: var(--font-size-body);
|
||||
}
|
||||
|
||||
.authLinks a {
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FC, FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
@ -18,6 +18,7 @@ import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { Config } from "../config/Config";
|
||||
import { Link } from "../button/Link";
|
||||
|
||||
export const LoginPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { merge } from "lodash";
|
||||
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
@ -15,7 +17,7 @@ import {
|
||||
export class Config {
|
||||
private static internalInstance: Config | undefined;
|
||||
|
||||
public static get(): ConfigOptions {
|
||||
public static get(): ResolvedConfigOptions {
|
||||
if (!this.internalInstance?.config)
|
||||
throw new Error("Config instance read before config got initialized");
|
||||
return this.internalInstance.config;
|
||||
@ -29,7 +31,7 @@ export class Config {
|
||||
Config.internalInstance.initPromise = downloadConfig(
|
||||
"../config.json",
|
||||
).then((config) => {
|
||||
internalInstance.config = { ...DEFAULT_CONFIG, ...config };
|
||||
internalInstance.config = merge({}, DEFAULT_CONFIG, config);
|
||||
});
|
||||
}
|
||||
return Config.internalInstance.initPromise;
|
||||
|
@ -77,6 +77,17 @@ export interface ConfigOptions {
|
||||
* A link to the end-user license agreement (EULA)
|
||||
*/
|
||||
eula: string;
|
||||
|
||||
media_devices?: {
|
||||
/**
|
||||
* Defines whether participants should start with audio enabled by default.
|
||||
*/
|
||||
enable_audio?: boolean;
|
||||
/**
|
||||
* Defines whether participants should start with video enabled by default.
|
||||
*/
|
||||
enable_video?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Overrides members from ConfigOptions that are always provided by the
|
||||
@ -88,6 +99,10 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
||||
server_name: string;
|
||||
};
|
||||
};
|
||||
media_devices: {
|
||||
enable_audio: boolean;
|
||||
enable_video: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
@ -98,4 +113,8 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
},
|
||||
},
|
||||
eula: "https://static.element.io/legal/online-EULA.pdf",
|
||||
media_devices: {
|
||||
enable_audio: true,
|
||||
enable_video: true,
|
||||
},
|
||||
};
|
||||
|
72
src/e2ee/matrixKeyProvider.test.ts
Normal file
72
src/e2ee/matrixKeyProvider.test.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
import { KeyProviderEvent } from "livekit-client";
|
||||
|
||||
import { MatrixKeyProvider } from "./matrixKeyProvider";
|
||||
|
||||
function mockRTCSession(): MatrixRTCSession {
|
||||
return {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
reemitEncryptionKeys: vi.fn(),
|
||||
} as unknown as MatrixRTCSession;
|
||||
}
|
||||
|
||||
describe("matrixKeyProvider", () => {
|
||||
test("initializes", () => {
|
||||
const keyProvider = new MatrixKeyProvider();
|
||||
expect(keyProvider).toBeTruthy();
|
||||
});
|
||||
|
||||
test("listens for key requests and emits existing keys", () => {
|
||||
const keyProvider = new MatrixKeyProvider();
|
||||
|
||||
const session = mockRTCSession();
|
||||
|
||||
keyProvider.setRTCSession(session);
|
||||
|
||||
expect(session.on).toHaveBeenCalledWith(
|
||||
MatrixRTCSessionEvent.EncryptionKeyChanged,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(session.off).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("stops listening when session changes", () => {
|
||||
const keyProvider = new MatrixKeyProvider();
|
||||
|
||||
const session1 = mockRTCSession();
|
||||
const session2 = mockRTCSession();
|
||||
|
||||
keyProvider.setRTCSession(session1);
|
||||
expect(session1.off).not.toHaveBeenCalled();
|
||||
|
||||
keyProvider.setRTCSession(session2);
|
||||
expect(session1.off).toHaveBeenCalledWith(
|
||||
MatrixRTCSessionEvent.EncryptionKeyChanged,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
test("emits existing keys", () => {
|
||||
const keyProvider = new MatrixKeyProvider();
|
||||
const setKeyListener = vi.fn();
|
||||
keyProvider.on(KeyProviderEvent.SetKey, setKeyListener);
|
||||
|
||||
const session = mockRTCSession();
|
||||
|
||||
keyProvider.setRTCSession(session);
|
||||
|
||||
expect(session.reemitEncryptionKeys).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -16,7 +16,7 @@ export class MatrixKeyProvider extends BaseKeyProvider {
|
||||
private rtcSession?: MatrixRTCSession;
|
||||
|
||||
public constructor() {
|
||||
super({ ratchetWindowSize: 0 });
|
||||
super({ ratchetWindowSize: 0, keyringSize: 256 });
|
||||
}
|
||||
|
||||
public setRTCSession(rtcSession: MatrixRTCSession): void {
|
||||
@ -35,15 +35,8 @@ export class MatrixKeyProvider extends BaseKeyProvider {
|
||||
);
|
||||
|
||||
// The new session could be aware of keys of which the old session wasn't,
|
||||
// so emit a key changed event.
|
||||
for (const [
|
||||
participant,
|
||||
encryptionKeys,
|
||||
] of this.rtcSession.getEncryptionKeys()) {
|
||||
for (const [index, encryptionKey] of encryptionKeys.entries()) {
|
||||
this.onEncryptionKeyChanged(encryptionKey, index, participant);
|
||||
}
|
||||
}
|
||||
// so emit key changed events
|
||||
this.rtcSession.reemitEncryptionKeys();
|
||||
}
|
||||
|
||||
private onEncryptionKeyChanged = (
|
||||
|
@ -97,7 +97,11 @@ function useMediaDevice(
|
||||
}
|
||||
|
||||
return {
|
||||
available: available ?? [],
|
||||
available: available
|
||||
? // Sometimes browsers (particularly Firefox) can return multiple
|
||||
// device entries for the exact same device ID; deduplicate them
|
||||
[...new Map(available.map((d) => [d.deviceId, d])).values()]
|
||||
: [],
|
||||
selectedId: alwaysDefault ? undefined : devId,
|
||||
select,
|
||||
};
|
||||
|
@ -219,6 +219,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
rtcSession.room.roomId,
|
||||
rtcSession.memberships.length,
|
||||
sendInstantly,
|
||||
rtcSession,
|
||||
);
|
||||
|
||||
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
|
||||
|
@ -18,6 +18,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { ElementWidgetActions, widget } from "../widget";
|
||||
import { Config } from "../config/Config";
|
||||
|
||||
/**
|
||||
* If there already are this many participants in the call, we automatically mute
|
||||
@ -71,8 +72,14 @@ function useMuteState(
|
||||
export function useMuteStates(): MuteStates {
|
||||
const devices = useMediaDevices();
|
||||
|
||||
const audio = useMuteState(devices.audioInput, () => true);
|
||||
const video = useMuteState(devices.videoInput, () => true);
|
||||
const audio = useMuteState(
|
||||
devices.audioInput,
|
||||
() => Config.get().media_devices.enable_audio,
|
||||
);
|
||||
const video = useMuteState(
|
||||
devices.videoInput,
|
||||
() => Config.get().media_devices.enable_video,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
widget?.api.transport
|
||||
|
@ -64,7 +64,7 @@ export const RoomAuthView: FC = () => {
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<Heading size="xl" weight="semibold" className={styles.headline}>
|
||||
{t("lobby.join_button")}
|
||||
{t("lobby.join_as_guest")}
|
||||
</Heading>
|
||||
<Form className={styles.form} onSubmit={onSubmit}>
|
||||
<FieldRow>
|
||||
@ -98,7 +98,9 @@ export const RoomAuthView: FC = () => {
|
||||
disabled={loading}
|
||||
data-testid="joincall_joincall"
|
||||
>
|
||||
{loading ? t("common.loading") : t("room_auth_view_join_button")}
|
||||
{loading
|
||||
? t("common.loading")
|
||||
: t("room_auth_view_continue_button")}
|
||||
</Button>
|
||||
<div id={recaptchaId} />
|
||||
</Form>
|
||||
|
@ -10,6 +10,7 @@ import { expect, test, vi } from "vitest";
|
||||
|
||||
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
||||
import { Config } from "../src/config/Config";
|
||||
import { DEFAULT_CONFIG } from "./config/ConfigOptions";
|
||||
|
||||
test("It joins the correct Session", async () => {
|
||||
const focusFromOlderMembership = {
|
||||
@ -34,8 +35,8 @@ test("It joins the correct Session", async () => {
|
||||
};
|
||||
|
||||
vi.spyOn(Config, "get").mockReturnValue({
|
||||
...DEFAULT_CONFIG,
|
||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||
eula: "",
|
||||
});
|
||||
const mockedSession = vi.mocked({
|
||||
room: {
|
||||
|
@ -467,6 +467,8 @@ declare global {
|
||||
*/
|
||||
export async function init(): Promise<void> {
|
||||
global.mx_rage_logger = new ConsoleLogger();
|
||||
|
||||
// configure loglevel based loggers:
|
||||
setLogExtension(logger, global.mx_rage_logger.log);
|
||||
// these are the child/prefixed loggers we want to capture from js-sdk
|
||||
// there doesn't seem to be an easy way to capture all children
|
||||
@ -474,6 +476,29 @@ export async function init(): Promise<void> {
|
||||
setLogExtension(logger.getChild(loggerName), global.mx_rage_logger.log);
|
||||
});
|
||||
|
||||
// intercept console logging so that we can get matrix_sdk logs:
|
||||
// this is nasty, but no logging hooks are provided
|
||||
[
|
||||
"trace" as const,
|
||||
"debug" as const,
|
||||
"info" as const,
|
||||
"warn" as const,
|
||||
"error" as const,
|
||||
].forEach((level) => {
|
||||
const originalMethod = window.console[level];
|
||||
if (!originalMethod) return;
|
||||
const prefix = `${level.toUpperCase()} matrix_sdk`;
|
||||
window.console[level] = (...args): void => {
|
||||
originalMethod(...args);
|
||||
// args for calls from the matrix-sdk-crypto-wasm look like:
|
||||
// ["DEBUG matrix_sdk_indexeddb::crypto_store: IndexedDbCryptoStore: opening main store matrix-js-sdk::matrix-sdk-crypto\n at /home/runner/.cargo/git/checkouts/matrix-rust-sdk-1f4927f82a3d27bb/07aa6d7/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs:267"]
|
||||
if (typeof args[0] === "string" && args[0].startsWith(prefix)) {
|
||||
// we pass all the args on to the logger in case there are more sent in future
|
||||
global.mx_rage_logger.log(LogLevel[level], "matrix_sdk", ...args);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return tryInitStorage();
|
||||
}
|
||||
|
||||
|
@ -199,7 +199,7 @@ test("participants are retained during a focus switch", () => {
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [":0", `${aliceId}:0`, `${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -243,12 +243,12 @@ test("screen sharing activates spotlight layout", () => {
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: [":0", `${aliceId}:0`, `${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0:screen-share`],
|
||||
grid: [":0", `${aliceId}:0`, `${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
c: {
|
||||
type: "spotlight-landscape",
|
||||
@ -256,17 +256,17 @@ test("screen sharing activates spotlight layout", () => {
|
||||
`${aliceId}:0:screen-share`,
|
||||
`${bobId}:0:screen-share`,
|
||||
],
|
||||
grid: [":0", `${aliceId}:0`, `${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
d: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${bobId}:0:screen-share`],
|
||||
grid: [":0", `${aliceId}:0`, `${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
e: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
grid: [":0", `${aliceId}:0`, `${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -335,8 +335,8 @@ export class CallViewModel extends ViewModel {
|
||||
const newItems = new Map(
|
||||
function* (this: CallViewModel): Iterable<[string, MediaItem]> {
|
||||
for (const p of [localParticipant, ...remoteParticipants]) {
|
||||
const userMediaId = p === localParticipant ? "local" : p.identity;
|
||||
const member = findMatrixMember(this.matrixRoom, userMediaId);
|
||||
const id = p === localParticipant ? "local" : p.identity;
|
||||
const member = findMatrixMember(this.matrixRoom, id);
|
||||
if (member === undefined)
|
||||
logger.warn(
|
||||
`Ruh, roh! No matrix member found for SFU participant '${p.identity}': creating g-g-g-ghost!`,
|
||||
@ -345,7 +345,7 @@ export class CallViewModel extends ViewModel {
|
||||
// Create as many tiles for this participant as called for by
|
||||
// the duplicateTiles option
|
||||
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
||||
const userMediaId = `${p.identity}:${i}`;
|
||||
const userMediaId = `${id}:${i}`;
|
||||
yield [
|
||||
userMediaId,
|
||||
prevItems.get(userMediaId) ??
|
||||
|
@ -13,43 +13,47 @@ import {
|
||||
withTestScheduler,
|
||||
} from "../utils/test";
|
||||
|
||||
test("set a participant's volume", async () => {
|
||||
test("control a participant's volume", async () => {
|
||||
const setVolumeSpy = vi.fn();
|
||||
await withRemoteMedia({}, { setVolume: setVolumeSpy }, (vm) =>
|
||||
withTestScheduler(({ expectObservable, schedule }) => {
|
||||
schedule("-a|", {
|
||||
a() {
|
||||
vm.setLocalVolume(0.8);
|
||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
|
||||
},
|
||||
});
|
||||
expectObservable(vm.localVolume).toBe("ab", { a: 1, b: 0.8 });
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("mute and unmute a participant", async () => {
|
||||
const setVolumeSpy = vi.fn();
|
||||
await withRemoteMedia({}, { setVolume: setVolumeSpy }, (vm) =>
|
||||
withTestScheduler(({ expectObservable, schedule }) => {
|
||||
schedule("-abc|", {
|
||||
schedule("-ab---c---d|", {
|
||||
a() {
|
||||
// Try muting by toggling
|
||||
vm.toggleLocallyMuted();
|
||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
|
||||
},
|
||||
b() {
|
||||
// Try unmuting by dragging the slider back up
|
||||
vm.setLocalVolume(0.6);
|
||||
vm.setLocalVolume(0.8);
|
||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
|
||||
vm.commitLocalVolume();
|
||||
expect(setVolumeSpy).toHaveBeenCalledWith(0.6);
|
||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
|
||||
},
|
||||
c() {
|
||||
// Try muting by dragging the slider back down
|
||||
vm.setLocalVolume(0.2);
|
||||
vm.setLocalVolume(0);
|
||||
vm.commitLocalVolume();
|
||||
expect(setVolumeSpy).toHaveBeenCalledWith(0.2);
|
||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
|
||||
},
|
||||
d() {
|
||||
// Try unmuting by toggling
|
||||
vm.toggleLocallyMuted();
|
||||
// The volume should return to the last non-zero committed volume
|
||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
|
||||
},
|
||||
});
|
||||
expectObservable(vm.locallyMuted).toBe("ab-c", {
|
||||
a: false,
|
||||
b: true,
|
||||
c: false,
|
||||
expectObservable(vm.localVolume).toBe("ab(cd)(ef)g", {
|
||||
a: 1,
|
||||
b: 0,
|
||||
c: 0.6,
|
||||
d: 0.8,
|
||||
e: 0.2,
|
||||
f: 0,
|
||||
g: 0.8,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
@ -26,10 +26,12 @@ import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
Subject,
|
||||
combineLatest,
|
||||
distinctUntilKeyChanged,
|
||||
fromEvent,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
@ -39,6 +41,7 @@ import { useEffect } from "react";
|
||||
import { ViewModel } from "./ViewModel";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { alwaysShowSelf } from "../settings/settings";
|
||||
import { accumulate } from "../utils/observable";
|
||||
|
||||
// TODO: Move this naming logic into the view model
|
||||
export function useDisplayName(vm: MediaViewModel): string {
|
||||
@ -232,18 +235,51 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
* A remote participant's user media.
|
||||
*/
|
||||
export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
private readonly _locallyMuted = new BehaviorSubject(false);
|
||||
/**
|
||||
* Whether we've disabled this participant's audio.
|
||||
*/
|
||||
public readonly locallyMuted: Observable<boolean> = this._locallyMuted;
|
||||
private readonly locallyMutedToggle = new Subject<void>();
|
||||
private readonly localVolumeAdjustment = new Subject<number>();
|
||||
private readonly localVolumeCommit = new Subject<void>();
|
||||
|
||||
private readonly _localVolume = new BehaviorSubject(1);
|
||||
/**
|
||||
* The volume to which we've set this participant's audio, as a scalar
|
||||
* The volume to which this participant's audio is set, as a scalar
|
||||
* multiplier.
|
||||
*/
|
||||
public readonly localVolume: Observable<number> = this._localVolume;
|
||||
public readonly localVolume: Observable<number> = merge(
|
||||
this.locallyMutedToggle.pipe(map(() => "toggle mute" as const)),
|
||||
this.localVolumeAdjustment,
|
||||
this.localVolumeCommit.pipe(map(() => "commit" as const)),
|
||||
).pipe(
|
||||
accumulate({ volume: 1, committedVolume: 1 }, (state, event) => {
|
||||
switch (event) {
|
||||
case "toggle mute":
|
||||
return {
|
||||
...state,
|
||||
volume: state.volume === 0 ? state.committedVolume : 0,
|
||||
};
|
||||
case "commit":
|
||||
// Dragging the slider to zero should have the same effect as
|
||||
// muting: keep the original committed volume, as if it were never
|
||||
// dragged
|
||||
return {
|
||||
...state,
|
||||
committedVolume:
|
||||
state.volume === 0 ? state.committedVolume : state.volume,
|
||||
};
|
||||
default:
|
||||
// Volume adjustment
|
||||
return { ...state, volume: event };
|
||||
}
|
||||
}),
|
||||
map(({ volume }) => volume),
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether this participant's audio is disabled.
|
||||
*/
|
||||
public readonly locallyMuted: Observable<boolean> = this.localVolume.pipe(
|
||||
map((volume) => volume === 0),
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
public constructor(
|
||||
id: string,
|
||||
@ -253,22 +289,24 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
) {
|
||||
super(id, member, participant, callEncrypted);
|
||||
|
||||
// Sync the local mute state and volume with LiveKit
|
||||
combineLatest([this._locallyMuted, this._localVolume], (muted, volume) =>
|
||||
muted ? 0 : volume,
|
||||
)
|
||||
// Sync the local volume with LiveKit
|
||||
this.localVolume
|
||||
.pipe(this.scope.bind())
|
||||
.subscribe((volume) => {
|
||||
(this.participant as RemoteParticipant).setVolume(volume);
|
||||
});
|
||||
.subscribe((volume) =>
|
||||
(this.participant as RemoteParticipant).setVolume(volume),
|
||||
);
|
||||
}
|
||||
|
||||
public toggleLocallyMuted(): void {
|
||||
this._locallyMuted.next(!this._locallyMuted.value);
|
||||
this.locallyMutedToggle.next();
|
||||
}
|
||||
|
||||
public setLocalVolume(value: number): void {
|
||||
this._localVolume.next(value);
|
||||
this.localVolumeAdjustment.next(value);
|
||||
}
|
||||
|
||||
public commitLocalVolume(): void {
|
||||
this.localVolumeCommit.next();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,6 +230,7 @@ const RemoteUserMediaTile = forwardRef<
|
||||
(v: number) => vm.setLocalVolume(v),
|
||||
[vm],
|
||||
);
|
||||
const onCommitLocalVolume = useCallback(() => vm.commitLocalVolume(), [vm]);
|
||||
|
||||
const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon;
|
||||
|
||||
@ -253,10 +254,10 @@ const RemoteUserMediaTile = forwardRef<
|
||||
label={t("video_tile.volume")}
|
||||
value={localVolume}
|
||||
onValueChange={onChangeLocalVolume}
|
||||
min={0.1}
|
||||
onValueCommit={onCommitLocalVolume}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
disabled={locallyMuted}
|
||||
/>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
78
src/useTheme.test.ts
Normal file
78
src/useTheme.test.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
Mock,
|
||||
test,
|
||||
vi,
|
||||
} from "vitest";
|
||||
|
||||
import { useTheme } from "./useTheme";
|
||||
import { useUrlParams } from "./UrlParams";
|
||||
|
||||
// Mock the useUrlParams hook
|
||||
vi.mock("./UrlParams", () => ({
|
||||
useUrlParams: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("useTheme", () => {
|
||||
let originalClassList: DOMTokenList;
|
||||
beforeEach(() => {
|
||||
// Save the original classList to setup spies
|
||||
originalClassList = document.body.classList;
|
||||
|
||||
vi.spyOn(originalClassList, "add");
|
||||
vi.spyOn(originalClassList, "remove");
|
||||
vi.spyOn(originalClassList, "item").mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ setTheme: null, add: ["cpd-theme-dark"] },
|
||||
{ setTheme: "light", add: ["cpd-theme-light"] },
|
||||
{ setTheme: "dark-high-contrast", add: ["cpd-theme-dark-hc"] },
|
||||
{ setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] },
|
||||
])("apply procedure", ({ setTheme, add }) => {
|
||||
test(`should apply ${add[0]} theme when ${setTheme} theme is specified`, () => {
|
||||
(useUrlParams as Mock).mockReturnValue({ theme: setTheme });
|
||||
|
||||
renderHook(() => useTheme());
|
||||
|
||||
expect(originalClassList.remove).toHaveBeenCalledWith(
|
||||
"cpd-theme-light",
|
||||
"cpd-theme-dark",
|
||||
"cpd-theme-light-hc",
|
||||
"cpd-theme-dark-hc",
|
||||
);
|
||||
expect(originalClassList.add).toHaveBeenCalledWith(...add);
|
||||
});
|
||||
});
|
||||
|
||||
test("should not reapply the same theme if it hasn't changed", () => {
|
||||
(useUrlParams as Mock).mockReturnValue({ theme: "dark" });
|
||||
// Simulate a previous theme
|
||||
originalClassList.item = vi.fn().mockReturnValue("cpd-theme-dark");
|
||||
|
||||
renderHook(() => useTheme());
|
||||
|
||||
expect(document.body.classList.add).not.toHaveBeenCalledWith(
|
||||
"cpd-theme-dark",
|
||||
);
|
||||
|
||||
// Ensure the 'no-theme' class is removed
|
||||
expect(document.body.classList.remove).toHaveBeenCalledWith("no-theme");
|
||||
expect(originalClassList.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user