Compare commits

..

1 Commits

Author SHA1 Message Date
Enrico Schwendig
0f1866517b change stats interval to 10s 2023-05-05 15:41:53 +01:00
105 changed files with 1711 additions and 3250 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
"build": "vite build",
"serve": "vite preview",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
@@ -53,7 +53,7 @@
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f884c78579c336a03bc20ff8f4e92c46582822b6",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#261bc81554580b442769a65ceed2b154178fbe1c",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
@@ -118,11 +118,6 @@
"\\.(css|less|svg)+$": "identity-obj-proxy",
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
},
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"
]
}
}
}

View File

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

View File

@@ -26,12 +26,14 @@
"Version: {{version}}": "Verze: {{version}}",
"Username": "Uživatelské jméno",
"User menu": "Uživatelské menu",
"User ID": "ID uživatele",
"Unmute microphone": "Zapnout mikrofon",
"Turn on camera": "Zapnout kameru",
"Turn off camera": "Vypnout kameru",
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
"Thanks! We'll get right on it.": "Děkujeme! Hned se na to vrhneme.",
"Take me Home": "Domovská obrazovka",
"Submitting feedback…": "Odesílání zpětné vazby…",
"Submit feedback": "Dát feedback",
"Stop sharing screen": "Zastavit sdílení obrazovek",
"Speaker {{n}}": "Reproduktor {{n}}",
@@ -46,6 +48,8 @@
"Sending debug logs…": "Posílání ladícího záznamu…",
"Send debug logs": "Poslat ladící záznam",
"Select an option": "Vyberte možnost",
"Saving…": "Ukládání…",
"Save": "Uložit",
"Return to home screen": "Vrátit se na domácí obrazovku",
"Remove": "Odstranit",
"Registering…": "Registrování…",
@@ -62,11 +66,12 @@
"Microphone permissions needed to join the call.": "Přístup k mikrofonu je nutný pro připojení se k hovoru.",
"Microphone {{n}}": "Mikrofon {{n}}",
"Microphone": "Mikrofon",
"Login to your account": "Přihlásit se ke svému účtu",
"Login to your account": "Přihlásit se ke svůmu účtu",
"Login": "Přihlášení",
"Logging in…": "Přihlašování se…",
"Local volume": "Lokální hlasitost",
"Loading…": "Načítání…",
"Loading room…": "Načítání místnosti…",
"Leave": "Opustit hovor",
"Join call now": "Připojit se k hovoru",
"Join call": "Připojit se k hovoru",
@@ -78,6 +83,7 @@
"{{name}} is talking…": "{{name}} mluví…",
"{{name}} is presenting": "{{name}} prezentuje",
"{{name}} (Connecting...)": "{{name}} (Připojení...)",
"{{displayName}}, your call is now ended": "{{displayName}}, váš hovor je nyní ukončen",
"{{count}} people connected|other": "{{count}} lidí připojeno",
"{{count}} people connected|one": "{{count}} lidí připojeno",
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Tato možnost způsobí, že zvuk účastníků hovoru se bude tvářit jako by přicházel z místa, kde jsou umístěni na obrazovce.(Experimentální možnost: může způsobit nestabilitu audia.)",
@@ -97,9 +103,11 @@
"Press and hold spacebar to talk over {{name}}": "Zmáčkněte a držte mezerník, abyste mluvili přes {{name}}",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Ostatní uživatelé se pokoušejí připojit k tomuto hovoru s nekompatibilních verzí. Tito uživatelé by se měli ujistit, že stránku načetli znovu:<1>{userLis}</1>",
"Not registered yet? <2>Create an account</2>": "Nejste registrovaní? <2>Vytvořit účet</2>",
"More menu": "Další možnosti",
"Join existing call?": "Připojit se k existujícimu hovoru?",
"Include debug logs": "Zahrnout ladící záznamy",
"Home": "Domov",
"Having trouble? Help us fix it.": "Máte problémy? Pomozte nám je spravit.",
"Grid layout menu": "Menu rozložení",
"Go": "Pokračovat",
"Full screen": "Zvětšit na celou obrazovku",
@@ -111,6 +119,7 @@
"Display name": "Zobrazované jméno",
"Developer": "Vývojář",
"Details": "Detaily",
"Description (optional)": "Popis (nepovinný)",
"Debug log request": "Žádost o protokoly ladění",
"Debug log": "Protokoly ladění",
"Create account": "Vytvořit účet",
@@ -125,8 +134,5 @@
"{{name}} (Waiting for video...)": "{{name}} (Čekání na video...)",
"This feature is only supported on Firefox.": "Tato funkce je podporována jen ve Firefoxu.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Odeslání ladících záznamů nám pomůže diagnostikovat problém.</0>",
"<0>Oops, something's gone wrong.</0>": "<0>Oops, něco se pokazilo.</0>",
"Use the upcoming grid system": "Používat nový systém pro zobrazení videí",
"Expose developer settings in the settings window.": "Zobrazit vývojářské nastavení.",
"Developer Settings": "Vývojářské nastavení"
"<0>Oops, something's gone wrong.</0>": "<0>Oops, něco se pokazilo.</0>"
}

View File

@@ -23,6 +23,7 @@
"Create account": "Konto erstellen",
"Debug log": "Debug-Protokoll",
"Debug log request": "Debug-Log Anfrage",
"Description (optional)": "Beschreibung (optional)",
"Details": "Details",
"Developer": "Entwickler",
"Display name": "Anzeigename",
@@ -32,6 +33,7 @@
"Full screen": "Vollbild",
"Go": "Los gehts",
"Grid layout menu": "Grid-Layout-Menü",
"Having trouble? Help us fix it.": "Du hast ein Problem? Hilf uns, es zu beheben.",
"Home": "Startseite",
"Include debug logs": "Debug-Protokolle einschließen",
"Incompatible versions": "Inkompatible Versionen",
@@ -43,6 +45,7 @@
"Join call now": "Anruf beitreten",
"Join existing call?": "An bestehendem Anruf teilnehmen?",
"Leave": "Verlassen",
"Loading room…": "Lade Raum …",
"Loading…": "Lade …",
"Local volume": "Lokale Lautstärke",
"Logging in…": "Anmelden …",
@@ -52,6 +55,7 @@
"Microphone permissions needed to join the call.": "Mikrofon-Berechtigung ist erforderlich, um dem Anruf beizutreten.",
"Microphone {{n}}": "Mikrofon {{n}}",
"More": "Mehr",
"More menu": "Weiteres Menü",
"Mute microphone": "Mikrofon stummschalten",
"No": "Nein",
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
@@ -72,6 +76,8 @@
"Release to stop": "Loslassen zum Stoppen",
"Remove": "Entfernen",
"Return to home screen": "Zurück zum Startbildschirm",
"Save": "Speichern",
"Saving…": "Speichere …",
"Select an option": "Wähle eine Option",
"Send debug logs": "Debug-Logs senden",
"Sending…": "Senden …",
@@ -86,6 +92,7 @@
"Spotlight": "Rampenlicht",
"Stop sharing screen": "Beenden der Bildschirmfreigabe",
"Submit feedback": "Rückmeldung geben",
"Submitting feedback…": "Sende Rückmeldung …",
"Take me Home": "Zurück zur Startseite",
"Talk over speaker": "Aktiven Sprecher verdrängen und sprechen",
"Talking…": "Sprechen …",
@@ -96,6 +103,7 @@
"Turn off camera": "Kamera ausschalten",
"Turn on camera": "Kamera einschalten",
"Unmute microphone": "Mikrofon aktivieren",
"User ID": "Benutzer-ID",
"User menu": "Benutzermenü",
"Username": "Benutzername",
"Version: {{version}}": "Version: {{version}}",
@@ -111,6 +119,7 @@
"Your recent calls": "Deine letzten Anrufe",
"{{count}} people connected|one": "{{count}} Person verbunden",
"{{count}} people connected|other": "{{count}} Personen verbunden",
"{{displayName}}, your call is now ended": "{{displayName}}, dein Anruf wurde beendet",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} präsentiert",
"{{name}} is talking…": "{{name}} spricht …",
@@ -130,17 +139,5 @@
"Expose developer settings in the settings window.": "Zeige die Entwicklereinstellungen im Einstellungsfenster.",
"Developer Settings": "Entwicklereinstellungen",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Mit der Teilnahme an der Beta akzeptierst du die Sammlung von anonymen Daten, die wir zur Verbesserung des Produkts verwenden. Weitere Informationen zu den von uns erhobenen Daten findest du in unserer <2>Datenschutzerklärung</2> und unseren <5>Cookie-Richtlinien</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.",
"Feedback": "Rückmeldung",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.",
"Your feedback": "Deine Rückmeldung",
"Thanks, we received your feedback!": "Danke, wir haben deine Rückmeldung erhalten!",
"Submitting…": "Sende …",
"Submit": "Absenden",
"{{count}} stars|other": "{{count}} Sterne",
"{{displayName}}, your call has ended.": "{{displayName}}, dein Anruf wurde beendet.",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Wir würden uns freuen, deine Rückmeldung zu hören, um deine Erfahrung verbessern zu können.</0>",
"How did it go?": "Wie ist es gelaufen?",
"{{count}} stars|one": "{{count}} Stern",
"<0>Thanks for your feedback!</0>": "<0>Danke für deine Rückmeldung!</0>"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam."
}

View File

@@ -9,6 +9,7 @@
"Share screen": "Κοινή χρήση οθόνης",
"Sending…": "Αποστολή…",
"Select an option": "Επιλέξτε μια επιλογή",
"Saving…": "Αποθήκευση…",
"Remove": "Αφαίρεση",
"Registering…": "Εγγραφή…",
"Press and hold to talk": "Πατήστε παρατεταμένα για να μιλήσετε",
@@ -51,6 +52,7 @@
"Spatial audio": "Χωρικός ήχος",
"Sign out": "Αποσύνδεση",
"Settings": "Ρυθμίσεις",
"Save": "Αποθήκευση",
"Return to home screen": "Επιστροφή στην αρχική οθόνη",
"Register": "Εγγραφή",
"Profile": "Προφίλ",
@@ -60,12 +62,14 @@
"Not now, return to home screen": "Όχι τώρα, επιστροφή στην αρχική οθόνη",
"No": "Όχι",
"Mute microphone": "Σίγαση μικροφώνου",
"More menu": "Μενού περισσότερα",
"More": "Περισσότερα",
"Microphone permissions needed to join the call.": "Απαιτούνται δικαιώματα μικροφώνου για συμμετοχή στην κλήση.",
"Microphone {{n}}": "Μικρόφωνο {{n}}",
"Microphone": "Μικρόφωνο",
"Login": "Σύνδεση",
"Loading…": "Φόρτωση…",
"Loading room…": "Φόρτωση δωματίου…",
"Leave": "Αποχώρηση",
"Join existing call?": "Συμμετοχή στην υπάρχουσα κλήση;",
"Join call now": "Συμμετοχή στην κλήση τώρα",
@@ -74,6 +78,7 @@
"Full screen": "Πλήρη οθόνη",
"Exit full screen": "Έξοδος από πλήρη οθόνη",
"Details": "Λεπτομέρειες",
"Description (optional)": "Περιγραφή (προαιρετική)",
"Create account": "Δημιουργία λογαριασμού",
"Copy and share this call link": "Αντιγράψτε και μοιραστείτε αυτόν τον σύνδεσμο κλήσης",
"Copy": "Αντιγραφή",
@@ -90,6 +95,7 @@
"{{name}} is presenting": "{{name}} παρουσιάζει",
"{{name}} (Waiting for video...)": "{{name}} (Αναμονή για βίντεο...)",
"{{name}} (Connecting...)": "{{name}} (Συνδέεται...)",
"{{displayName}}, your call is now ended": "{{displayName}}, η κλήση σας τερματίστηκε",
"{{count}} people connected|other": "{{count}} άτομα συνδεδεμένα",
"{{count}} people connected|one": "{{count}} άτομο συνδεδεμένο"
}

View File

@@ -1,9 +1,7 @@
{
"{{count}} people connected|one": "{{count}} person connected",
"{{count}} people connected|other": "{{count}} people connected",
"{{count}} stars|one": "{{count}} star",
"{{count}} stars|other": "{{count}} stars",
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
"{{name}} (Connecting...)": "{{name}} (Connecting...)",
"{{name}} (Waiting for video...)": "{{name}} (Waiting for video...)",
"{{name}} is presenting": "{{name}} is presenting",
@@ -16,8 +14,6 @@
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>",
"<0>Oops, something's gone wrong.</0>": "<0>Oops, something's gone wrong.</0>",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Submitting debug logs will help us track down the problem.</0>",
"<0>Thanks for your feedback!</0>": "<0>Thanks for your feedback!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>We'd love to hear your feedback so we can improve your experience.</0>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
"Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.",
"Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.",
@@ -36,13 +32,13 @@
"Close": "Close",
"Confirm password": "Confirm password",
"Connection lost": "Connection lost",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Copied!": "Copied!",
"Copy": "Copy",
"Copy and share this call link": "Copy and share this call link",
"Create account": "Create account",
"Debug log": "Debug log",
"Debug log request": "Debug log request",
"Description (optional)": "Description (optional)",
"Details": "Details",
"Developer": "Developer",
"Developer Settings": "Developer Settings",
@@ -51,15 +47,13 @@
"Element Call Home": "Element Call Home",
"Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback",
"Fetching group call timed out.": "Fetching group call timed out.",
"Freedom": "Freedom",
"Full screen": "Full screen",
"Go": "Go",
"Grid layout menu": "Grid layout menu",
"Having trouble? Help us fix it.": "Having trouble? Help us fix it.",
"Home": "Home",
"How did it go?": "How did it go?",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
"Include debug logs": "Include debug logs",
"Incompatible versions": "Incompatible versions",
"Incompatible versions!": "Incompatible versions!",
@@ -70,6 +64,7 @@
"Join call now": "Join call now",
"Join existing call?": "Join existing call?",
"Leave": "Leave",
"Loading room…": "Loading room…",
"Loading…": "Loading…",
"Local volume": "Local volume",
"Logging in…": "Logging in…",
@@ -79,6 +74,7 @@
"Microphone {{n}}": "Microphone {{n}}",
"Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.",
"More": "More",
"More menu": "More menu",
"Mute microphone": "Mute microphone",
"No": "No",
"Not now, return to home screen": "Not now, return to home screen",
@@ -99,6 +95,8 @@
"Release to stop": "Release to stop",
"Remove": "Remove",
"Return to home screen": "Return to home screen",
"Save": "Save",
"Saving…": "Saving…",
"Select an option": "Select an option",
"Send debug logs": "Send debug logs",
"Sending debug logs…": "Sending debug logs…",
@@ -113,13 +111,11 @@
"Speaker {{n}}": "Speaker {{n}}",
"Spotlight": "Spotlight",
"Stop sharing screen": "Stop sharing screen",
"Submit": "Submit",
"Submit feedback": "Submit feedback",
"Submitting…": "Submitting…",
"Submitting feedback…": "Submitting feedback…",
"Take me Home": "Take me Home",
"Talk over speaker": "Talk over speaker",
"Talking…": "Talking…",
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.",
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
"This feature is only supported on Firefox.": "This feature is only supported on Firefox.",
@@ -129,6 +125,7 @@
"Turn on camera": "Turn on camera",
"Unmute microphone": "Unmute microphone",
"Use the upcoming grid system": "Use the upcoming grid system",
"User ID": "User ID",
"User menu": "User menu",
"Username": "Username",
"Version: {{version}}": "Version: {{version}}",
@@ -142,6 +139,5 @@
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
"Yes, join call": "Yes, join call",
"You can't talk at the same time": "You can't talk at the same time",
"Your feedback": "Your feedback",
"Your recent calls": "Your recent calls"
}

View File

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

View File

@@ -10,6 +10,7 @@
"{{name}} is talking…": "{{nimi}} räägib…",
"{{name}} is presenting": "{{nimi}} esitab",
"{{name}} (Connecting...)": "{{nimi}} (ühendamisel...)",
"{{displayName}}, your call is now ended": "{{displayName}}, sinu kõne on nüüd lõppenud",
"{{count}} people connected|other": "{{count}} osalejat liitunud",
"{{count}} people connected|one": "{{count}} osaleja liitunud",
"Invite people": "Kutsu inimesi",
@@ -19,6 +20,7 @@
"Incompatible versions": "Ühildumatud versioonid",
"Include debug logs": "Lisa veatuvastuslogid",
"Home": "Avavaatesse",
"Having trouble? Help us fix it.": "Kas on probleeme? Aita meil asja parandada.",
"Grid layout menu": "Ruudustikvaate menüü",
"Go": "Jätka",
"Full screen": "Täisekraan",
@@ -29,6 +31,7 @@
"Display name": "Kuvatav nimi",
"Developer": "Arendaja",
"Details": "Täpsemalt",
"Description (optional)": "Kirjeldus (valikuline)",
"Debug log request": "Veaotsingulogi päring",
"Debug log": "Veaotsingulogi",
"Create account": "Loo konto",
@@ -57,6 +60,7 @@
"Mute microphone": "Summuta mikrofon",
"Your recent calls": "Hiljutised kõned",
"You can't talk at the same time": "Üheaegselt ei saa rääkida",
"More menu": "Rohkem valikuid",
"More": "Rohkem",
"Microphone permissions needed to join the call.": "Kõnega liitumiseks on vaja lubada mikrofoni kasutamine.",
"Microphone {{n}}": "Mikrofon {{n}}",
@@ -66,12 +70,15 @@
"Logging in…": "Sisselogimine …",
"Local volume": "Kohalik helitugevus",
"Loading…": "Laadimine …",
"Loading room…": "Ruumi laadimine …",
"Leave": "Lahku",
"Join existing call?": "Liitu juba käimasoleva kõnega?",
"Join call now": "Kõnega liitumine",
"Join call": "Kõnega liitumine",
"User ID": "Kasutajatunnus",
"Turn on camera": "Lülita kaamera sisse",
"Turn off camera": "Lülita kaamera välja",
"Submitting feedback…": "Tagasiside saatmine…",
"Take me Home": "Mine avalehele",
"Submit feedback": "Jaga tagasisidet",
"Stop sharing screen": "Lõpeta ekraani jagamine",
@@ -88,6 +95,8 @@
"Sending debug logs…": "Veaotsingulogide saatmine…",
"Send debug logs": "Saada veaotsingulogid",
"Select an option": "Vali oma eelistus",
"Saving…": "Salvestamine…",
"Save": "Salvesta",
"Return to home screen": "Tagasi avalehele",
"Remove": "Eemalda",
"Release to stop": "Peatamiseks vabasta klahv",
@@ -130,17 +139,5 @@
"Expose developer settings in the settings window.": "Näita seadistuste aknas arendajale vajalikke seadeid.",
"Developer Settings": "Arendaja seadistused",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Nõustudes selle beetaversiooni kasutamisega sa nõustud ka toote arendamiseks kasutatavate anonüümsete andmete kogumisega. Täpsemat teavet kogutavate andmete kohta leiad meie <2>Privaatsuspoliitikast</2> ja meie <5>Küpsiste kasutamise reeglitest</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.",
"Your feedback": "Sinu tagasiside",
"Thanks, we received your feedback!": "Tänud, me oleme sinu tagasiside kätte saanud!",
"Submitting…": "Saadan…",
"Submit": "Saada",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.",
"Feedback": "Tagasiside",
"{{count}} stars|one": "{{count}} tärn",
"{{count}} stars|other": "{{count}} tärni",
"How did it go?": "Kuidas sujus?",
"{{displayName}}, your call has ended.": "{{displayName}}, sinu kõne on lõppenud.",
"<0>Thanks for your feedback!</0>": "<0>Täname Sind tagasiside eest!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Meie rakenduse paremaks muutmiseks me hea meelega ootame Sinu arvamusi.</0>"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu."
}

View File

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

View File

@@ -22,6 +22,7 @@
"Create account": "Créer un compte",
"Debug log": "Journal de débogage",
"Debug log request": "Demande dun journal de débogage",
"Description (optional)": "Description (facultatif)",
"Details": "Informations",
"Developer": "Développeur",
"Display name": "Nom daffichage",
@@ -31,6 +32,7 @@
"Full screen": "Plein écran",
"Go": "Commencer",
"Grid layout menu": "Menu en grille",
"Having trouble? Help us fix it.": "Un problème ? Aidez nous à le résoudre.",
"Home": "Accueil",
"Include debug logs": "Inclure les journaux de débogage",
"Incompatible versions": "Versions incompatibles",
@@ -41,6 +43,7 @@
"Join call now": "Rejoindre lappel maintenant",
"Join existing call?": "Rejoindre un appel existant ?",
"Leave": "Partir",
"Loading room…": "Chargement du salon…",
"Loading…": "Chargement…",
"Local volume": "Volume local",
"Logging in…": "Connexion…",
@@ -50,6 +53,7 @@
"Microphone permissions needed to join the call.": "Accès au microphone requis pour rejoindre lappel.",
"Microphone {{n}}": "Microphone {{n}}",
"More": "Plus",
"More menu": "Menu plus",
"Mute microphone": "Couper le micro",
"No": "Non",
"Not now, return to home screen": "Pas maintenant, retourner à laccueil",
@@ -70,6 +74,8 @@
"Release to stop": "Relâcher pour arrêter",
"Remove": "Supprimer",
"Return to home screen": "Retour à laccueil",
"Save": "Enregistrer",
"Saving…": "Enregistrement…",
"Select an option": "Sélectionnez une option",
"Send debug logs": "Envoyer les journaux de débogage",
"Sending…": "Envoi…",
@@ -82,6 +88,7 @@
"Spotlight": "Premier plan",
"Stop sharing screen": "Arrêter le partage décran",
"Submit feedback": "Envoyer des retours",
"Submitting feedback…": "Envoi des retours…",
"Take me Home": "Retouner à laccueil",
"Talk over speaker": "Parler par dessus lintervenant",
"Thanks! We'll get right on it.": "Merci ! Nous allons nous y attaquer.",
@@ -91,6 +98,7 @@
"{{roomName}} - Walkie-talkie call": "{{roomName}} — Appel talkie-walkie",
"{{name}} is talking…": "{{name}} est en train de parler…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{displayName}}, your call is now ended": "{{displayName}}, votre appel est désormais terminé",
"{{count}} people connected|other": "{{count}} personnes connectées",
"{{count}} people connected|one": "{{count}} personne connectée",
"Your recent calls": "Appels récents",
@@ -107,6 +115,7 @@
"Version: {{version}}": "Version : {{version}}",
"Username": "Nom dutilisateur",
"User menu": "Menu utilisateur",
"User ID": "Identifiant utilisateur",
"Unmute microphone": "Allumer le micro",
"Turn on camera": "Allumer la caméra",
"Turn off camera": "Couper la caméra",
@@ -130,17 +139,5 @@
"Expose developer settings in the settings window.": "Affiche les paramètres développeurs dans la fenêtre des paramètres.",
"Developer Settings": "Paramètres développeurs",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "En participant à cette beta, vous consentez à la collecte de données anonymes, qui seront utilisées pour améliorer le produit. Vous trouverez plus dinformations sur les données collectées dans notre <2>Politique de vie privée</2> et notre <5>Politique de cookies</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de lappel.",
"Your feedback": "Votre commentaire",
"Thanks, we received your feedback!": "Merci, nous avons reçu vos commentaires !",
"Submitting…": "Envoi…",
"Submit": "Envoyer",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, veuillez nous envoyer une courte description ci-dessous.",
"Feedback": "Commentaires",
"{{count}} stars|other": "{{count}} favoris",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Nous aimerions avoir vos commentaires afin que nous puissions améliorer votre expérience.</0>",
"{{count}} stars|one": "{{count}} favori",
"{{displayName}}, your call has ended.": "{{displayName}}, votre appel est terminé.",
"<0>Thanks for your feedback!</0>": "<0>Merci pour votre commentaire !</0>",
"How did it go?": "Comment cela sest-il passé ?"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de lappel."
}

View File

@@ -23,6 +23,7 @@
"Create account": "Buat akun",
"Debug log": "Catatan pengawakutuan",
"Debug log request": "Permintaan catatan pengawakutuan",
"Description (optional)": "Deskripsi (opsional)",
"Details": "Detail",
"Developer": "Pengembang",
"Display name": "Nama tampilan",
@@ -33,6 +34,7 @@
"Full screen": "Layar penuh",
"Go": "Bergabung",
"Grid layout menu": "Menu tata letak kisi",
"Having trouble? Help us fix it.": "Mengalami masalah? Bantu kami memperbaikinya.",
"Home": "Beranda",
"Include debug logs": "Termasuk catatan pengawakutuan",
"Incompatible versions": "Versi tidak kompatibel",
@@ -44,6 +46,7 @@
"Join call now": "Bergabung ke panggilan sekarang",
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
"Leave": "Keluar",
"Loading room…": "Memuat ruangan…",
"Loading…": "Memuat…",
"Local volume": "Volume lokal",
"Logging in…": "Memasuki…",
@@ -53,6 +56,7 @@
"Microphone permissions needed to join the call.": "Izin mikrofon dibutuhkan untuk bergabung ke panggilan ini.",
"Microphone {{n}}": "Mikrofon {{n}}",
"More": "Lainnya",
"More menu": "Menu lainnya",
"Mute microphone": "Bisukan mikrofon",
"No": "Tidak",
"Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda",
@@ -73,6 +77,8 @@
"Release to stop": "Lepaskan untuk berhenti",
"Remove": "Hapus",
"Return to home screen": "Kembali ke layar beranda",
"Save": "Simpan",
"Saving…": "Menyimpan…",
"Select an option": "Pilih sebuah opsi",
"Send debug logs": "Kirim catatan pengawakutuan",
"Sending…": "Mengirimkan…",
@@ -87,6 +93,7 @@
"Spotlight": "Sorotan",
"Stop sharing screen": "Berhenti membagikan layar",
"Submit feedback": "Kirim masukan",
"Submitting feedback…": "Mengirimkan masukan…",
"Take me Home": "Bawa saya ke Beranda",
"Talk over speaker": "Bicara pada pembicara",
"Talking…": "Berbicara…",
@@ -97,6 +104,7 @@
"Turn off camera": "Matikan kamera",
"Turn on camera": "Nyalakan kamera",
"Unmute microphone": "Suarakan mikrofon",
"User ID": "ID pengguna",
"User menu": "Menu pengguna",
"Username": "Nama pengguna",
"Version: {{version}}": "Versi: {{version}}",
@@ -113,6 +121,7 @@
"Your recent calls": "Panggilan Anda terkini",
"{{count}} people connected|one": "{{count}} orang terhubung",
"{{count}} people connected|other": "{{count}} orang terhubung",
"{{displayName}}, your call is now ended": "{{displayName}}, panggilan Anda sekarang telah berakhir",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is presenting": "{{name}} sedang mempresentasi",
"{{name}} is talking…": "{{name}} sedang berbicara…",
@@ -130,16 +139,5 @@
"Expose developer settings in the settings window.": "Ekspos pengaturan pengembang dalam jendela pengaturan.",
"Developer Settings": "Pengaturan Pengembang",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Dengan bergabung dalam beta ini, Anda mengizinkan kami untuk mengumpulkan data anonim, yang kami gunakan untuk meningkatkan produk ini. Anda dapat mempelajari lebih lanjut tentang data apa yang kami lacak dalam <2>Kebijakan Privasi</2> dan <5>Kebijakan Kuki</5> kami.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.",
"Feedback": "Masukan",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
"Submit": "Kirim",
"Submitting…": "Mengirim",
"Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!",
"Your feedback": "Masukan Anda",
"{{displayName}}, your call has ended.": "{{displayName}}, panggilan Anda telah berakhir.",
"<0>Thanks for your feedback!</0>": "<0>Terima kasih atas masukan Anda!</0>",
"How did it go?": "Bagaimana rasanya?",
"{{count}} stars|one": "{{count}} bintang",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Kami ingin mendengar masukan Anda supaya kami bisa meningkatkan pengalaman Anda.</0>"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan."
}

View File

@@ -24,8 +24,10 @@
"Copied!": "コピーしました!",
"Copy and share this call link": "通話リンクをコピーし共有",
"Copy": "コピー",
"Description (optional)": "概要(任意)",
"Debug log": "デバッグログ",
"Create account": "アカウントを作成",
"Having trouble? Help us fix it.": "問題が起きましたか?修正にご協力ください。",
"Go": "続行",
"Fetching group call timed out.": "グループ通話の取得がタイムアウトしました。",
"Element Call Home": "Element Call ホーム",
@@ -51,13 +53,16 @@
"Login": "ログイン",
"Logging in…": "ログインしています…",
"Loading…": "読み込んでいます…",
"Loading room…": "ルームを読み込んでいます…",
"Leave": "退出",
"Version: {{version}}": "バージョン:{{version}}",
"Username": "ユーザー名",
"User menu": "ユーザーメニュー",
"User ID": "ユーザーID",
"Unmute microphone": "マイクのミュートを解除",
"Turn on camera": "カメラをつける",
"Turn off camera": "カメラを切る",
"Submitting feedback…": "フィードバックを送信しています…",
"Submit feedback": "フィードバックを送信",
"Stop sharing screen": "画面共有を停止",
"Spotlight": "スポットライト",
@@ -68,6 +73,8 @@
"Settings": "設定",
"Sending…": "送信しています…",
"Sending debug logs…": "デバッグログを送信しています…",
"Saving…": "保存しています…",
"Save": "保存",
"Return to home screen": "ホーム画面に戻る",
"Registering…": "登録しています…",
"Register": "登録",
@@ -94,6 +101,7 @@
"WebRTC is not supported or is being blocked in this browser.": "お使いのブラウザでWebRTCがサポートされていないか、またはブロックされています。",
"Login to your account": "アカウントにログイン",
"Freedom": "自由",
"{{displayName}}, your call is now ended": "{{displayName}}、通話が終了しました",
"Talking…": "話しています…",
"Remove": "削除",
"No": "いいえ",

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
{
"Register": "Зарегистрироваться",
"Saving…": "Сохранение…",
"Registering…": "Регистрация…",
"Logging in…": "Вход…",
"{{names}}, {{name}}": "{{names}}, {{name}}",
@@ -9,6 +10,7 @@
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
"Talking…": "Говорите…",
"Submitting feedback…": "Отправка отзыва…",
"Submit feedback": "Отправить отзыв",
"Sending debug logs…": "Отправка журнала отладки…",
"Select an option": "Выберите вариант",
@@ -36,6 +38,7 @@
"Version: {{version}}": "Версия: {{version}}",
"Username": "Имя пользователя",
"User menu": "Меню пользователя",
"User ID": "ID пользователя",
"Unmute microphone": "Включить микрофон",
"Turn on camera": "Включить камеру",
"Turn off camera": "Отключить камеру",
@@ -54,6 +57,7 @@
"Sending…": "Отправка…",
"Local volume": "Местная громкость",
"Call type menu": "Меню \"Тип звонка\"",
"More menu": "Полное меню",
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Звонок-рация",
"Include debug logs": "Приложить журнал отладки",
"Download debug logs": "Скачать журнал отладки",
@@ -61,6 +65,7 @@
"Debug log": "Журнал отладки",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.",
"Send debug logs": "Отправить журнал отладки",
"Save": "Сохранить",
"Return to home screen": "Вернуться в Начало",
"Remove": "Удалить",
"Recaptcha not loaded": "Невозможно начать проверку",
@@ -81,6 +86,7 @@
"Login to your account": "Войдите в свой аккаунт",
"Login": "Вход",
"Loading…": "Загрузка…",
"Loading room…": "Загрузка комнаты…",
"Leave": "Покинуть",
"Join existing call?": "Присоединиться к существующему звонку?",
"Join call now": "Присоединиться сейчас",
@@ -91,6 +97,7 @@
"Incompatible versions!": "Несовместимые версии!",
"Incompatible versions": "Несовместимые версии",
"Home": "Начало",
"Having trouble? Help us fix it.": "Есть проблема? Помогите нам её устранить.",
"Go": "Далее",
"Full screen": "Полноэкранный режим",
"Freedom": "Свобода",
@@ -99,6 +106,7 @@
"Display name": "Видимое имя",
"Developer": "Разработчику",
"Details": "Подробности",
"Description (optional)": "Описание (необязательно)",
"Create account": "Создать аккаунт",
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
"Copied!": "Скопировано!",
@@ -116,6 +124,7 @@
"Accept camera/microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к камере/микрофону.",
"{{name}} is talking…": "{{name}} говорит…",
"{{name}} is presenting": "{{name}} показывает",
"{{displayName}}, your call is now ended": "{{displayName}}, ваш звонок завершён",
"{{count}} people connected|other": "{{count}} подключилось",
"{{count}} people connected|one": "{{count}} подключился",
"Element Call Home": "Главная Element Call",

View File

@@ -12,6 +12,7 @@
"Talking…": "Rozprávanie…",
"Talk over speaker": "Hovor cez reproduktor",
"Take me Home": "Zober ma domov",
"Submitting feedback…": "Odosielanie spätnej väzby…",
"Submit feedback": "Odoslať spätnú väzbu",
"Stop sharing screen": "Zastaviť zdieľanie obrazovky",
"Show call inspector": "Zobraziť inšpektora hovorov",
@@ -20,6 +21,8 @@
"Sending debug logs…": "Odosielanie záznamov o ladení…",
"Send debug logs": "Odoslať záznamy o ladení",
"Select an option": "Vyberte možnosť",
"Saving…": "Ukladanie…",
"Save": "Uložiť",
"Return to home screen": "Návrat na domovskú obrazovku",
"Remove": "Odstrániť",
"Release spacebar key to stop": "Pustite medzerník pre ukončenie",
@@ -40,6 +43,7 @@
"Not now, return to home screen": "Teraz nie, vrátiť sa na domovskú obrazovku",
"No": "Nie",
"Mute microphone": "Stlmiť mikrofón",
"More menu": "Ponuka viac",
"More": "Viac",
"Microphone permissions needed to join the call.": "Povolenie mikrofónu je potrebné na pripojenie k hovoru.",
"Microphone {{n}}": "Mikrofón {{n}}",
@@ -48,6 +52,7 @@
"Login": "Prihlásiť sa",
"Logging in…": "Prihlasovanie…",
"Loading…": "Načítanie…",
"Loading room…": "Načítanie miestnosti…",
"Leave": "Opustiť",
"Join existing call?": "Pripojiť sa k existujúcemu hovoru?",
"Join call now": "Pripojiť sa k hovoru teraz",
@@ -58,6 +63,7 @@
"Incompatible versions!": "Nekompatibilné verzie!",
"Incompatible versions": "Nekompatibilné verzie",
"Home": "Domov",
"Having trouble? Help us fix it.": "Máte problém? Pomôžte nám ho opraviť.",
"Grid layout menu": "Ponuka rozloženia mriežky",
"Go": "Prejsť",
"Full screen": "Zobrazenie na celú obrazovku",
@@ -75,6 +81,7 @@
"Version: {{version}}": "Verzia: {{version}}",
"Username": "Meno používateľa",
"User menu": "Používateľské menu",
"User ID": "ID používateľa",
"Unmute microphone": "Zrušiť stlmenie mikrofónu",
"Turn on camera": "Zapnúť kameru",
"Turn off camera": "Vypnúť kameru",
@@ -89,6 +96,7 @@
"Display name": "Zobrazované meno",
"Developer": "Vývojár",
"Details": "Podrobnosti",
"Description (optional)": "Popis (voliteľné)",
"Debug log request": "Žiadosť o záznam ladenia",
"Debug log": "Záznam o ladení",
"Create account": "Vytvoriť účet",
@@ -121,6 +129,7 @@
"{{name}} is presenting": "{{name}} prezentuje",
"{{name}} (Waiting for video...)": "{{name}} (Čaká sa na video...)",
"{{name}} (Connecting...)": "{{name}} (Pripájanie...)",
"{{displayName}}, your call is now ended": "{{displayName}}, váš hovor je teraz ukončený",
"{{count}} people connected|other": "{{count}} osôb pripojených",
"{{count}} people connected|one": "{{count}} osoba pripojená",
"This feature is only supported on Firefox.": "Táto funkcia je podporovaná len v prehliadači Firefox.",
@@ -130,17 +139,5 @@
"Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
"Developer Settings": "Nastavenia pre vývojárov",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Účasťou v tejto beta verzii súhlasíte so zhromažďovaním anonymných údajov, ktoré použijeme na zlepšenie produktu. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich <2>Zásadách ochrany osobných údajov</2> a <5>Zásadách používania súborov cookie</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.",
"Your feedback": "Vaša spätná väzba",
"Thanks, we received your feedback!": "Ďakujeme, dostali sme vašu spätnú väzbu!",
"Submitting…": "Odosielanie…",
"Submit": "Odoslať",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.",
"Feedback": "Spätná väzba",
"{{count}} stars|one": "{{count}} hviezdička",
"How did it go?": "Ako to išlo?",
"{{count}} stars|other": "{{count}} hviezdičiek",
"{{displayName}}, your call has ended.": "{{displayName}}, váš hovor skončil.",
"<0>Thanks for your feedback!</0>": "<0> Ďakujeme za vašu spätnú väzbu!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0> Radi si vypočujeme vašu spätnú väzbu, aby sme mohli zlepšiť vaše skúsenosti.</0>"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru."
}

View File

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

View File

@@ -14,6 +14,7 @@
"Version: {{version}}": "Версія: {{version}}",
"Username": "Ім'я користувача",
"User menu": "Меню користувача",
"User ID": "ID користувача",
"Unmute microphone": "Увімкнути мікрофон",
"Turn on camera": "Увімкнути камеру",
"Turn off camera": "Вимкнути камеру",
@@ -24,6 +25,7 @@
"Talking…": "Говоріть…",
"Talk over speaker": "Говорити через динамік",
"Take me Home": "Перейти до Домівки",
"Submitting feedback…": "Надсилання відгуку…",
"Submit feedback": "Надіслати відгук",
"Stop sharing screen": "Припинити показ екрана",
"Spotlight": "У центрі уваги",
@@ -39,6 +41,8 @@
"Sending debug logs…": "Надсилання журналу налагодження…",
"Send debug logs": "Надіслати журнал налагодження",
"Select an option": "Вибрати опцію",
"Saving…": "Збереження…",
"Save": "Зберегти",
"Return to home screen": "Повернутися на екран домівки",
"Remove": "Вилучити",
"Release to stop": "Відпустіть, щоб закінчити",
@@ -59,6 +63,7 @@
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
"No": "Ні",
"Mute microphone": "Заглушити мікрофон",
"More menu": "Усе меню",
"More": "Докладніше",
"Microphone permissions needed to join the call.": "Для участі у виклику необхідний дозвіл на користування мікрофоном.",
"Microphone {{n}}": "Мікрофон {{n}}",
@@ -67,6 +72,7 @@
"Login": "Увійти",
"Logging in…": "Вхід…",
"Local volume": "Локальна гучність",
"Loading room…": "Завантаження кімнати…",
"Leave": "Вийти",
"Join existing call?": "Приєднатися до наявного виклику?",
"Join call now": "Приєднатися до виклику зараз",
@@ -78,6 +84,7 @@
"Incompatible versions": "Несумісні версії",
"Include debug logs": "Долучити журнали налагодження",
"Home": "Домівка",
"Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.",
"Grid layout menu": "Меню у вигляді сітки",
"Go": "Далі",
"Full screen": "Повноекранний режим",
@@ -88,6 +95,7 @@
"Display name": "Показуване ім'я",
"Developer": "Розробнику",
"Details": "Подробиці",
"Description (optional)": "Опис (необов'язково)",
"Debug log request": "Запит журналу налагодження",
"Debug log": "Журнал налагодження",
"Create account": "Створити обліковий запис",
@@ -116,6 +124,7 @@
"{{names}}, {{name}}": "{{names}}, {{name}}",
"{{name}} is talking…": "{{name}} балакає…",
"{{name}} is presenting": "{{name}} показує",
"{{displayName}}, your call is now ended": "{{displayName}}, ваш виклик завершено",
"{{count}} people connected|other": "{{count}} під'єдналися",
"{{count}} people connected|one": "{{count}} під'єднується",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
@@ -130,17 +139,5 @@
"Expose developer settings in the settings window.": "Відкрийте налаштування розробника у вікні налаштувань.",
"Developer Settings": "Налаштування розробника",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Користуючись дочасним доступом, ви даєте згоду на збір анонімних даних, які ми використовуємо для вдосконалення продукту. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашій <2>Політиці Приватності</2> і нашій <5>Політиці про куки</5>.",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.",
"Your feedback": "Ваш відгук",
"Thanks, we received your feedback!": "Дякуємо, ми отримали ваш відгук!",
"Submitting…": "Надсилання…",
"Submit": "Надіслати",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
"Feedback": "Відгук",
"<0>Thanks for your feedback!</0>": "<0>Дякуємо за ваш відгук!</0>",
"{{count}} stars|one": "{{count}} зірка",
"{{count}} stars|other": "{{count}} зірок",
"{{displayName}}, your call has ended.": "{{displayName}}, ваш виклик завершено.",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.</0>",
"How did it go?": "Вам усе сподобалось?"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику."
}

View File

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

View File

@@ -13,6 +13,7 @@
"Version: {{version}}": "版本:{{version}}",
"Username": "用户名",
"User menu": "用户菜单",
"User ID": "用户ID",
"Unmute microphone": "取消麦克风静音",
"Turn on camera": "开启摄像头",
"Turn off camera": "关闭摄像头",
@@ -23,6 +24,7 @@
"Talking…": "正在发言……",
"Talk over speaker": "通过扬声器发言",
"Take me Home": "返回主页",
"Submitting feedback…": "正在提交反馈……",
"Submit feedback": "提交反馈",
"Stop sharing screen": "停止屏幕共享",
"Spotlight": "聚焦模式",
@@ -45,6 +47,7 @@
"{{name}} is presenting": "{{name}}正在展示",
"{{name}} (Waiting for video...)": "{{name}}(等待视频……)",
"{{name}} (Connecting...)": "{{name}} (正在连接……)",
"{{displayName}}, your call is now ended": "{{displayName}},您的通话已结束",
"{{count}} people connected|other": "{{count}}人已连接",
"{{count}} people connected|one": "{{count}}人已连接",
"Inspector": "检查器",
@@ -55,6 +58,8 @@
"Sending debug logs…": "正在发送调试日志……",
"Send debug logs": "发送调试日志",
"Select an option": "选择一个选项",
"Saving…": "正在保存……",
"Save": "保存",
"Return to home screen": "返回主页",
"Remove": "移除",
"Release to stop": "松开后停止",
@@ -75,6 +80,7 @@
"Not now, return to home screen": "暂不,先返回主页",
"No": "否",
"Mute microphone": "麦克风静音",
"More menu": "更多",
"More": "更多",
"Microphone permissions needed to join the call.": "加入通话需要麦克风权限。",
"Microphone {{n}}": "麦克风 {{n}}",
@@ -84,6 +90,7 @@
"Logging in…": "登录中……",
"Local volume": "本地音量",
"Loading…": "加载中……",
"Loading room…": "加载房间中……",
"Leave": "离开",
"Join existing call?": "加入现有的通话?",
"Join call now": "现在加入通话",
@@ -94,6 +101,7 @@
"Incompatible versions": "不兼容版本",
"Include debug logs": "包含调试日志",
"Home": "主页",
"Having trouble? Help us fix it.": "遇到麻烦?帮助我们解决问题。",
"Grid layout menu": "网格布局菜单",
"Go": "开始",
"Full screen": "全屏",
@@ -105,6 +113,7 @@
"Display name": "显示名称",
"Developer": "开发者",
"Details": "详情",
"Description (optional)": "描述(可选)",
"Debug log request": "调试日志请求",
"Debug log": "调试日志",
"Create account": "创建账户",
@@ -122,7 +131,5 @@
"Call type menu": "通话类型菜单",
"Call link copied": "链接已复制",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "点击“现在加入”则表示同意我们的<2>条款与条件<2>",
"Avatar": "头像",
"<0>Oops, something's gone wrong.</0>": "<0>哎哟,出问题了。</0>",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": ""
"Avatar": "头像"
}

View File

@@ -8,6 +8,7 @@
"{{name}} is presenting": "{{name}} 已上線",
"{{name}} (Waiting for video...)": "{{name}} (等候視訊中...)",
"{{name}} (Connecting...)": "{{name}} (連結中...)",
"{{displayName}}, your call is now ended": "{{displayName}},您的通話已結束",
"{{count}} people connected|other": "{{count}} 人已連結",
"{{count}} people connected|one": "{{count}} 人已連結",
"Use the upcoming grid system": "使用即將推出的網格系統",
@@ -29,6 +30,7 @@
"Version: {{version}}": "版本: {{version}}",
"Username": "使用者名稱",
"User menu": "使用者選單",
"User ID": "使用者 ID",
"Unmute microphone": "取消麥克風靜音",
"Turn on camera": "開啟相機",
"Turn off camera": "關閉相機",
@@ -40,6 +42,7 @@
"Talking…": "對話中…",
"Talk over speaker": "以擴音對話",
"Take me Home": "帶我回主畫面",
"Submitting feedback…": "遞交回饋…",
"Submit feedback": "遞交回覆",
"Stop sharing screen": "停止分享螢幕畫面",
"Spotlight": "聚焦",
@@ -55,6 +58,8 @@
"Sending debug logs…": "傳送除錯記錄檔中…",
"Send debug logs": "傳送除錯紀錄",
"Select an option": "選擇一個選項",
"Saving…": "儲存中…",
"Save": "儲存",
"Return to home screen": "回到首頁",
"Remove": "移除",
"Release to stop": "放開以停止",
@@ -75,6 +80,7 @@
"Not now, return to home screen": "現在不行,回到首頁",
"No": "否",
"Mute microphone": "麥克風靜音",
"More menu": "更多選單",
"More": "更多",
"Microphone permissions needed to join the call.": "加入通話前需要取得麥克風的權限。",
"Microphone {{n}}": "麥克風 {{n}}",
@@ -84,6 +90,7 @@
"Logging in…": "登入中…",
"Local volume": "您的音量",
"Loading…": "載入中…",
"Loading room…": "載入聊天室…",
"Leave": "離開",
"Join existing call?": "加入已開始的通話嗎?",
"Join call now": "現在加入通話",
@@ -95,6 +102,7 @@
"Incompatible versions": "不相容版本",
"Include debug logs": "包含除錯紀錄",
"Home": "首頁",
"Having trouble? Help us fix it.": "遇到問題嗎?請讓我們協助您。",
"Grid layout menu": "格框式清單",
"Go": "前往",
"Full screen": "全螢幕",
@@ -106,6 +114,7 @@
"Display name": "顯示名稱",
"Developer": "開發者",
"Details": "詳細說明",
"Description (optional)": "描述(選擇性)",
"Debug log request": "請求偵錯報告",
"Debug log": "除錯紀錄",
"Create account": "建立帳號",
@@ -130,17 +139,5 @@
"Accept camera/microphone permissions to join the call.": "請授權使用您的相機/麥克風以加入對話。",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>何不設定密碼以保留此帳號?</0><1>您可以保留暱稱並設定頭像,以便未來通話時使用</1>",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "參與此測試版即表示您同意蒐集匿名資料,我們使用這些資料來改進產品。您可以在我們的<2>隱私政策</2>與我們的 <5>Cookie 政策</5> 中找到關於我們追蹤哪些資料的更多資訊。",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。",
"Your feedback": "您的回饋",
"Thanks, we received your feedback!": "感謝,我們已經收到您的回饋了!",
"Submitting…": "正在遞交……",
"Submit": "遞交",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。",
"Feedback": "回饋",
"{{count}} stars|other": "{{count}} 個星星",
"<0>Thanks for your feedback!</0>": "<0>感謝您的回饋!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>我們想要聽到您的回饋,如此我們才能改善您的體驗。</0>",
"{{count}} stars|one": "{{count}} 個星星",
"{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。",
"How did it go?": "進展如何?"
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。"
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2021 - 2023 New Vector Ltd
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,9 +29,7 @@ import { usePageFocusStyle } from "./usePageFocusStyle";
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
import { InspectorContextProvider } from "./room/GroupCallInspector";
import { CrashView, LoadingView } from "./FullScreenView";
import { DisconnectedBanner } from "./DisconnectedBanner";
import { Initializer } from "./initializer";
import { MediaHandlerProvider } from "./settings/useMediaHandler";
const SentryRoute = Sentry.withSentryRouting(Route);
@@ -57,35 +55,32 @@ export default function App({ history }: AppProps) {
{loaded ? (
<Suspense fallback={null}>
<ClientProvider>
<MediaHandlerProvider>
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<DisconnectedBanner />
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
</SentryRoute>
</Switch>
</OverlayProvider>
</Sentry.ErrorBoundary>
</InspectorContextProvider>
</MediaHandlerProvider>
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
</SentryRoute>
</Switch>
</OverlayProvider>
</Sentry.ErrorBoundary>
</InspectorContextProvider>
</ClientProvider>
</Suspense>
) : (

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from "classnames";
import React, { HTMLAttributes, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css";
import { useClient } from "./ClientContext";
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> {
children?: ReactNode;
className?: string;
}
export function DisconnectedBanner({
children,
className,
...rest
}: DisconnectedBannerProps) {
const { t } = useTranslation();
const { disconnected } = useClient();
return (
<>
{disconnected && (
<div className={classNames(styles.banner, className)} {...rest}>
{children}
{t("Connectivity to the server has been lost.")}
</div>
)}
</>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
import React, { useCallback } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useClient } from "./ClientContext";
import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./settings/SettingsModal";
import { ProfileModal } from "./profile/ProfileModal";
import { UserMenu } from "./UserMenu";
interface Props {
@@ -35,17 +35,10 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
const onAction = useCallback(
async (value: string) => {
(value: string) => {
switch (value) {
case "user":
setDefaultSettingsTab("profile");
modalState.open();
break;
case "settings":
setDefaultSettingsTab("audio");
modalState.open();
break;
case "logout":
@@ -71,13 +64,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
displayName || (userName ? userName.replace("@", "") : undefined)
}
/>
{modalState.isOpen && (
<SettingsModal
client={client}
defaultTab={defaultSettingsTab}
{...modalProps}
/>
)}
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
</>
);
}

View File

@@ -29,7 +29,6 @@ import {
MuteCameraTracker,
MuteMicrophoneTracker,
UndecryptableToDeviceEventTracker,
QualitySurveyEventTracker,
} from "./PosthogEvents";
import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams";
@@ -70,7 +69,6 @@ export enum RegistrationType {
interface PlatformProperties {
appVersion: string;
matrixBackend: "embedded" | "jssdk";
callBackend: "livekit" | "full-mesh";
}
interface PosthogSettings {
@@ -192,7 +190,6 @@ export class PosthogAnalytics {
return {
appVersion,
matrixBackend: widget ? "embedded" : "jssdk",
callBackend: "full-mesh",
};
}
@@ -434,5 +431,4 @@ export class PosthogAnalytics {
public eventMuteMicrophone = new MuteMicrophoneTracker();
public eventMuteCamera = new MuteCameraTracker();
public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker();
public eventQualitySurvey = new QualitySurveyEventTracker();
}

View File

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

View File

@@ -124,13 +124,6 @@ export class PosthogSpanProcessor implements SpanProcessor {
const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`;
const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`;
const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`;
const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`;
const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`;
const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`;
const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`;
const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`;
const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`;
PosthogAnalytics.instance.trackEvent(
{
eventName: "MediaReceived",
@@ -140,12 +133,6 @@ export class PosthogSpanProcessor implements SpanProcessor {
videoReceived: videoReceived,
maxJitter: maxJitter,
maxPacketLoss: maxPacketLoss,
peerConnections: peerConnections,
percentageConcealedAudio: percentageConcealedAudio,
opponentUsersInCall: opponentUsersInCall,
opponentDevicesInCall: opponentDevicesInCall,
diffDevicesToPeerConnections: diffDevicesToPeerConnections,
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
},
// Send instantly because the window might be closing
{ send_instantly: true }

View File

@@ -1,38 +0,0 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Gets the index of the last element in the array to satsify the given
* predicate.
*/
// TODO: remove this once TypeScript recognizes the existence of
// Array.prototype.findLastIndex
export function findLastIndex<T>(
array: T[],
predicate: (item: T) => boolean
): number | null {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i;
}
return null;
}
/**
* Counts the number of elements in an array that satsify the given predicate.
*/
export const count = <T>(array: T[], predicate: (item: T) => boolean): number =>
array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0);

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -238,7 +238,7 @@ export function SettingsButton({
return (
<TooltipTrigger tooltip={tooltip}>
<Button variant="toolbar" {...rest}>
<SettingsIcon width={20} height={20} />
<SettingsIcon />
</Button>
</TooltipTrigger>
);
@@ -246,11 +246,9 @@ export function SettingsButton({
export function InviteButton({
className,
variant = "toolbar",
...rest
}: {
className?: string;
variant?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
@@ -259,7 +257,7 @@ export function InviteButton({
return (
<TooltipTrigger tooltip={tooltip}>
<Button variant={variant} {...rest}>
<Button variant="toolbar" {...rest}>
<AddUserIcon />
</Button>
</TooltipTrigger>

View File

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

View File

@@ -133,7 +133,6 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
type="text"
required
autoComplete="off"
data-testid="home_callName"
/>
<Button
@@ -141,7 +140,6 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
size="lg"
className={styles.button}
disabled={loading}
data-testid="home_go"
>
{loading ? t("Loading…") : t("Go")}
</Button>

View File

@@ -142,7 +142,6 @@ export const UnauthenticatedView: FC = () => {
type="text"
required
autoComplete="off"
data-testid="home_callName"
/>
</FieldRow>
<FieldRow>
@@ -153,7 +152,6 @@ export const UnauthenticatedView: FC = () => {
placeholder={t("Display name")}
type="text"
required
data-testid="home_displayName"
autoComplete="off"
/>
</FieldRow>
@@ -173,12 +171,7 @@ export const UnauthenticatedView: FC = () => {
<ErrorMessage error={error} />
</FieldRow>
)}
<Button
type="submit"
size="lg"
disabled={loading}
data-testid="home_go"
>
<Button type="submit" size="lg" disabled={loading}>
{loading ? t("Loading…") : t("Go")}
</Button>
<div id={recaptchaId} />
@@ -186,14 +179,14 @@ export const UnauthenticatedView: FC = () => {
</main>
<footer className={styles.footer}>
<Body className={styles.mobileLoginLink}>
<Link color="primary" to="/login" data-testid="home_login">
<Link color="primary" to="/login">
{t("Login to your account")}
</Link>
</Body>
<Body>
<Trans>
Not registered yet?{" "}
<Link color="primary" to="/register" data-testid="home_register">
<Link color="primary" to="/register">
Create an account
</Link>
</Trans>

View File

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

Before

Width:  |  Height:  |  Size: 290 B

View File

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

Before

Width:  |  Height:  |  Size: 620 B

View File

@@ -1,4 +1,4 @@
<svg data-testid="videoTile_muted" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.20333 0.963373C0.474437 0.690007 0.913989 0.690007 1.1851 0.963373L11.5983 11.4633C11.8694 11.7367 11.8694 12.1799 11.5983 12.4533C11.3272 12.7267 10.8876 12.7267 10.6165 12.4533L0.20333 1.95332C-0.0677768 1.67995 -0.0677768 1.23674 0.20333 0.963373Z" fill="white"/>
<path d="M0.418261 3.63429C0.226267 3.95219 0.115674 4.32557 0.115674 4.725V9.85832C0.115674 11.0181 1.0481 11.9583 2.19831 11.9583H8.65411L0.447396 3.66596C0.437225 3.65568 0.427513 3.64511 0.418261 3.63429Z" fill="white"/>
<path d="M9.95036 4.725V8.33212L4.30219 2.625H7.86772C9.01793 2.625 9.95036 3.5652 9.95036 4.725Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 922 B

After

Width:  |  Height:  |  Size: 892 B

View File

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

View File

@@ -45,12 +45,6 @@ class DependencyLoadStates {
export class Initializer {
private static internalInstance: Initializer;
private isInitialized = false;
public static isInitialized(): boolean {
return Initializer.internalInstance?.isInitialized;
}
public static initBeforeReact() {
// this maybe also needs to return a promise in the future,
// if we have to do async inits before showing the loading screen
@@ -229,7 +223,6 @@ export class Initializer {
if (this.loadStates.allDepsAreLoaded()) {
// resolve if there is no dependency that is not loaded
resolve();
this.isInitialized = true;
}
}
private initPromise: Promise<void> | null;

View File

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

View File

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

View File

@@ -72,7 +72,6 @@ interface InputFieldProps {
autoCorrect?: string;
autoCapitalize?: string;
value?: string;
defaultValue?: string;
placeholder?: string;
defaultChecked?: boolean;
onChange?: (event: ChangeEvent) => void;

View File

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

View File

@@ -1,41 +0,0 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.starIcon {
cursor: pointer;
}
.starRating {
display: flex;
justify-content: center;
flex: 1;
}
.inputContainer {
display: inline-block;
}
.hideElement {
border: 0;
clip-path: content-box;
height: 0px;
width: 0px;
margin: -1px;
overflow: hidden;
padding: 0;
width: 1px;
display: inline-block;
}

View File

@@ -1,85 +0,0 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import styles from "./StarRatingInput.module.css";
import { ReactComponent as StarSelected } from "../icons/StarSelected.svg";
import { ReactComponent as StarUnselected } from "../icons/StarUnselected.svg";
interface Props {
starCount: number;
onChange: (stars: number) => void;
required?: boolean;
}
export function StarRatingInput({
starCount,
onChange,
required,
}: Props): JSX.Element {
const [rating, setRating] = useState(0);
const [hover, setHover] = useState(0);
const { t } = useTranslation();
return (
<div className={styles.starRating}>
{[...Array(starCount)].map((_star, index) => {
index += 1;
return (
<div
className={styles.inputContainer}
onMouseEnter={() => setHover(index)}
onMouseLeave={() => setHover(rating)}
key={index}
>
<input
className={styles.hideElement}
type="radio"
id={"starInput" + String(index)}
value={String(index) + "Star"}
name="star rating"
onChange={(_ev) => {
setRating(index);
onChange(index);
}}
required
/>
<label
className={styles.hideElement}
id={"starInvisibleLabel" + String(index)}
htmlFor={"starInput" + String(index)}
>
{t("{{count}} stars", {
count: index,
})}
</label>
<label
className={styles.starIcon}
id={"starIcon" + String(index)}
htmlFor={"starInput" + String(index)}
>
{index <= (hover || rating) ? (
<StarSelected />
) : (
<StarUnselected />
)}
</label>
</div>
);
})}
</div>
);
}

View File

@@ -61,11 +61,11 @@ function waitForSync(client: MatrixClient) {
data: ISyncStateData
) => {
if (state === "PREPARED") {
client.removeListener(ClientEvent.Sync, onSync);
resolve();
} else if (state === "ERROR") {
client.removeListener(ClientEvent.Sync, onSync);
} else if (state === "ERROR") {
reject(data?.error);
client.removeListener(ClientEvent.Sync, onSync);
}
};
client.on(ClientEvent.Sync, onSync);

View File

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

View File

@@ -17,33 +17,13 @@ limitations under the License.
import { Span } from "@opentelemetry/api";
import { MatrixCall } from "matrix-js-sdk";
import { CallEvent } from "matrix-js-sdk/src/webrtc/call";
import {
TransceiverStats,
CallFeedStats,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { ObjectFlattener } from "./ObjectFlattener";
import { ElementCallOpenTelemetry } from "./otel";
import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan";
import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan";
type StreamId = string;
type MID = string;
/**
* Tracks an individual call within a group call, either to a full-mesh peer or a focus
*/
export class OTelCall {
private readonly trackFeedSpan = new Map<
StreamId,
OTelCallAbstractMediaStreamSpan
>();
private readonly trackTransceiverSpan = new Map<
MID,
OTelCallAbstractMediaStreamSpan
>();
constructor(
public userId: string,
public deviceId: string,
@@ -136,62 +116,4 @@ export class OTelCall {
this.span.addEvent("matrix.call.iceCandidateError", flatObject);
};
public onCallFeedStats(callFeeds: CallFeedStats[]): void {
let prvFeeds: StreamId[] = [...this.trackFeedSpan.keys()];
callFeeds.forEach((feed) => {
if (!this.trackFeedSpan.has(feed.stream)) {
this.trackFeedSpan.set(
feed.stream,
new OTelCallFeedMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
feed
)
);
}
this.trackFeedSpan.get(feed.stream)?.update(feed);
prvFeeds = prvFeeds.filter((prvStreamId) => prvStreamId !== feed.stream);
});
prvFeeds.forEach((prvStreamId) => {
this.trackFeedSpan.get(prvStreamId)?.end();
this.trackFeedSpan.delete(prvStreamId);
});
}
public onTransceiverStats(transceiverStats: TransceiverStats[]): void {
let prvTransSpan: MID[] = [...this.trackTransceiverSpan.keys()];
transceiverStats.forEach((transStats) => {
if (!this.trackTransceiverSpan.has(transStats.mid)) {
this.trackTransceiverSpan.set(
transStats.mid,
new OTelCallTransceiverMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
transStats
)
);
}
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
prvTransSpan = prvTransSpan.filter(
(prvStreamId) => prvStreamId !== transStats.mid
);
});
prvTransSpan.forEach((prvMID) => {
this.trackTransceiverSpan.get(prvMID)?.end();
this.trackTransceiverSpan.delete(prvMID);
});
}
public end(): void {
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
this.trackTransceiverSpan.forEach((transceiverSpan) =>
transceiverSpan.end()
);
this.span.end();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,6 @@ import {
ConnectionStatsReport,
ByteSentStatsReport,
SummaryStatsReport,
CallFeedReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils";
@@ -175,7 +174,7 @@ export class OTelGroupCallMembership {
userCalls.get(callTrackingInfo.deviceId).callId !==
callTrackingInfo.call.callId
) {
callTrackingInfo.end();
callTrackingInfo.span.end();
this.callsByCallId.delete(callTrackingInfo.call.callId);
}
}
@@ -331,102 +330,25 @@ export class OTelGroupCallMembership {
});
}
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report.report?.callId;
if (callId) {
call = this.callsByCallId.get(callId);
}
if (!call) {
this.callMembershipSpan?.addEvent(
OTelStatsReportType.CallFeedReport + "_unknown_callId",
{
"call.callId": callId,
"call.opponentMemberId": report.report?.opponentMemberId
? report.report?.opponentMemberId
: "unknown",
}
);
logger.error(
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`
);
return;
} else {
call.onCallFeedStats(report.report.callFeeds);
call.onTransceiverStats(report.report.transceiver);
}
}
public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport>
) {
this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport,
statsReport.report
);
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.ConnectionReport;
const data =
ObjectFlattener.flattenConnectionStatsReportObject(statsReport);
this.buildStatsEventSpan({ type, data });
}
public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport>
) {
this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport,
statsReport.report
);
}
public buildCallStatsSpan(
type: OTelStatsReportType,
report: ByteSentStatsReport | ConnectionStatsReport
): void {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report?.callId;
if (callId) {
call = this.callsByCallId.get(callId);
}
if (!call) {
this.callMembershipSpan?.addEvent(type + "_unknown_callid", {
"call.callId": callId,
"call.opponentMemberId": report.opponentMemberId
? report.opponentMemberId
: "unknown",
});
logger.error(`Received ${type} with unknown call ID: ${callId}`);
return;
}
const data = ObjectFlattener.flattenReportObject(type, report);
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
call.span
);
const options = {
links: [
{
context: call.span.spanContext(),
},
],
};
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
type,
options,
ctx
);
span.setAttribute("matrix.callId", callId);
span.setAttribute(
"matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown"
);
span.addEvent("matrix.call.connection_stats_event", data);
span.end();
const type = OTelStatsReportType.ByteSentReport;
const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport);
this.buildStatsEventSpan({ type, data });
}
public onSummaryStatsReport(
@@ -459,6 +381,45 @@ export class OTelGroupCallMembership {
span.end();
}
}
private buildStatsEventSpan(event: OTelStatsReportEvent): void {
// @ TODO: fix this - Because on multiple calls we receive multiple stats report spans.
// This could be break if stats arrived in same time from different call objects.
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
const ctx = setSpan(
opentelemetry.context.active(),
this.callMembershipSpan
);
this.statsReportSpan.span =
ElementCallOpenTelemetry.instance?.tracer.startSpan(
"matrix.groupCallMembership.statsReport",
undefined,
ctx
);
if (this.statsReportSpan.span === undefined) {
return;
}
this.statsReportSpan.span.setAttribute(
"matrix.confId",
this.groupCall.groupCallId
);
this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId);
this.statsReportSpan.span.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name"
);
this.statsReportSpan.span.addEvent(event.type, event.data);
this.statsReportSpan.stats.push(event);
} else if (
this.statsReportSpan.span !== undefined &&
this.callMembershipSpan
) {
this.statsReportSpan.span.addEvent(event.type, event.data);
this.statsReportSpan.span.end();
this.statsReportSpan = { span: undefined, stats: [] };
}
}
}
interface OTelStatsReportEvent {
@@ -467,8 +428,7 @@ interface OTelStatsReportEvent {
}
enum OTelStatsReportType {
ConnectionReport = "matrix.call.stats.connection",
ByteSentReport = "matrix.call.stats.byteSent",
ConnectionReport = "matrix.stats.connection",
ByteSentReport = "matrix.stats.byteSent",
SummaryReport = "matrix.stats.summary",
CallFeedReport = "matrix.stats.call_feed",
}

View File

@@ -23,12 +23,16 @@ import {
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
export class ObjectFlattener {
public static flattenReportObject(
prefix: string,
report: ConnectionStatsReport | ByteSentStatsReport
public static flattenConnectionStatsReportObject(
statsReport: GroupCallStatsReport<ConnectionStatsReport>
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.conn.",
0
);
return flatObject;
}

View File

@@ -42,7 +42,7 @@ export class ElementCallOpenTelemetry {
const config = Config.get();
// we always enable opentelemetry in general. We only enable the OTLP
// collector if a URL is defined (and in future if another setting is defined)
// Posthog reporting is enabled or disabled
// The posthog exporter is always enabled, posthog reporting is enabled or disabled
// within the posthog code.
const shouldEnableOtlp = Boolean(config.opentelemetry?.collector_url);

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.content {
width: 100%;
max-width: 350px;
align-self: center;
}
.avatarFieldRow {
justify-content: center;
}

View File

@@ -0,0 +1,141 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent, useCallback, useEffect, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { useProfile } from "./useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Modal, ModalContent } from "../Modal";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileModal.module.css";
interface Props {
client: MatrixClient;
onClose: () => void;
[rest: string]: unknown;
}
export function ProfileModal({ client, ...rest }: Props) {
const { onClose } = rest;
const { t } = useTranslation();
const {
success,
error,
loading,
displayName: initialDisplayName,
avatarUrl,
saveProfile,
} = useProfile(client);
const [displayName, setDisplayName] = useState(initialDisplayName || "");
const [removeAvatar, setRemoveAvatar] = useState(false);
const onRemoveAvatar = useCallback(() => {
setRemoveAvatar(true);
}, []);
const onChangeDisplayName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
},
[setDisplayName]
);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const displayNameDataEntry = data.get("displayName");
const avatar: File | string = data.get("avatar");
const avatarSize =
typeof avatar == "string" ? avatar.length : avatar.size;
const displayName =
typeof displayNameDataEntry == "string"
? displayNameDataEntry
: displayNameDataEntry.name;
saveProfile({
displayName,
avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar && (!avatar || avatarSize === 0),
});
},
[saveProfile, removeAvatar]
);
useEffect(() => {
if (success) {
onClose();
}
}, [success, onClose]);
return (
<Modal title={t("Profile")} isDismissable {...rest}>
<ModalContent>
<form onSubmit={onSubmit}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
</FieldRow>
<FieldRow>
<InputField
id="userId"
name="userId"
label={t("User ID")}
type="text"
disabled
value={client.getUserId()}
/>
</FieldRow>
<FieldRow>
<InputField
id="displayName"
name="displayName"
label={t("Display name")}
type="text"
required
autoComplete="off"
placeholder={t("Display name")}
value={displayName}
onChange={onChangeDisplayName}
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow rightAlign>
<Button type="button" variant="secondary" onPress={onClose}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading ? t("Saving…") : t("Save")}
</Button>
</FieldRow>
</form>
</ModalContent>
</Modal>
);
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,22 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback } from "react";
import React, { useCallback, useEffect } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake";
import {
useSubmitRageshake,
useRageshakeRequest,
} from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
import styles from "../input/SelectInput.module.css";
import feedbackStyles from "../input/FeedbackInput.module.css";
interface Props {
roomId?: string;
inCall: boolean;
roomId: string;
onClose?: () => void;
// TODO: add all props for for <Modal>
[index: string]: unknown;
}
export function FeedbackSettingsTab({ roomId }: Props) {
export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest();
@@ -51,36 +57,37 @@ export function FeedbackSettingsTab({ roomId }: Props) {
roomId,
});
if (roomId && sendLogs) {
if (inCall && sendLogs) {
sendRageshakeRequest(roomId, rageshakeRequestId);
}
},
[submitRageshake, roomId, sendRageshakeRequest]
[inCall, submitRageshake, roomId, sendRageshakeRequest]
);
useEffect(() => {
if (sent) {
onClose();
}
}, [sent, onClose]);
return (
<div>
<h4 className={styles.label}>{t("Submit feedback")}</h4>
<Body>
{t(
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below."
)}
</Body>
<form onSubmit={onSubmitFeedback}>
<FieldRow>
<InputField
className={feedbackStyles.feedback}
id="description"
name="description"
label={t("Your feedback")}
placeholder={t("Your feedback")}
type="textarea"
disabled={sending || sent}
/>
</FieldRow>
{sent ? (
<Body> {t("Thanks, we received your feedback!")}</Body>
) : (
<Modal
title={t("Submit feedback")}
isDismissable
onClose={onClose}
{...rest}
>
<ModalContent>
<Body>{t("Having trouble? Help us fix it.")}</Body>
<form onSubmit={onSubmitFeedback}>
<FieldRow>
<InputField
id="description"
name="description"
label={t("Description (optional)")}
type="textarea"
/>
</FieldRow>
<FieldRow>
<InputField
id="sendLogs"
@@ -89,17 +96,19 @@ export function FeedbackSettingsTab({ roomId }: Props) {
type="checkbox"
defaultChecked
/>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={sending}>
{sending ? t("Submitting…") : t("Submit")}
{sending ? t("Submitting feedback…") : t("Submit feedback")}
</Button>
</FieldRow>
)}
</form>
</div>
</form>
</ModalContent>
</Modal>
);
}

View File

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

View File

@@ -34,7 +34,7 @@ import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useMediaHandler } from "../settings/useMediaHandler";
import { findDeviceByName, getNamedDevices } from "../media-utils";
import { findDeviceByName, getDevices } from "../media-utils";
declare global {
interface Window {
@@ -102,17 +102,12 @@ export function GroupCallView({
// Get the available devices so we can match the selected device
// to its ID. This involves getting a media stream (see docs on
// the function) so we only do it once and re-use the result.
//
// But we only want to preload the devices if we get audio or video devices included in the widget request!
// By default, the device list is null!
let devices: MediaDeviceInfo[] | null = null;
const devices = await getDevices();
const { audioInput, videoInput } = ev.detail
.data as unknown as JoinCallData;
if (audioInput !== null) {
// we load the devices because w have an audio device in the widget request
devices = await getNamedDevices();
const deviceId = await findDeviceByName(
audioInput,
"audioinput",
@@ -129,11 +124,6 @@ export function GroupCallView({
}
if (videoInput !== null) {
// we only need to load the devices once time
if (devices === null) {
// we load the devices because w have a video device in the widget request
devices = await getNamedDevices();
}
const deviceId = await findDeviceByName(
videoInput,
"videoinput",
@@ -213,11 +203,7 @@ export function GroupCallView({
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
}
if (
!isPasswordlessUser &&
!isEmbedded &&
!PosthogAnalytics.instance.isEnabled()
) {
if (!isPasswordlessUser && !isEmbedded) {
history.push("/");
}
}, [groupCall, leave, isPasswordlessUser, isEmbedded, history]);
@@ -282,23 +268,8 @@ export function GroupCallView({
);
}
} else if (left) {
// The call ended view is shown for two reasons: prompting guests to create
// an account, and prompting users that have opted into analytics to provide
// feedback. We don't show a feedback prompt to widget users however (at
// least for now), because we don't yet have designs that would allow widget
// users to dismiss the feedback prompt and close the call window without
// submitting anything.
if (
isPasswordlessUser ||
(PosthogAnalytics.instance.isEnabled() && !isEmbedded)
) {
return (
<CallEndedView
endedCallId={groupCall.groupCallId}
client={client}
isPasswordlessUser={isPasswordlessUser}
/>
);
if (isPasswordlessUser) {
return <CallEndedView client={client} />;
} else {
// If the user is a regular user, we'll have sent them back to the homepage,
// so just sit here & do nothing: otherwise we would (briefly) mount the
@@ -310,7 +281,7 @@ export function GroupCallView({
} else if (isEmbedded) {
return (
<FullScreenView>
<h1>{t("Loading…")}</h1>
<h1>{t("Loading room…")}</h1>
</FullScreenView>
);
} else {

View File

@@ -82,21 +82,17 @@ limitations under the License.
bottom: 0;
}
/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cqmin units */
@container videoTile (width > 0) {
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* To make avatars scale smoothly with their tiles during animations, we
override the styles set on the element */
--avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */
width: var(--avatarSize) !important;
height: var(--avatarSize) !important;
border-radius: 10000px !important;
}
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* To make avatars scale smoothly with their tiles during animations, we
override the styles set on the element */
--avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2);
width: var(--avatarSize) !important;
height: var(--avatarSize) !important;
border-radius: 10000px !important;
}
@media (min-height: 300px) {

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,7 +24,6 @@ import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { OverlayTriggerState } from "@react-stately/overlays";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import type { IWidgetApiRequest } from "matrix-widget-api";
@@ -34,8 +33,6 @@ import {
MicButton,
VideoButton,
ScreenshareButton,
SettingsButton,
InviteButton,
} from "../button";
import {
Header,
@@ -44,10 +41,19 @@ import {
RoomHeaderInfo,
VersionMismatchWarning,
} from "../Header";
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
import {
VideoGrid,
useVideoGridLayout,
ChildrenProperties,
} from "../video-grid/VideoGrid";
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { useMediaHandler } from "../settings/useMediaHandler";
import {
useNewGrid,
@@ -68,11 +74,6 @@ import { AudioSink } from "../video-grid/AudioSink";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { VideoTile } from "../video-grid/VideoTile";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -127,6 +128,7 @@ export function InCallView({
}: Props) {
const { t } = useTranslation();
usePreventScroll();
const joinRule = useJoinRule(groupCall.room);
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
@@ -149,9 +151,10 @@ export function InCallView({
const [audioContext, audioDestination] = useAudioContext();
const [showInspector] = useShowInspector();
const { hideScreensharing } = useUrlParams();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
const joinRule = useJoinRule(groupCall.room);
const { hideScreensharing } = useUrlParams();
useCallViewKeyboardShortcuts(
containerRef1,
@@ -216,8 +219,6 @@ export function InCallView({
focused: screenshareFeeds.length === 0 && callFeed === activeSpeaker,
isLocal: member.userId === localUserId && deviceId === localDeviceId,
presenter,
isSpeaker: callFeed === activeSpeaker,
largeBaseSize: false,
connectionState,
});
}
@@ -230,7 +231,6 @@ export function InCallView({
// Add the screenshares too
for (const screenshareFeed of screenshareFeeds) {
const member = screenshareFeed.getMember()!;
const deviceId = screenshareFeed.deviceId!;
const connectionState = participants
.get(member)
?.get(screenshareFeed.deviceId!)?.connectionState;
@@ -245,9 +245,6 @@ export function InCallView({
focused: true,
isLocal: screenshareFeed.isLocal(),
presenter: false,
isSpeaker: screenshareFeed === activeSpeaker,
largeBaseSize: true,
placeNear: `${member.userId} ${deviceId}`,
connectionState,
});
}
@@ -305,7 +302,7 @@ export function InCallView({
}
if (maximisedParticipant) {
return (
<VideoTile
<VideoTileContainer
targetHeight={bounds.height}
targetWidth={bounds.width}
key={maximisedParticipant.id}
@@ -313,10 +310,10 @@ export function InCallView({
getAvatar={renderAvatar}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={true}
maximised={Boolean(maximisedParticipant)}
fullscreen={maximisedParticipant === fullscreenParticipant}
onFullscreen={toggleFullscreen}
showSpeakingIndicator={false}
/>
);
}
@@ -327,16 +324,17 @@ export function InCallView({
layout={layout}
disableAnimations={prefersReducedMotion || isSafari}
>
{(props) => (
<VideoTile
{({ item, ...rest }: ChildrenProperties) => (
<VideoTileContainer
item={item}
getAvatar={renderAvatar}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={items.length < 3}
maximised={false}
fullscreen={false}
onFullscreen={toggleFullscreen}
showSpeakingIndicator={items.length > 2}
{...props}
{...rest}
/>
)}
</Grid>
@@ -348,36 +346,6 @@ export function InCallView({
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openSettings = useCallback(() => {
settingsModalState.open();
}, [settingsModalState]);
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openInvite = useCallback(() => {
inviteModalState.open();
}, [inviteModalState]);
const containerClasses = classNames(styles.inRoom, {
[styles.maximised]: maximisedParticipant,
});
@@ -415,13 +383,11 @@ export function InCallView({
key="1"
muted={microphoneMuted}
onPress={toggleMicrophoneMuted}
data-testid="incall_mute"
/>,
<VideoButton
key="2"
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
data-testid="incall_videomute"
/>
);
@@ -432,18 +398,25 @@ export function InCallView({
key="3"
enabled={isScreensharing}
onPress={toggleScreensharing}
data-testid="incall_screenshare"
/>
);
}
if (!maximisedParticipant) {
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
buttons.push(
<OverflowMenu
key="4"
inCall
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
showInvite={joinRule === JoinRule.Public}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>
);
}
}
buttons.push(
<HangupButton key="6" onPress={onLeave} data-testid="incall_leave" />
);
buttons.push(<HangupButton key="6" onPress={onLeave} />);
footer = <div className={styles.footer}>{buttons}</div>;
}
@@ -461,9 +434,7 @@ export function InCallView({
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
{joinRule === JoinRule.Public && (
<InviteButton variant="icon" onClick={openInvite} />
)}
<UserMenuContainer preventNavigation />
</RightNav>
</Header>
)}
@@ -477,22 +448,12 @@ export function InCallView({
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
{rageshakeRequestModalState.isOpen && !noControls && (
{rageshakeRequestModalState.isOpen && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomIdOrAlias={roomIdOrAlias}
/>
)}
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && (
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}
</div>
);
}

View File

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

View File

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

143
src/room/OverflowMenu.tsx Normal file
View File

@@ -0,0 +1,143 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { OverlayTriggerState } from "@react-stately/overlays";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { Menu } from "../Menu";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
import { Config } from "../config/Config";
interface Props {
roomIdOrAlias: string;
inCall: boolean;
groupCall: GroupCall;
showInvite: boolean;
feedbackModalState: OverlayTriggerState;
feedbackModalProps: {
isOpen: boolean;
onClose: () => void;
};
}
export function OverflowMenu({
roomIdOrAlias,
inCall,
groupCall,
showInvite,
feedbackModalState,
feedbackModalProps,
}: Props) {
const { t } = useTranslation();
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
// TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444
const onAction = useCallback(
(key) => {
switch (key) {
case "invite":
inviteModalState.open();
break;
case "settings":
settingsModalState.open();
break;
case "feedback":
feedbackModalState.open();
break;
}
},
[feedbackModalState, inviteModalState, settingsModalState]
);
const tooltip = useCallback(() => t("More"), [t]);
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger tooltip={tooltip} placement="top">
<Button variant="toolbar">
<OverflowIcon />
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label={t("More menu")} onAction={onAction}>
{showInvite && (
<Item key="invite" textValue={t("Invite people")}>
<AddUserIcon />
<span>{t("Invite people")}</span>
</Item>
)}
<Item key="settings" textValue={t("Settings")}>
<SettingsIcon />
<span>{t("Settings")}</span>
</Item>
{Config.get().rageshake?.submit_url && (
<Item key="feedback" textValue={t("Submit feedback")}>
<FeedbackIcon />
<span>{t("Submit feedback")}</span>
</Item>
)}
</Menu>
)}
</PopoverMenuTrigger>
{settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />}
{inviteModalState.isOpen && (
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}
{feedbackModalState.isOpen && (
<FeedbackModal
{...feedbackModalProps}
roomId={groupCall?.room.roomId}
inCall={inCall}
/>
)}
</>
);
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ import { useTranslation } from "react-i18next";
import { useDelayedState } from "../useDelayedState";
import { useModalTriggerState } from "../Modal";
import { InviteModal } from "./InviteModal";
import { HangupButton, InviteButton, SettingsButton } from "../button";
import { HangupButton, InviteButton } from "../button";
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
import styles from "./PTTCallView.module.css";
import { Facepile } from "../Facepile";
@@ -41,10 +41,10 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
import { usePTTSounds } from "../sound/usePttSounds";
import { PTTClips } from "../sound/PTTClips";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { Size } from "../Avatar";
import { ParticipantInfo } from "./useGroupCall";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
function getPromptText(
networkWaiting: boolean,
@@ -126,9 +126,8 @@ export const PTTCallView: React.FC<Props> = ({
const { t } = useTranslation();
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: settingsModalState, modalProps: settingsModalProps } =
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
const facepileSize = bounds.width < 800 ? Size.SM : Size.MD;
const showControls = bounds.height > 500;
@@ -233,7 +232,14 @@ export const PTTCallView: React.FC<Props> = ({
/>
</div>
<div className={styles.footer}>
<SettingsButton onPress={() => settingsModalState.open()} />
<OverflowMenu
inCall
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
showInvite={false}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>
{!isEmbedded && <HangupButton onPress={onLeave} />}
<InviteButton onPress={() => inviteModalState.open()} />
</div>
@@ -259,7 +265,7 @@ export const PTTCallView: React.FC<Props> = ({
<div className={styles.talkingInfo} />
))}
<PTTButton
enabled={!inviteModalState.isOpen && !settingsModalState.isOpen}
enabled={!feedbackModalState.isOpen}
showTalkOverError={showTalkOverError}
activeSpeakerUserId={activeSpeakerUserId}
activeSpeakerDisplayName={activeSpeakerDisplayName}
@@ -306,13 +312,6 @@ export const PTTCallView: React.FC<Props> = ({
</div>
</div>
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && showControls && (
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}

View File

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

View File

@@ -1,5 +1,5 @@
/*
Copyright 2021-2023 New Vector Ltd
Copyright 2021-2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView";
import { useUrlParams } from "../UrlParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { translatedError } from "../TranslatedError";
import { useOptInAnalytics } from "../settings/useSetting";
@@ -100,13 +101,15 @@ export const RoomPage: FC = () => {
}
return (
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
createPtt={isPtt}
>
{groupCallView}
</GroupCallLoader>
<MediaHandlerProvider client={client}>
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
createPtt={isPtt}
>
{groupCallView}
</GroupCallLoader>
</MediaHandlerProvider>
);
};

View File

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

View File

@@ -34,7 +34,6 @@ import {
ByteSentStatsReport,
ConnectionStatsReport,
SummaryStatsReport,
CallFeedReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { usePageUnload } from "./usePageUnload";
@@ -65,7 +64,7 @@ export interface UseGroupCallReturnType {
localVideoMuted: boolean;
error: TranslatedError | null;
initLocalCallFeed: () => void;
enter: () => Promise<void>;
enter: () => void;
leave: () => void;
toggleLocalVideoMuted: () => void;
toggleMicrophoneMuted: () => void;
@@ -364,12 +363,6 @@ export function useGroupCall(
groupCallOTelMembership?.onSummaryStatsReport(report);
}
function onCallFeedStatsReport(
report: GroupCallStatsReport<CallFeedReport>
): void {
groupCallOTelMembership?.onCallFeedStatsReport(report);
}
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
groupCall.on(
@@ -394,11 +387,6 @@ export function useGroupCall(
onByteSentStatsReport
);
groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport);
groupCall.on(
GroupCallStatsReportEvent.CallFeedStats,
onCallFeedStatsReport
);
groupCall.room.currentState.on(
RoomStateEvent.Update,
checkForParallelCalls
@@ -462,10 +450,6 @@ export function useGroupCall(
GroupCallStatsReportEvent.SummaryStats,
onSummaryStatsReport
);
groupCall.removeListener(
GroupCallStatsReportEvent.CallFeedStats,
onCallFeedStatsReport
);
groupCall.room.currentState.off(
RoomStateEvent.Update,
checkForParallelCalls
@@ -483,7 +467,7 @@ export function useGroupCall(
[groupCall]
);
const enter = useCallback(async () => {
const enter = useCallback(() => {
if (
groupCall.state !== GroupCallState.LocalCallFeedUninitialized &&
groupCall.state !== GroupCallState.LocalCallFeedInitialized
@@ -498,7 +482,7 @@ export function useGroupCall(
// have started tracking by the time calls start getting created.
groupCallOTelMembership?.onJoinCall();
await groupCall.enter().catch((error) => {
groupCall.enter().catch((error) => {
console.error(error);
updateState({ error });
});

View File

@@ -1,113 +0,0 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useEffect, useRef } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { useProfile } from "../profile/useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileSettingsTab.module.css";
interface Props {
client: MatrixClient;
}
export function ProfileSettingsTab({ client }: Props) {
const { t } = useTranslation();
const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
const formRef = useRef<HTMLFormElement | null>(null);
const formChanged = useRef(false);
const onFormChange = useCallback(() => {
formChanged.current = true;
}, []);
const removeAvatar = useRef(false);
const onRemoveAvatar = useCallback(() => {
removeAvatar.current = true;
formChanged.current = true;
}, []);
useEffect(() => {
const form = formRef.current!;
// Auto-save when the user dismisses this component
return () => {
if (formChanged.current) {
const data = new FormData(form);
const displayNameDataEntry = data.get("displayName");
const avatar = data.get("avatar");
const avatarSize =
typeof avatar == "string" ? avatar.length : avatar?.size ?? 0;
const displayName =
typeof displayNameDataEntry == "string"
? displayNameDataEntry
: displayNameDataEntry?.name ?? null;
saveProfile({
displayName,
avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
});
}
};
}, [saveProfile]);
return (
<form onChange={onFormChange} ref={formRef} className={styles.content}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
</FieldRow>
<FieldRow>
<InputField
id="userId"
name="userId"
label={t("Username")}
type="text"
disabled
value={client.getUserId()!}
/>
</FieldRow>
<FieldRow>
<InputField
id="displayName"
name="displayName"
label={t("Display name")}
type="text"
required
autoComplete="off"
placeholder={t("Display name")}
defaultValue={displayName}
data-testid="profile_displayname"
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
</form>
);
}

View File

@@ -19,10 +19,6 @@ limitations under the License.
height: 480px;
}
.settingsModal p {
color: var(--secondary-content);
}
.tabContainer {
padding: 27px 20px;
}
@@ -30,3 +26,12 @@ limitations under the License.
.fieldRowText {
margin-bottom: 0;
}
/*
This style guarantees a fixed width of the tab bar in the settings window.
The "Developer" item in the tab bar can be toggled.
Without a defined width activating the developer tab makes the tab container jump to the right.
*/
.tabLabel {
min-width: 80px;
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
import React from "react";
import { Item } from "@react-stately/collections";
import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk";
import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
@@ -26,8 +25,6 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
import { ReactComponent as UserIcon } from "../icons/User.svg";
import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg";
import { SelectInput } from "../input/SelectInput";
import { useMediaHandler } from "./useMediaHandler";
import {
@@ -42,14 +39,9 @@ import { Button } from "../button";
import { useDownloadDebugLog } from "./submit-rageshake";
import { Body, Caption } from "../typography/Typography";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
interface Props {
isOpen: boolean;
client: MatrixClient;
roomId?: string;
defaultTab?: string;
onClose: () => void;
}
@@ -65,9 +57,7 @@ export const SettingsModal = (props: Props) => {
audioOutput,
audioOutputs,
setAudioOutput,
useDeviceNames,
} = useMediaHandler();
useDeviceNames();
const [spatialAudio, setSpatialAudio] = useSpatialAudio();
const [showInspector, setShowInspector] = useShowInspector();
@@ -78,15 +68,6 @@ export const SettingsModal = (props: Props) => {
const downloadDebugLog = useDownloadDebugLog();
const [selectedTab, setSelectedTab] = useState<string | undefined>();
const onSelectedTabChanged = useCallback(
(tab) => {
setSelectedTab(tab);
},
[setSelectedTab]
);
const optInDescription = (
<Caption>
<Trans>
@@ -106,13 +87,8 @@ export const SettingsModal = (props: Props) => {
className={styles.settingsModal}
{...props}
>
<TabContainer
onSelectionChange={onSelectedTabChanged}
selectedKey={selectedTab ?? props.defaultTab ?? "audio"}
className={styles.tabContainer}
>
<TabContainer className={styles.tabContainer}>
<TabItem
key="audio"
title={
<>
<AudioIcon width={16} height={16} />
@@ -169,7 +145,6 @@ export const SettingsModal = (props: Props) => {
</FieldRow>
</TabItem>
<TabItem
key="video"
title={
<>
<VideoIcon width={16} height={16} />
@@ -192,29 +167,6 @@ export const SettingsModal = (props: Props) => {
</SelectInput>
</TabItem>
<TabItem
key="profile"
title={
<>
<UserIcon width={15} height={15} />
<span>{t("Profile")}</span>
</>
}
>
<ProfileSettingsTab client={props.client} />
</TabItem>
<TabItem
key="feedback"
title={
<>
<FeedbackIcon width={16} height={16} />
<span>{t("Feedback")}</span>
</>
}
>
<FeedbackSettingsTab roomId={props.roomId} />
</TabItem>
<TabItem
key="more"
title={
<>
<OverflowIcon width={16} height={16} />
@@ -222,10 +174,18 @@ export const SettingsModal = (props: Props) => {
</>
}
>
<h4>Developer</h4>
<p>
Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}
</p>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}
/>
</FieldRow>
<FieldRow>
<InputField
id="developerSettingsTab"
@@ -240,22 +200,9 @@ export const SettingsModal = (props: Props) => {
}
/>
</FieldRow>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}
/>
</FieldRow>
</TabItem>
{developerSettingsTab && (
<TabItem
key="developer"
title={
<>
<DeveloperIcon width={16} height={16} />

View File

@@ -101,7 +101,6 @@ export function useSubmitRageshake(): {
body.append("user_agent", userAgent);
body.append("installed_pwa", "false");
body.append("touch_input", touchInput);
body.append("call_backend", "full-mesh");
if (client) {
const userId = client.getUserId();
@@ -250,7 +249,7 @@ export function useSubmitRageshake(): {
body.append(
"file",
gzip(ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump()),
"traces.json.gz"
"traces.json"
);
if (inspectorState) {

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable @typescript-eslint/ban-ts-comment */
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler";
import React, {
useState,
useEffect,
@@ -23,27 +41,18 @@ import React, {
useContext,
createContext,
ReactNode,
useRef,
} from "react";
import { useClient } from "../ClientContext";
import { getNamedDevices } from "../media-utils";
export interface MediaHandlerContextInterface {
audioInput: string | undefined;
audioInput: string;
audioInputs: MediaDeviceInfo[];
setAudioInput: (deviceId: string) => void;
videoInput: string | undefined;
videoInput: string;
videoInputs: MediaDeviceInfo[];
setVideoInput: (deviceId: string) => void;
audioOutput: string | undefined;
audioOutput: string;
audioOutputs: MediaDeviceInfo[];
setAudioOutput: (deviceId: string) => void;
/**
* A hook which requests for devices to be named. This requires media
* permissions.
*/
useDeviceNames: () => void;
}
const MediaHandlerContext =
@@ -54,7 +63,6 @@ interface MediaPreferences {
videoInput?: string;
audioOutput?: string;
}
function getMediaPreferences(): MediaPreferences {
const mediaPreferences = localStorage.getItem("matrix-media-preferences");
@@ -62,10 +70,10 @@ function getMediaPreferences(): MediaPreferences {
try {
return JSON.parse(mediaPreferences);
} catch (e) {
return {};
return undefined;
}
} else {
return {};
return undefined;
}
}
@@ -80,13 +88,11 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void {
})
);
}
interface Props {
client: MatrixClient;
children: ReactNode;
}
export function MediaHandlerProvider({ children }: Props): JSX.Element {
const { client } = useClient();
export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
const [
{
audioInput,
@@ -97,109 +103,118 @@ export function MediaHandlerProvider({ children }: Props): JSX.Element {
audioOutputs,
},
setState,
] = useState(() => ({
audioInput: undefined as string | undefined,
videoInput: undefined as string | undefined,
audioOutput: undefined as string | undefined,
audioInputs: [] as MediaDeviceInfo[],
videoInputs: [] as MediaDeviceInfo[],
audioOutputs: [] as MediaDeviceInfo[],
}));
] = useState(() => {
const mediaPreferences = getMediaPreferences();
const mediaHandler = client.getMediaHandler();
// A ref counting the number of components currently mounted that want
// to know device names
const numComponentsWantingNames = useRef(0);
mediaHandler.restoreMediaSettings(
mediaPreferences?.audioInput,
mediaPreferences?.videoInput
);
const updateDevices = useCallback(
async (client: MatrixClient, initial: boolean) => {
// Only request device names if components actually want them, because it
// could trigger an extra permission pop-up
const devices = await (numComponentsWantingNames.current > 0
? getNamedDevices()
: navigator.mediaDevices.enumerateDevices());
const mediaPreferences = getMediaPreferences();
const audioInputs = devices.filter((d) => d.kind === "audioinput");
const videoInputs = devices.filter((d) => d.kind === "videoinput");
const audioOutputs = devices.filter((d) => d.kind === "audiooutput");
const audioInput = (
mediaPreferences.audioInput === undefined
? audioInputs.at(0)
: audioInputs.find(
(d) => d.deviceId === mediaPreferences.audioInput
) ?? audioInputs.at(0)
)?.deviceId;
const videoInput = (
mediaPreferences.videoInput === undefined
? videoInputs.at(0)
: videoInputs.find(
(d) => d.deviceId === mediaPreferences.videoInput
) ?? videoInputs.at(0)
)?.deviceId;
const audioOutput =
mediaPreferences.audioOutput === undefined
? undefined
: audioOutputs.find(
(d) => d.deviceId === mediaPreferences.audioOutput
)?.deviceId;
updateMediaPreferences({ audioInput, videoInput, audioOutput });
setState({
audioInput,
videoInput,
audioOutput,
audioInputs,
videoInputs,
audioOutputs,
});
if (
initial ||
audioInput !== mediaPreferences.audioInput ||
videoInput !== mediaPreferences.videoInput
) {
client.getMediaHandler().setMediaInputs(audioInput, videoInput);
}
},
[setState]
);
const useDeviceNames = useCallback(() => {
// This is a little weird from React's perspective as it looks like a
// dynamic hook, but it works
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (client) {
numComponentsWantingNames.current++;
if (numComponentsWantingNames.current === 1)
updateDevices(client, false);
return () => void numComponentsWantingNames.current--;
}
}, []);
}, [client, updateDevices]);
return {
// @ts-ignore, ignore that audioInput is a private members of mediaHandler
audioInput: mediaHandler.audioInput,
// @ts-ignore, ignore that videoInput is a private members of mediaHandler
videoInput: mediaHandler.videoInput,
audioOutput: undefined,
audioInputs: [],
videoInputs: [],
audioOutputs: [],
};
});
useEffect(() => {
if (client) {
updateDevices(client, true);
const onDeviceChange = () => updateDevices(client, false);
navigator.mediaDevices.addEventListener("devicechange", onDeviceChange);
const mediaHandler = client.getMediaHandler();
return () => {
navigator.mediaDevices.removeEventListener(
"devicechange",
onDeviceChange
function updateDevices(): void {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const mediaPreferences = getMediaPreferences();
const audioInputs = devices.filter(
(device) => device.kind === "audioinput"
);
client.getMediaHandler().stopAllStreams();
};
const audioConnected = audioInputs.some(
// @ts-ignore
(device) => device.deviceId === mediaHandler.audioInput
);
// @ts-ignore
let audioInput = mediaHandler.audioInput;
if (!audioConnected && audioInputs.length > 0) {
audioInput = audioInputs[0].deviceId;
}
const videoInputs = devices.filter(
(device) => device.kind === "videoinput"
);
const videoConnected = videoInputs.some(
// @ts-ignore
(device) => device.deviceId === mediaHandler.videoInput
);
// @ts-ignore
let videoInput = mediaHandler.videoInput;
if (!videoConnected && videoInputs.length > 0) {
videoInput = videoInputs[0].deviceId;
}
const audioOutputs = devices.filter(
(device) => device.kind === "audiooutput"
);
let audioOutput = undefined;
if (
mediaPreferences &&
audioOutputs.some(
(device) => device.deviceId === mediaPreferences.audioOutput
)
) {
audioOutput = mediaPreferences.audioOutput;
}
if (
// @ts-ignore
(mediaHandler.videoInput && mediaHandler.videoInput !== videoInput) ||
// @ts-ignore
mediaHandler.audioInput !== audioInput
) {
mediaHandler.setMediaInputs(audioInput, videoInput);
}
updateMediaPreferences({ audioInput, videoInput, audioOutput });
setState({
audioInput,
videoInput,
audioOutput,
audioInputs,
videoInputs,
audioOutputs,
});
});
}
}, [client, updateDevices]);
updateDevices();
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, updateDevices);
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
return () => {
mediaHandler.removeListener(
MediaHandlerEvent.LocalStreamsChanged,
updateDevices
);
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
mediaHandler.stopAllStreams();
};
}, [client]);
const setAudioInput: (deviceId: string) => void = useCallback(
(deviceId: string) => {
updateMediaPreferences({ audioInput: deviceId });
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
client?.getMediaHandler().setAudioInput(deviceId);
client.getMediaHandler().setAudioInput(deviceId);
},
[client]
);
@@ -230,7 +245,6 @@ export function MediaHandlerProvider({ children }: Props): JSX.Element {
audioOutput,
audioOutputs,
setAudioOutput,
useDeviceNames,
}),
[
audioInput,
@@ -242,7 +256,6 @@ export function MediaHandlerProvider({ children }: Props): JSX.Element {
audioOutput,
audioOutputs,
setAudioOutput,
useDeviceNames,
]
);

View File

@@ -25,14 +25,12 @@ limitations under the License.
list-style: none;
padding: 0;
margin: 0 auto 24px auto;
gap: 16px;
overflow: scroll;
max-width: 100%;
}
.tab {
max-width: 190px;
min-width: fit-content;
height: 32px;
box-sizing: border-box;
border-radius: 8px;
background-color: transparent;
display: flex;
@@ -40,7 +38,6 @@ limitations under the License.
padding: 0 8px;
border: none;
cursor: pointer;
font-size: var(--font-size-body);
}
.tab > * {
@@ -81,18 +78,17 @@ limitations under the License.
@media (min-width: 800px) {
.tab {
width: 200px;
padding: 0 16px;
}
.tab > * {
margin: 0 12px 0 0;
margin: 0 16px 0 0;
}
.tabContainer {
width: 100%;
flex-direction: row;
padding: 20px 18px;
padding: 27px 20px;
box-sizing: border-box;
overflow: hidden;
}
@@ -100,7 +96,6 @@ limitations under the License.
.tabList {
flex-direction: column;
margin-bottom: 0;
gap: 0;
}
.tabPanel {

View File

@@ -21,14 +21,14 @@ import { MutableRefObject, RefCallback, useCallback } from "react";
* same DOM node.
*/
export const useMergedRefs = <T>(
...refs: (MutableRefObject<T | null> | RefCallback<T | null> | null)[]
...refs: (MutableRefObject<T | null> | RefCallback<T | null>)[]
): RefCallback<T | null> =>
useCallback(
(value) =>
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref !== null) {
} else {
ref.current = value;
}
}),

View File

@@ -18,7 +18,7 @@ limitations under the License.
contain: strict;
position: relative;
flex-grow: 1;
padding: 0 20px var(--footerHeight);
padding: 0 20px;
overflow-y: auto;
overflow-x: hidden;
}
@@ -28,6 +28,7 @@ limitations under the License.
display: grid;
grid-auto-rows: 163px;
gap: 8px;
padding-bottom: var(--footerHeight);
}
.slot {
@@ -36,7 +37,7 @@ limitations under the License.
@media (min-width: 800px) {
.grid {
padding: 0 22px var(--footerHeight);
padding: 0 22px;
}
.slotGrid {

View File

@@ -43,12 +43,8 @@ import {
fillGaps,
forEachCellInArea,
cycleTileSize,
addItems,
tryMoveTile,
resize,
promoteSpeakers,
appendItems,
} from "./model";
import { TileWrapper } from "./TileWrapper";
interface GridState extends Grid {
/**
@@ -84,21 +80,15 @@ const useGridState = (
}),
};
// Step 2: Resize the grid if necessary and backfill gaps left behind by
// removed tiles
// Resizing already takes care of backfilling gaps
const grid2 =
columns !== grid1.columns ? resize(grid1, columns!) : fillGaps(grid1);
// Step 2: Backfill gaps left behind by removed tiles
const grid2 = fillGaps(grid1);
// Step 3: Add new tiles to the end of the grid
const existingItemIds = new Set(
grid2.cells.filter((c) => c !== undefined).map((c) => c!.item.id)
);
const newItems = items.filter((i) => !existingItemIds.has(i.id));
const grid3 = addItems(newItems, grid2);
// Step 4: Promote speakers to the top
promoteSpeakers(grid3);
const grid3 = appendItems(newItems, grid2);
return { ...grid3, generation: prevGrid.generation + 1 };
},
@@ -213,10 +203,14 @@ export const NewVideoGrid: FC<Props> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slotGrid, slotGridGeneration, gridBounds]);
const columns = useMemo(
() =>
// The grid bounds might not be known yet
gridBounds.width === 0
const [columns] = useReactiveState<number | null>(
// Since grid resizing isn't implemented yet, pick a column count on mount
// and stick to it
(prevColumns) =>
prevColumns !== undefined && prevColumns !== null
? prevColumns
: // The grid bounds might not be known yet
gridBounds.width === 0
? null
: Math.max(2, Math.floor(gridBounds.width * 0.0045)),
[gridBounds]
@@ -251,7 +245,6 @@ export const NewVideoGrid: FC<Props> = ({
opacity: 0,
scale: 0,
shadow: 1,
shadowSpread: 0,
zIndex: 1,
x,
y,
@@ -262,7 +255,7 @@ export const NewVideoGrid: FC<Props> = ({
enter: { opacity: 1, scale: 1, immediate: disableAnimations },
update: ({ item, x, y, width, height }: Tile) =>
item.id === dragState.current?.tileId
? null
? {}
: {
x,
y,
@@ -272,81 +265,65 @@ export const NewVideoGrid: FC<Props> = ({
},
leave: { opacity: 0, scale: 0, immediate: disableAnimations },
config: { mass: 0.7, tension: 252, friction: 25 },
})
}),
[tiles, disableAnimations]
// react-spring's types are bugged and can't infer the spring type
) as unknown as [TransitionFn<Tile, TileSpring>, SpringRef<TileSpring>];
// Because we're using react-spring in imperative mode, we're responsible for
// firing animations manually whenever the tiles array updates
useEffect(() => {
springRef.start();
}, [tiles, springRef]);
const animateDraggedTile = (endOfGesture: boolean) => {
const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!;
const tile = tiles.find((t) => t.item.id === tileId)!;
const originIndex = grid!.cells.findIndex((c) => c?.item.id === tileId);
const originCell = grid!.cells[originIndex]!;
springRef.current
.find((c) => (c.item as Tile).item.id === tileId)
?.start(
endOfGesture
? {
scale: 1,
zIndex: 1,
shadow: 1,
x: tile.x,
y: tile.y,
width: tile.width,
height: tile.height,
immediate: disableAnimations || ((key) => key === "zIndex"),
// Allow the tile's position to settle before pushing its
// z-index back down
delay: (key) => (key === "zIndex" ? 500 : 0),
}
: {
scale: 1.1,
zIndex: 2,
shadow: 15,
x: tileX,
y: tileY,
immediate:
disableAnimations ||
((key) => key === "zIndex" || key === "x" || key === "y"),
}
);
springRef.start((_i, controller) => {
if ((controller.item as Tile).item.id === tileId) {
if (endOfGesture) {
return {
scale: 1,
zIndex: 1,
shadow: 1,
x: tile.x,
y: tile.y,
width: tile.width,
height: tile.height,
immediate: disableAnimations || ((key) => key === "zIndex"),
// Allow the tile's position to settle before pushing its
// z-index back down
delay: (key) => (key === "zIndex" ? 500 : 0),
};
} else {
return {
scale: 1.1,
zIndex: 2,
shadow: 15,
x: tileX,
y: tileY,
immediate:
disableAnimations ||
((key) => key === "zIndex" || key === "x" || key === "y"),
};
}
} else {
return {};
}
});
const columns = grid!.columns;
const rows = row(grid!.cells.length - 1, grid!) + 1;
const cursorColumn = Math.floor(
(cursorX / slotGrid!.clientWidth) * columns
const overTile = tiles.find(
(t) =>
cursorX >= t.x &&
cursorX < t.x + t.width &&
cursorY >= t.y &&
cursorY < t.y + t.height
);
const cursorRow = Math.floor((cursorY / slotGrid!.clientHeight) * rows);
const cursorColumnOnTile = Math.floor(
((cursorX - tileX) / tile.width) * originCell.columns
);
const cursorRowOnTile = Math.floor(
((cursorY - tileY) / tile.height) * originCell.rows
);
const dest =
Math.max(
0,
Math.min(
columns - originCell.columns,
cursorColumn - cursorColumnOnTile
)
) +
grid!.columns *
Math.max(
0,
Math.min(rows - originCell.rows, cursorRow - cursorRowOnTile)
);
if (dest !== originIndex) setGrid((g) => tryMoveTile(g, originIndex, dest));
if (overTile !== undefined && overTile.item.id !== tileId) {
setGrid((g) => ({
...g!,
cells: g!.cells.map((c) => {
if (c?.item === overTile.item) return { ...c, item: tile.item };
if (c?.item === tile.item) return { ...c, item: overTile.item };
return c;
}),
}));
}
};
// Callback for useDrag. We could call useDrag here, but the default
@@ -469,19 +446,16 @@ export const NewVideoGrid: FC<Props> = ({
>
{slots}
</div>
{tileTransitions((spring, tile) => (
<TileWrapper
key={tile.item.id}
id={tile.item.id}
onDragRef={onTileDragRef}
targetWidth={tile.width}
targetHeight={tile.height}
item={tile.item}
{...spring}
>
{children}
</TileWrapper>
))}
{tileTransitions((style, tile) =>
children({
...style,
key: tile.item.id,
targetWidth: tile.width,
targetHeight: tile.height,
item: tile.item,
onDragRef: onTileDragRef,
})
)}
</div>
);
};

View File

@@ -26,10 +26,7 @@ export interface TileDescriptor {
member: RoomMember;
focused: boolean;
presenter: boolean;
isSpeaker: boolean;
callFeed?: CallFeed;
isLocal?: boolean;
largeBaseSize: boolean;
placeNear?: string;
connectionState: ConnectionState;
}

View File

@@ -1,101 +0,0 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FC, memo, ReactNode, RefObject, useRef } from "react";
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
import { SpringValue, to } from "@react-spring/web";
import { TileDescriptor } from "./TileDescriptor";
import { ChildrenProperties } from "./VideoGrid";
interface Props {
id: string;
onDragRef: RefObject<
(
tileId: string,
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => void
>;
targetWidth: number;
targetHeight: number;
item: TileDescriptor;
opacity: SpringValue<number>;
scale: SpringValue<number>;
shadow: SpringValue<number>;
shadowSpread: SpringValue<number>;
zIndex: SpringValue<number>;
x: SpringValue<number>;
y: SpringValue<number>;
width: SpringValue<number>;
height: SpringValue<number>;
children: (props: ChildrenProperties) => ReactNode;
}
/**
* A wrapper around a tile in a video grid. This component exists to decouple
* child components from the grid.
*/
export const TileWrapper: FC<Props> = memo(
({
id,
onDragRef,
targetWidth,
targetHeight,
item,
opacity,
scale,
shadow,
shadowSpread,
zIndex,
x,
y,
width,
height,
children,
}) => {
const ref = useRef<HTMLElement | null>(null);
useDrag((state) => onDragRef?.current!(id, state), {
target: ref,
filterTaps: true,
preventScroll: true,
});
return (
<>
{children({
ref,
style: {
opacity,
scale,
zIndex,
x,
y,
width,
height,
boxShadow: to(
[shadow, shadowSpread],
(s, ss) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px ${ss}px`
),
},
targetWidth,
targetHeight,
item,
})}
</>
);
}
);

View File

@@ -0,0 +1,97 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from "react";
import { useMemo } from "react";
import { RoomMember } from "matrix-js-sdk";
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
import { VideoTile } from "./VideoTile";
import { Button } from "../button";
import { ConnectionState } from "../room/useGroupCall";
import { TileDescriptor } from "./TileDescriptor";
export default {
title: "VideoGrid",
parameters: {
layout: "fullscreen",
},
};
export const ParticipantsTest = () => {
const { layout, setLayout } = useVideoGridLayout(false);
const [participantCount, setParticipantCount] = useState(1);
const items: TileDescriptor[] = useMemo(
() =>
new Array(participantCount).fill(undefined).map((_, i) => ({
id: (i + 1).toString(),
member: new RoomMember("!fake:room.id", `@user${i}:fake.dummy`),
focused: false,
presenter: false,
connectionState: ConnectionState.Connected,
})),
[participantCount]
);
return (
<>
<div style={{ display: "flex", width: "100vw", height: "32px" }}>
<Button
onPress={() =>
setLayout(layout === "freedom" ? "spotlight" : "freedom")
}
>
Toggle Layout
</Button>
{participantCount < 12 && (
<Button onPress={() => setParticipantCount((count) => count + 1)}>
Add Participant
</Button>
)}
{participantCount > 0 && (
<Button onPress={() => setParticipantCount((count) => count - 1)}>
Remove Participant
</Button>
)}
</div>
<div
style={{
display: "flex",
width: "100vw",
height: "calc(100vh - 32px)",
}}
>
<VideoGrid layout={layout} items={items}>
{({ item, ...rest }) => (
<VideoTile
key={item.id}
name={`User ${item.id}`}
disableSpeakingIndicator={items.length < 3}
connectionState={ConnectionState.Connected}
{...rest}
/>
)}
</VideoGrid>
</div>
</>
);
};
ParticipantsTest.args = {
layout: "freedom",
participantCount: 1,
};

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022-2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,34 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {
ComponentProps,
Key,
Ref,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import React, { Key, useCallback, useEffect, useRef, useState } from "react";
import { FullGestureState, useDrag, useGesture } from "@use-gesture/react";
import {
EventTypes,
FullGestureState,
Handler,
useGesture,
} from "@use-gesture/react";
import {
animated,
SpringRef,
SpringValue,
SpringValues,
useSprings,
} from "@react-spring/web";
import useMeasure from "react-use-measure";
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
import styles from "./VideoGrid.module.css";
import { Layout } from "../room/GridLayoutMenu";
import { TileDescriptor } from "./TileDescriptor";
import { TileWrapper } from "./TileWrapper";
interface TilePosition {
x: number;
@@ -52,7 +39,7 @@ interface TilePosition {
}
interface Tile {
key: string;
key: Key;
order: number;
item: TileDescriptor;
remove: boolean;
@@ -64,7 +51,6 @@ export interface TileSpring {
opacity: number;
scale: number;
shadow: number;
shadowSpread: number;
zIndex: number;
x: number;
y: number;
@@ -186,16 +172,8 @@ function getOneOnOneLayoutTilePositions(
const gridAspectRatio = gridWidth / gridHeight;
const smallPip = gridAspectRatio < 1 || gridWidth < 700;
const maxPipWidth = smallPip ? 114 : 230;
const maxPipHeight = smallPip ? 163 : 155;
// Cap the PiP size at 1/3 the remote tile size, preserving aspect ratio
const pipScaleFactor = Math.min(
1,
remotePosition.width / 3 / maxPipWidth,
remotePosition.height / 3 / maxPipHeight
);
const pipWidth = maxPipWidth * pipScaleFactor;
const pipHeight = maxPipHeight * pipScaleFactor;
const pipWidth = smallPip ? 114 : 230;
const pipHeight = smallPip ? 163 : 155;
const pipGap = getPipGap(gridAspectRatio, gridWidth);
const pipMinX = remotePosition.x + pipGap;
@@ -730,18 +708,20 @@ interface DragTileData {
y: number;
}
export interface ChildrenProperties {
ref: Ref<HTMLElement>;
style: ComponentProps<typeof animated.div>["style"];
/**
* The width this tile will have once its animations have settled.
*/
export interface ChildrenProperties extends ReactDOMAttributes {
key: Key;
targetWidth: number;
/**
* The height this tile will have once its animations have settled.
*/
targetHeight: number;
item: TileDescriptor;
opacity: SpringValue<number>;
scale: SpringValue<number>;
shadow: SpringValue<number>;
zIndex: SpringValue<number>;
x: SpringValue<number>;
y: SpringValue<number>;
width: SpringValue<number>;
height: SpringValue<number>;
[index: string]: unknown;
}
export interface VideoGridProps {
@@ -912,8 +892,6 @@ export function VideoGrid({
// Whether the tile positions were valid at the time of the previous
// animation
const tilePositionsWereValid = tilePositionsValid.current;
const oneOnOneLayout =
tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused);
return (tileIndex: number) => {
const tile = tiles[tileIndex];
@@ -933,14 +911,12 @@ export function VideoGrid({
opacity: 1,
zIndex: 2,
shadow: 15,
shadowSpread: 0,
immediate: (key: string) =>
disableAnimations ||
key === "zIndex" ||
key === "x" ||
key === "y" ||
key === "shadow" ||
key === "shadowSpread",
key === "shadow",
from: {
shadow: 0,
scale: 0,
@@ -998,14 +974,10 @@ export function VideoGrid({
opacity: remove ? 0 : 1,
zIndex: tilePosition.zIndex,
shadow: 1,
shadowSpread: oneOnOneLayout && tile.item.isLocal ? 1 : 0,
from,
reset,
immediate: (key: string) =>
disableAnimations ||
key === "zIndex" ||
key === "shadow" ||
key === "shadowSpread",
disableAnimations || key === "zIndex" || key === "shadow",
// If we just stopped dragging a tile, give it time for the
// animation to settle before pushing its z-index back down
delay: (key: string) => (key === "zIndex" ? 500 : 0),
@@ -1074,132 +1046,117 @@ export function VideoGrid({
[tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
);
// Callback for useDrag. We could call useDrag here, but the default
// pattern of spreading {...bind()} across the children to bind the gesture
// ends up breaking memoization and ruining this component's performance.
// Instead, we pass this callback to each tile via a ref, to let them bind the
// gesture using the much more sensible ref-based method.
const onTileDrag = (
tileId: string,
{
active,
xy,
movement,
tap,
last,
event,
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => {
event.preventDefault();
const bindTile = useDrag(
({ args: [key], active, xy, movement, tap, last, event }) => {
event.preventDefault();
if (tap) {
onTap(tileId);
return;
}
if (layout !== "freedom") return;
const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
const dragTile = tiles[dragTileIndex];
const dragTilePosition = tilePositions[dragTile.order];
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
let newTiles = tiles;
if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable
if (!dragTile.item.isLocal) return;
// Position should only update on the very last event, to avoid
// compounding the offset on every drag event
if (last) {
const remotePosition = tilePositions[1];
const pipGap = getPipGap(
gridBounds.width / gridBounds.height,
gridBounds.width
);
const pipMinX = remotePosition.x + pipGap;
const pipMinY = remotePosition.y + pipGap;
const pipMaxX =
remotePosition.x +
remotePosition.width -
dragTilePosition.width -
pipGap;
const pipMaxY =
remotePosition.y +
remotePosition.height -
dragTilePosition.height -
pipGap;
const newPipXRatio =
(dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
const newPipYRatio =
(dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
if (tap) {
onTap(key);
return;
}
} else {
const hoverTile = tiles.find(
(tile) =>
tile.key !== tileId &&
isInside(cursorPosition, tilePositions[tile.order])
);
if (hoverTile) {
// Shift the tiles into their new order
newTiles = newTiles.map((tile) => {
let order = tile.order;
if (order < dragTile.order) {
if (order >= hoverTile.order) order++;
} else if (order > dragTile.order) {
if (order <= hoverTile.order) order--;
} else {
order = hoverTile.order;
}
if (layout !== "freedom") return;
let focused;
if (tile === hoverTile) {
focused = dragTile.focused;
} else if (tile === dragTile) {
focused = hoverTile.focused;
} else {
focused = tile.focused;
}
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
const dragTile = tiles[dragTileIndex];
const dragTilePosition = tilePositions[dragTile.order];
return { ...tile, order, focused };
});
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
reorderTiles(newTiles, layout);
let newTiles = tiles;
setTileState((state) => ({ ...state, tiles: newTiles }));
}
}
if (tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable
if (!dragTile.item.isLocal) return;
if (active) {
if (!draggingTileRef.current) {
draggingTileRef.current = {
key: dragTile.key,
offsetX: dragTilePosition.x,
offsetY: dragTilePosition.y,
x: movement[0],
y: movement[1],
};
// Position should only update on the very last event, to avoid
// compounding the offset on every drag event
if (last) {
const remotePosition = tilePositions[1];
const pipGap = getPipGap(
gridBounds.width / gridBounds.height,
gridBounds.width
);
const pipMinX = remotePosition.x + pipGap;
const pipMinY = remotePosition.y + pipGap;
const pipMaxX =
remotePosition.x +
remotePosition.width -
dragTilePosition.width -
pipGap;
const pipMaxY =
remotePosition.y +
remotePosition.height -
dragTilePosition.height -
pipGap;
const newPipXRatio =
(dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
const newPipYRatio =
(dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
}
} else {
draggingTileRef.current.x = movement[0];
draggingTileRef.current.y = movement[1];
const hoverTile = tiles.find(
(tile) =>
tile.key !== key &&
isInside(cursorPosition, tilePositions[tile.order])
);
if (hoverTile) {
// Shift the tiles into their new order
newTiles = newTiles.map((tile) => {
let order = tile.order;
if (order < dragTile.order) {
if (order >= hoverTile.order) order++;
} else if (order > dragTile.order) {
if (order <= hoverTile.order) order--;
} else {
order = hoverTile.order;
}
let focused;
if (tile === hoverTile) {
focused = dragTile.focused;
} else if (tile === dragTile) {
focused = hoverTile.focused;
} else {
focused = tile.focused;
}
return { ...tile, order, focused };
});
reorderTiles(newTiles, layout);
setTileState((state) => ({ ...state, tiles: newTiles }));
}
}
} else {
draggingTileRef.current = null;
}
api.start(animate(newTiles));
};
if (active) {
if (!draggingTileRef.current) {
draggingTileRef.current = {
key: dragTile.key,
offsetX: dragTilePosition.x,
offsetY: dragTilePosition.y,
x: movement[0],
y: movement[1],
};
} else {
draggingTileRef.current.x = movement[0];
draggingTileRef.current.y = movement[1];
}
} else {
draggingTileRef.current = null;
}
const onTileDragRef = useRef(onTileDrag);
onTileDragRef.current = onTileDrag;
api.start(animate(newTiles));
},
{ filterTaps: true, pointer: { buttons: [1] } }
);
const onGridGesture = useCallback(
(
@@ -1246,23 +1203,18 @@ export function VideoGrid({
return (
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
{springs.map((spring, i) => {
{springs.map((style, i) => {
const tile = tiles[i];
const tilePosition = tilePositions[tile.order];
return (
<TileWrapper
key={tile.key}
id={tile.key}
onDragRef={onTileDragRef}
targetWidth={tilePosition.width}
targetHeight={tilePosition.height}
item={tile.item}
{...spring}
>
{children}
</TileWrapper>
);
return children({
...bindTile(tile.key),
...style,
key: tile.item.id,
targetWidth: tilePosition.width,
targetHeight: tilePosition.height,
item: tile.item,
});
})}
</div>
);

View File

@@ -18,8 +18,8 @@ limitations under the License.
position: absolute;
contain: strict;
top: 0;
container-name: videoTile;
container-type: size;
width: var(--tileWidth);
height: var(--tileHeight);
--tileRadius: 8px;
border-radius: var(--tileRadius);
overflow: hidden;
@@ -45,7 +45,7 @@ limitations under the License.
transform: scaleX(-1);
}
.videoTile::after {
.videoTile.speaking::after {
position: absolute;
top: -1px;
left: -1px;
@@ -54,12 +54,6 @@ limitations under the License.
content: "";
border-radius: var(--tileRadius);
box-shadow: inset 0 0 0 4px var(--accent) !important;
opacity: 0;
transition: opacity ease 0.15s;
}
.videoTile.speaking::after {
opacity: 1;
}
.videoTile.maximised {
@@ -89,12 +83,6 @@ limitations under the License.
z-index: 1;
}
.infoBubble > svg {
height: 16px;
width: 16px;
margin-right: 4px;
}
.toolbar {
position: absolute;
top: 0;
@@ -138,6 +126,10 @@ limitations under the License.
bottom: 16px;
}
.memberName > * {
margin-right: 6px;
}
.memberName > :last-child {
margin-right: 0px;
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022-2023 New Vector Ltd
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,105 +14,84 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, forwardRef, useCallback } from "react";
import { animated } from "@react-spring/web";
import React, { ForwardedRef, forwardRef } from "react";
import { animated, SpringValue } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import styles from "./VideoTile.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
import { AudioButton, FullscreenButton } from "../button/Button";
import { ConnectionState } from "../room/useGroupCall";
import { TileDescriptor } from "./TileDescriptor";
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
import { useCallFeed } from "./useCallFeed";
import { useSpatialMediaStream } from "./useMediaStream";
import { useRoomMemberName } from "./useRoomMemberName";
import { useModalTriggerState } from "../Modal";
import { useMergedRefs } from "../useMergedRefs";
interface Props {
item: TileDescriptor;
maximised: boolean;
fullscreen: boolean;
onFullscreen: (participant: TileDescriptor) => void;
name: string;
connectionState: ConnectionState;
speaking?: boolean;
audioMuted?: boolean;
videoMuted?: boolean;
screenshare?: boolean;
avatar?: JSX.Element;
mediaRef?: React.RefObject<MediaElement>;
onOptionsPress?: () => void;
localVolume?: number;
hasAudio?: boolean;
maximised?: boolean;
fullscreen?: boolean;
onFullscreen?: () => void;
className?: string;
showSpeakingIndicator: boolean;
style?: ComponentProps<typeof animated.div>["style"];
targetWidth: number;
targetHeight: number;
getAvatar: (
roomMember: RoomMember,
width: number,
height: number
) => JSX.Element;
audioContext: AudioContext;
audioDestination: AudioNode;
showOptions?: boolean;
isLocal?: boolean;
disableSpeakingIndicator?: boolean;
opacity?: SpringValue<number>;
scale?: SpringValue<number>;
shadow?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;
width?: SpringValue<number>;
height?: SpringValue<number>;
}
export const VideoTile = forwardRef<HTMLElement, Props>(
(
{
item,
name,
connectionState,
speaking,
audioMuted,
videoMuted,
screenshare,
avatar,
mediaRef,
onOptionsPress,
localVolume,
hasAudio,
maximised,
fullscreen,
onFullscreen,
className,
showSpeakingIndicator,
style,
targetWidth,
targetHeight,
getAvatar,
audioContext,
audioDestination,
showOptions,
isLocal,
// TODO: disableSpeakingIndicator is not used atm.
disableSpeakingIndicator,
opacity,
scale,
shadow,
zIndex,
x,
y,
width,
height,
...rest
},
tileRef1
ref
) => {
const { t } = useTranslation();
const {
isLocal,
audioMuted,
videoMuted,
localVolume,
hasAudio,
speaking,
stream,
purpose,
} = useCallFeed(item.callFeed);
const screenshare = purpose === SDPStreamMetadataPurpose.Screenshare;
const { rawDisplayName: name } = useRoomMemberName(item.member);
const [tileRef2, mediaRef] = useSpatialMediaStream(
stream ?? null,
audioContext,
audioDestination,
localVolume,
// The feed is muted if it's local audio (because we don't want our own audio,
// but it's a hook and we can't call it conditionally so we're stuck with it)
// or if there's a maximised feed in which case we always render audio via audio
// elements because we wire it up at the video tile container level and only one
// video tile container is displayed.
isLocal || maximised
);
const tileRef = useMergedRefs(tileRef1, tileRef2);
const {
modalState: videoTileSettingsModalState,
modalProps: videoTileSettingsModalProps,
} = useModalTriggerState();
const onOptionsPress = videoTileSettingsModalState.open;
const onFullscreenCallback = useCallback(() => {
onFullscreen(item);
}, [onFullscreen, item]);
const toolbarButtons: JSX.Element[] = [];
if (item.connectionState == ConnectionState.Connected && !isLocal) {
if (connectionState == ConnectionState.Connected && !isLocal) {
if (hasAudio) {
toolbarButtons.push(
<AudioButton
@@ -130,14 +109,14 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
key="fullscreen"
className={styles.button}
fullscreen={fullscreen}
onPress={onFullscreenCallback}
onPress={onFullscreen}
/>
);
}
}
let caption: string;
switch (item.connectionState) {
switch (connectionState) {
case ConnectionState.EstablishingCall:
caption = t("{{name}} (Connecting...)", { name });
break;
@@ -150,65 +129,62 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
break;
}
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
return (
<>
<animated.div
className={classNames(styles.videoTile, className, {
[styles.isLocal]: isLocal,
[styles.speaking]: speaking && showSpeakingIndicator,
[styles.muted]: audioMuted,
[styles.screenshare]: screenshare,
[styles.maximised]: maximised,
})}
style={style}
ref={tileRef}
data-testid="videoTile"
>
{toolbarButtons.length > 0 && !maximised && (
<div className={classNames(styles.toolbar)}>{toolbarButtons}</div>
)}
{videoMuted && (
<>
<div className={styles.videoMutedOverlay} />
{getAvatar(item.member, targetWidth, targetHeight)}
</>
)}
{!maximised &&
(screenshare ? (
<div className={styles.presenterLabel}>
<span>{t("{{name}} is presenting", { name })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>
{
/* If the user is speaking, it's safe to say they're unmuted.
<animated.div
className={classNames(styles.videoTile, className, {
[styles.isLocal]: isLocal,
[styles.speaking]: speaking,
[styles.muted]: audioMuted,
[styles.screenshare]: screenshare,
[styles.maximised]: maximised,
})}
style={{
opacity,
scale,
boxShadow: shadow?.to(
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
),
zIndex,
x,
y,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore React does in fact support assigning custom properties,
// but React's types say no
"--tileWidth": width?.to((w) => `${w}px`),
"--tileHeight": height?.to((h) => `${h}px`),
}}
ref={ref as ForwardedRef<HTMLDivElement>}
{...rest}
>
{toolbarButtons.length > 0 && !maximised && (
<div className={classNames(styles.toolbar)}>{toolbarButtons}</div>
)}
{videoMuted && (
<>
<div className={styles.videoMutedOverlay} />
{avatar}
</>
)}
{!maximised &&
(screenshare ? (
<div className={styles.presenterLabel}>
<span>{t("{{name}} is presenting", { name })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>
{
/* If the user is speaking, it's safe to say they're unmuted.
Mute state is currently sent over to-device messages, which
aren't quite real-time, so this is an important kludge to make
sure no one appears muted when they've clearly begun talking. */
speaking || !audioMuted ? <MicIcon /> : <MicMutedIcon />
}
<span data-testid="videoTile_caption" title={caption}>
{caption}
</span>
</div>
))}
<video
data-testid="videoTile_video"
ref={mediaRef}
playsInline
disablePictureInPicture
/>
</animated.div>
{videoTileSettingsModalState.isOpen && !maximised && item.callFeed && (
<VideoTileSettingsModal
{...videoTileSettingsModalProps}
feed={item.callFeed}
/>
)}
</>
audioMuted && !videoMuted && !speaking && <MicMutedIcon />
}
{videoMuted && <VideoMutedIcon />}
<span title={caption}>{caption}</span>
</div>
))}
<video ref={mediaRef} playsInline disablePictureInPicture />
</animated.div>
);
}
);

View File

@@ -0,0 +1,156 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import React, { FC, memo, RefObject } from "react";
import { useCallback } from "react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { SpringValue } from "@react-spring/web";
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
import { useCallFeed } from "./useCallFeed";
import { useSpatialMediaStream } from "./useMediaStream";
import { useRoomMemberName } from "./useRoomMemberName";
import { VideoTile } from "./VideoTile";
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
import { useModalTriggerState } from "../Modal";
import { TileDescriptor } from "./TileDescriptor";
interface Props {
item: TileDescriptor;
targetWidth: number;
targetHeight: number;
getAvatar: (
roomMember: RoomMember,
width: number,
height: number
) => JSX.Element;
audioContext: AudioContext;
audioDestination: AudioNode;
disableSpeakingIndicator: boolean;
maximised: boolean;
fullscreen: boolean;
onFullscreen: (item: TileDescriptor) => void;
opacity?: SpringValue<number>;
scale?: SpringValue<number>;
shadow?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;
width?: SpringValue<number>;
height?: SpringValue<number>;
onDragRef?: RefObject<
(
tileId: string,
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => void
>;
}
export const VideoTileContainer: FC<Props> = memo(
({
item,
targetWidth,
targetHeight,
getAvatar,
audioContext,
audioDestination,
disableSpeakingIndicator,
maximised,
fullscreen,
onFullscreen,
onDragRef,
...rest
}) => {
const {
isLocal,
audioMuted,
videoMuted,
localVolume,
hasAudio,
speaking,
stream,
purpose,
} = useCallFeed(item.callFeed);
const { rawDisplayName } = useRoomMemberName(item.member);
const [tileRef, mediaRef] = useSpatialMediaStream(
stream ?? null,
audioContext,
audioDestination,
localVolume,
// The feed is muted if it's local audio (because we don't want our own audio,
// but it's a hook and we can't call it conditionally so we're stuck with it)
// or if there's a maximised feed in which case we always render audio via audio
// elements because we wire it up at the video tile container level and only one
// video tile container is displayed.
isLocal || maximised
);
useDrag((state) => onDragRef?.current!(item.id, state), {
target: tileRef,
filterTaps: true,
preventScroll: true,
});
const {
modalState: videoTileSettingsModalState,
modalProps: videoTileSettingsModalProps,
} = useModalTriggerState();
const onOptionsPress = () => {
videoTileSettingsModalState.open();
};
const onFullscreenCallback = useCallback(() => {
onFullscreen(item);
}, [onFullscreen, item]);
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
return (
<>
<VideoTile
isLocal={isLocal}
speaking={speaking && !disableSpeakingIndicator}
audioMuted={audioMuted}
videoMuted={videoMuted}
screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
name={rawDisplayName}
connectionState={item.connectionState}
ref={tileRef}
mediaRef={mediaRef}
avatar={
getAvatar && getAvatar(item.member, targetWidth, targetHeight)
}
onOptionsPress={onOptionsPress}
localVolume={localVolume}
hasAudio={hasAudio}
maximised={maximised}
fullscreen={fullscreen}
onFullscreen={onFullscreenCallback}
{...rest}
/>
{videoTileSettingsModalState.isOpen && !maximised && item.callFeed && (
<VideoTileSettingsModal
{...videoTileSettingsModalProps}
feed={item.callFeed}
/>
)}
</>
);
}
);

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