Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/friendly-url

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner
2023-07-15 09:48:08 +02:00
113 changed files with 2493 additions and 1375 deletions

View File

@@ -25,3 +25,4 @@ LIVEKIT_SECRET="secret"
# VITE_THEME_SYSTEM=#21262c # VITE_THEME_SYSTEM=#21262c
# VITE_THEME_BACKGROUND=#15191e # VITE_THEME_BACKGROUND=#15191e
# VITE_THEME_BACKGROUND_85=#15191ed9 # VITE_THEME_BACKGROUND_85=#15191ed9
# VITE_THEME_SUBTLE_PRIMARY=#26282D

View File

@@ -1,6 +1,16 @@
module.exports = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org"],
extends: ["plugin:matrix-org/react", "plugin:matrix-org/a11y", "prettier"], extends: [
"prettier",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:matrix-org/typescript",
],
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
project: ["./tsconfig.json"],
},
env: { env: {
browser: true, browser: true,
node: true, node: true,

View File

@@ -19,6 +19,6 @@ jobs:
- name: i18n - name: i18n
run: "yarn run i18n:check" run: "yarn run i18n:check"
- name: ESLint - name: ESLint
run: "yarn run lint:js" run: "yarn run lint:eslint"
- name: Type check - name: Type check
run: "yarn run lint:types" run: "yarn run lint:types"

View File

@@ -4,5 +4,6 @@
"base_url": "https://call.ems.host", "base_url": "https://call.ems.host",
"server_name": "call.ems.host" "server_name": "call.ems.host"
} }
} },
"eula": "https://static.element.io/legal/online-EULA.pdf"
} }

View File

@@ -6,7 +6,7 @@
} }
}, },
"livekit": { "livekit": {
"livekit_service_url": "https://livekit.element.dev" "livekit_service_url": "https://livekit-jwt.call.element.dev"
}, },
"posthog": { "posthog": {
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU", "api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",

View File

@@ -9,8 +9,9 @@
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"prettier:check": "prettier -c .", "prettier:check": "prettier -c .",
"prettier:format": "prettier -w .", "prettier:format": "prettier -w .",
"lint": "yarn lint:types && yarn lint:js", "lint": "yarn lint:types && yarn lint:eslint",
"lint:js": "eslint --max-warnings 0 src", "lint:eslint": "eslint --max-warnings 0 src",
"lint:eslint-fix": "eslint --max-warnings 0 src --fix",
"lint:types": "tsc", "lint:types": "tsc",
"i18n": "node_modules/i18next-parser/bin/cli.js", "i18n": "node_modules/i18next-parser/bin/cli.js",
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update", "i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
@@ -19,7 +20,7 @@
}, },
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
"@livekit/components-react": "^1.0.3", "@livekit/components-react": "^1.0.7",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/api": "^1.4.0",
"@opentelemetry/context-zone": "^1.9.1", "@opentelemetry/context-zone": "^1.9.1",
@@ -46,8 +47,10 @@
"@sentry/react": "^6.13.3", "@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3", "@sentry/tracing": "^6.13.3",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
"@types/react-router-dom": "^5.3.3",
"@types/sdp-transform": "^2.4.5", "@types/sdp-transform": "^2.4.5",
"@use-gesture/react": "^10.2.11", "@use-gesture/react": "^10.2.11",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1", "@vitejs/plugin-react": "^4.0.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"color-hash": "^2.0.1", "color-hash": "^2.0.1",
@@ -55,9 +58,9 @@
"i18next": "^21.10.0", "i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8", "i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4", "i18next-http-backend": "^1.4.4",
"livekit-client": "^1.9.7", "livekit-client": "1.12.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#426d29d6b9a9d71a3c0d7fe6f7bac3473cd10832", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
"matrix-widget-api": "^1.3.1", "matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8", "mermaid": "^8.13.8",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
@@ -69,13 +72,15 @@
"react-dom": "18", "react-dom": "18",
"react-i18next": "^11.18.6", "react-i18next": "^11.18.6",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-router": "6",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-use-clipboard": "^1.0.7", "react-use-clipboard": "^1.0.7",
"react-use-measure": "^2.1.1", "react-use-measure": "^2.1.1",
"sdp-transform": "^2.14.1", "sdp-transform": "^2.14.1",
"tinyqueue": "^2.0.3", "tinyqueue": "^2.0.3",
"unique-names-generator": "^4.6.0" "unique-names-generator": "^4.6.0",
"uuid": "9",
"@types/uuid": "9",
"@types/content-type": "^1.1.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.5", "@babel/core": "^7.16.5",

View File

@@ -5,8 +5,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.",
"Audio": "Звук", "Audio": "Звук",
"Avatar": "Аватар", "Avatar": "Аватар",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия</2>",
"Call link copied": "Връзка към разговора бе копирана", "Call link copied": "Връзка към разговора бе копирана",
"Call type menu": "Меню \"тип на разговора\"", "Call type menu": "Меню \"тип на разговора\"",
"Camera": "Камера", "Camera": "Камера",
@@ -75,7 +73,6 @@
"Take me Home": "Отиди в Начало", "Take me Home": "Отиди в Начало",
"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 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>": "Този сайт се предпазва от ReCAPTCHA и важат <2>Политиката за поверителност</2> и <6>Условията за ползване на услугата</6> на Google.<9></9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия</12>",
"Turn off camera": "Изключи камерата", "Turn off camera": "Изключи камерата",
"Turn on camera": "Включи камерата", "Turn on camera": "Включи камерата",
"Unmute microphone": "Включи микрофона", "Unmute microphone": "Включи микрофона",

View File

@@ -62,7 +62,6 @@
"Inspector": "Insepktor", "Inspector": "Insepktor",
"Incompatible versions!": "Nekompatibilní verze!", "Incompatible versions!": "Nekompatibilní verze!",
"Incompatible versions": "Nekompatibilní verze", "Incompatible versions": "Nekompatibilní verze",
"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>": "Tato stárnka je chráněna pomocí ReCAPTCHA a Google <2>zásad ochrany osobních údajů</2> a <6>podmínky služby</6> platí.<9></9>Kliknutím na \"Registrovat\", souhlasíte s <12>Pravidly a podmínkami</12>",
"Walkie-talkie call name": "Jméno vysílačkového hovoru", "Walkie-talkie call name": "Jméno vysílačkového hovoru",
"Walkie-talkie call": "Vysílačkový hovor", "Walkie-talkie call": "Vysílačkový hovor",
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",
@@ -90,8 +89,6 @@
"Create account": "Vytvořit účet", "Create account": "Vytvořit účet",
"Copy": "Kopírovat", "Copy": "Kopírovat",
"Call type menu": "Menu typu hovoru", "Call type menu": "Menu typu hovoru",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions</2>",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.",
"<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>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?</0><1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory </1>", "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?</0><1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory </1>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Připojit se</0><1>Or</1><2>Zkopírovat odkaz a připojit se později</2>", "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Připojit se</0><1>Or</1><2>Zkopírovat odkaz a připojit se později</2>",

View File

@@ -5,8 +5,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.",
"Audio": "Audio", "Audio": "Audio",
"Avatar": "Avatar", "Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Los gehts“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen</2>",
"Call link copied": "Anruflink kopiert", "Call link copied": "Anruflink kopiert",
"Call type menu": "Anruftyp Menü", "Call type menu": "Anruftyp Menü",
"Camera": "Kamera", "Camera": "Kamera",
@@ -74,7 +72,6 @@
"Take me Home": "Zurück zur Startseite", "Take me Home": "Zurück zur Startseite",
"Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.", "Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.",
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?", "This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
"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>": "Diese Website wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6> von Google.<9></9>Indem Du auf „Registrieren“ klickst, stimmst du unseren <12>Geschäftsbedingungen</12> zu",
"Turn off camera": "Kamera ausschalten", "Turn off camera": "Kamera ausschalten",
"Turn on camera": "Kamera einschalten", "Turn on camera": "Kamera einschalten",
"Unmute microphone": "Mikrofon aktivieren", "Unmute microphone": "Mikrofon aktivieren",
@@ -115,5 +112,9 @@
"{{count}} stars|one": "{{count}} Stern", "{{count}} stars|one": "{{count}} Stern",
"<0>Thanks for your feedback!</0>": "<0>Danke für deine Rückmeldung!</0>", "<0>Thanks for your feedback!</0>": "<0>Danke für deine Rückmeldung!</0>",
"{{displayName}} is presenting": "{{displayName}} präsentiert", "{{displayName}} is presenting": "{{displayName}} präsentiert",
"Show connection stats": "Verbindungsstatistiken zeigen" "Show connection stats": "Verbindungsstatistiken zeigen",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Los gehts“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen."
} }

View File

@@ -11,7 +11,7 @@
"Remove": "Αφαίρεση", "Remove": "Αφαίρεση",
"Registering…": "Εγγραφή…", "Registering…": "Εγγραφή…",
"Not registered yet? <2>Create an account</2>": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό</2>", "Not registered yet? <2>Create an account</2>": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό</2>",
"Login to your account": "Συνδεθείτε στο λογαριασμό σας", "Login to your account": "Συνδεθείτε στον λογαριασμό σας",
"Logging in…": "Σύνδεση…", "Logging in…": "Σύνδεση…",
"Invite people": "Προσκαλέστε άτομα", "Invite people": "Προσκαλέστε άτομα",
"Invite": "Πρόσκληση", "Invite": "Πρόσκληση",
@@ -71,5 +71,46 @@
"Close": "Κλείσιμο", "Close": "Κλείσιμο",
"Change layout": "Αλλαγή διάταξης", "Change layout": "Αλλαγή διάταξης",
"Camera": "Κάμερα", "Camera": "Κάμερα",
"Audio": "Ήχος" "Audio": "Ήχος",
"Send debug logs": "Αποστολή αρχείων καταγραφής",
"Recaptcha dismissed": "Το recaptcha απορρίφθηκε",
"<0>Thanks for your feedback!</0>": "<0>Ευχαριστώ για τα σχόλιά σας!</0>",
"Call type menu": "Μενού είδους κλήσης",
"Local volume": "Τοπική ένταση",
"Home": "Αρχική",
"Show connection stats": "Εμφάνιση στατιστικών σύνδεσης",
"Unmute microphone": "Κατάργηση σίγασης μικροφώνου",
"Take me Home": "Μετάβαση στην Αρχική",
"{{displayName}} is presenting": "{{displayName}} παρουσιάζει",
"<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>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Συμμετοχή στην κλήση τώρα</0><1>Or</1><2>Αντιγραφή συνδέσμου κλήσης και συμμετοχή αργότερα</2>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Θα θέλαμε να ακούσουμε τα σχόλιά σας ώστε να βελτιώσουμε την εμπειρία σας.</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>Γιατί να μην ολοκληρώσετε με τη δημιουργία ενός κωδικού πρόσβασης για τη διατήρηση του λογαριασμού σας;</0><1>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.</1>",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ένας άλλος χρήστης σε αυτή την κλήση έχει ένα πρόβλημα. Για την καλύτερη διάγνωση αυτών των προβλημάτων θα θέλαμε να συλλέξουμε ένα αρχείο καταγραφής σφαλμάτων.",
"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>Πολιτική cookies</5>.",
"Grid layout menu": "Μενού διάταξης πλέγματος",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}</1>",
"Thanks! We'll get right on it.": "Ευχαριστούμε! Θα το ερευνήσουμε αμέσως.",
"Expose developer settings in the settings window.": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
"Feedback": "Ανατροφοδότηση",
"Submitting…": "Υποβολή…",
"Thanks, we received your feedback!": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!",
"{{count}} stars|other": "{{count}} αστέρια",
"{{count}} stars|one": "{{count}} αστέρι",
"{{displayName}}, your call has ended.": "{{displayName}}, η κλήση σας τερματίστηκε.",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.</0>",
"How did it go?": "Πώς σας φάνηκε;",
"Include debug logs": "Να συμπεριληφθούν αρχεία καταγραφής",
"Recaptcha not loaded": "Το Recaptcha δεν φορτώθηκε",
"Debug log": "Αρχείο καταγραφής",
"Developer": "Προγραμματιστής",
"Download debug logs": "Λήψη αρχείων καταγραφής",
"Sending debug logs…": "Αποστολή αρχείων καταγραφής…",
"Submit": "Υποβολή",
"Your feedback": "Τα σχόλιά σας",
"Fetching group call timed out.": "Η ομαδική κλήση έληξε από τέλος χρόνου.",
"Freedom": "Ελευθερία",
"Spotlight": "Spotlight",
"Element Call Home": "Element Κεντρική Οθόνη Κλήσεων"
} }

View File

@@ -37,6 +37,7 @@
"Display name": "Display name", "Display name": "Display name",
"Download debug logs": "Download debug logs", "Download debug logs": "Download debug logs",
"Element Call Home": "Element Call Home", "Element Call Home": "Element Call Home",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
"Exit full screen": "Exit full screen", "Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback", "Feedback": "Feedback",

View File

@@ -6,8 +6,6 @@
"Register": "Registrarse", "Register": "Registrarse",
"Not registered yet? <2>Create an account</2>": "¿No estás registrado todavía? <2>Crear una cuenta</2>", "Not registered yet? <2>Create an account</2>": "¿No estás registrado todavía? <2>Crear una cuenta</2>",
"Login to your account": "Iniciar sesión en tu cuenta", "Login to your account": "Iniciar sesión en tu cuenta",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptarás nuestros <2>Términos y condiciones</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones</2>",
"Yes, join call": "Si, unirse a la llamada", "Yes, join call": "Si, unirse a la llamada",
"Walkie-talkie call name": "Nombre de la llamada Walkie-talkie", "Walkie-talkie call name": "Nombre de la llamada Walkie-talkie",
"Walkie-talkie call": "Llamada Walkie-talkie", "Walkie-talkie call": "Llamada Walkie-talkie",
@@ -21,7 +19,6 @@
"Unmute microphone": "Desilenciar el micrófono", "Unmute microphone": "Desilenciar el micrófono",
"Turn on camera": "Encender la cámara", "Turn on camera": "Encender la cámara",
"Turn off camera": "Apagar la cámara", "Turn off camera": "Apagar la cámara",
"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>": "Este sitio está protegido por ReCAPTCHA y se aplica <2>la Política de Privacidad</2> y <6>los Términos de Servicio</6> de Google.<9></9>Al hacer clic en \"Registrar\" aceptarás nuestros <12>Términos y condiciones</12>",
"Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.", "Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.",
"Take me Home": "Volver al inicio", "Take me Home": "Volver al inicio",
"Submit feedback": "Enviar comentarios", "Submit feedback": "Enviar comentarios",
@@ -101,5 +98,23 @@
"Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.", "Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
"Developer Settings": "Ajustes de desarrollador", "Developer Settings": "Ajustes de desarrollador",
"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>.", "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." "<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.",
"{{displayName}} is presenting": "{{displayName}} está presentando",
"<0>Thanks for your feedback!</0>": "<0>¡Gracias por tus comentarios!</0>",
"How did it go?": "¿Cómo ha ido?",
"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>End User Licensing Agreement (EULA)</12>": "Este sitio está protegido por ReCAPTCHA y se aplican la <2>Política de Privacidad</2> y los <6>Términos de Servicio de Google.<9></9>Al hacer clic en \"Registrar\", aceptas nuestro <12>Contrato de Licencia de Usuario Final (CLUF)</12>",
"Show connection stats": "Mostrar estadísticas de conexión",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call no está encriptado de extremo a extremo de manera temporal mientras probamos la escalabilidad del servicio.",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Al hacer clic en \"Comenzar\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
"Thanks, we received your feedback!": "¡Gracias, hemos recibido tus comentarios!",
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Nos encantaría conocer tu opinión para que podamos mejorar tu experiencia</0>",
"Feedback": "Danos tu opinión",
"Submit": "Enviar",
"{{count}} stars|one": "{{count}} estrella",
"{{count}} stars|other": "{{count}} estrellas",
"{{displayName}}, your call has ended.": "{{displayName}}, tu llamada ha finalizado.",
"Submitting…": "Enviando…",
"Your feedback": "Tus comentarios"
} }

View File

@@ -32,14 +32,12 @@
"Camera": "Kaamera", "Camera": "Kaamera",
"Call type menu": "Kõnetüübi valik", "Call type menu": "Kõnetüübi valik",
"Call link copied": "Kõne link on kopeeritud", "Call link copied": "Kõne link on kopeeritud",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Liitu kõnega“nõustud sa meie <2>kasutustingimustega</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega</2>",
"Avatar": "Tunnuspilt", "Avatar": "Tunnuspilt",
"Audio": "Heli", "Audio": "Heli",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ühel teisel selles kõnes osalejal on lahenduse kasutamisel tekkinud probleem ning selle põhjuse leidmiseks me sooviksime koguda silumislogisid.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ühel teisel selles kõnes osalejal on lahenduse kasutamisel tekkinud probleem ning selle põhjuse leidmiseks me sooviksime koguda silumislogisid.",
"Passwords must match": "Salasõnad ei klapi", "Passwords must match": "Salasõnad ei klapi",
"Password": "Salasõna", "Password": "Salasõna",
"Not registered yet? <2>Create an account</2>": "Pole veel registreerunud? <2>Loo kasutajakonto</2>", "Not registered yet? <2>Create an account</2>": "Sa pole veel registreerunud? <2>Loo kasutajakonto</2>",
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele", "Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
"No": "Ei", "No": "Ei",
"Mute microphone": "Summuta mikrofon", "Mute microphone": "Summuta mikrofon",
@@ -53,7 +51,7 @@
"Loading…": "Laadimine …", "Loading…": "Laadimine …",
"Leave": "Lahku", "Leave": "Lahku",
"Join existing call?": "Liitu juba käimasoleva kõnega?", "Join existing call?": "Liitu juba käimasoleva kõnega?",
"Join call now": "Kõnega liitumine", "Join call now": "Liitu kõnega kohe",
"Join call": "Kõnega liitumine", "Join call": "Kõnega liitumine",
"Turn on camera": "Lülita kaamera sisse", "Turn on camera": "Lülita kaamera sisse",
"Turn off camera": "Lülita kaamera välja", "Turn off camera": "Lülita kaamera välja",
@@ -93,7 +91,6 @@
"Walkie-talkie call": "Walkie-talkie stiilis kõne", "Walkie-talkie call": "Walkie-talkie stiilis kõne",
"Walkie-talkie call name": "Walkie-talkie stiilis kõne nimi", "Walkie-talkie call name": "Walkie-talkie stiilis kõne nimi",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.", "WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.",
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>Terms and conditions</12>": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika</2> ja <6>teenusetingimused</6>.<9></9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega</12>",
"Element Call Home": "Element Call Home", "Element Call Home": "Element Call Home",
"Copy": "Kopeeri", "Copy": "Kopeeri",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.</0>", "<0>Submitting debug logs will help us track down the problem.</0>": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.</0>",
@@ -115,5 +112,9 @@
"<0>Thanks for your feedback!</0>": "<0>Täname Sind tagasiside eest!</0>", "<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>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>",
"Show connection stats": "Näita ühenduse statistikat", "Show connection stats": "Näita ühenduse statistikat",
"{{displayName}} is presenting": "{{displayName}} on esitlemas" "{{displayName}} is presenting": "{{displayName}} on esitlemas",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika</2> ning <6>Teenusetingimused</6>.<9></9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Seni kuni me testime skaleeritavust, siis Element Call ajutiselt pole läbivalt krüptitud."
} }

View File

@@ -45,8 +45,6 @@
"Camera": "دوربین", "Camera": "دوربین",
"Call type menu": "منوی نوع تماس", "Call type menu": "منوی نوع تماس",
"Call link copied": "لینک تماس کپی شد", "Call link copied": "لینک تماس کپی شد",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "با کلیک بر روی پیوستن به تماس، شما با <2>شرایط و قوانین استفاده</2> موافقت می‌کنید",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "با کلیک بر روی برو، شما با <2>شرایط و قوانین استفاده</2> موافقت می‌کنید",
"Avatar": "آواتار", "Avatar": "آواتار",
"Audio": "صدا", "Audio": "صدا",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیب‌یابی را جمع‌آوری کنیم.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیب‌یابی را جمع‌آوری کنیم.",
@@ -88,7 +86,6 @@
"Version: {{version}}": "نسخه: {{نسخه}}", "Version: {{version}}": "نسخه: {{نسخه}}",
"User menu": "فهرست کاربر", "User menu": "فهرست کاربر",
"Unmute microphone": "ناخموشی میکروفون", "Unmute microphone": "ناخموشی میکروفون",
"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>": "این سایت توسط ReCAPTCHA محافظت می شود و <2>خط مشی رازداری</2> و <6>شرایط خدمات</6> Google اعمال می شود.<9></9>با کلیک کردن بر روی \"ثبت نام\"، شما با <12 >شرایط و ضوابط </12> ما موافقت می کنید",
"This call already exists, would you like to join?": "این تماس از قبل وجود دارد، می‌خواهید بپیوندید؟", "This call already exists, would you like to join?": "این تماس از قبل وجود دارد، می‌خواهید بپیوندید؟",
"Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.", "Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.",
"Submit feedback": "بازخورد ارائه دهید", "Submit feedback": "بازخورد ارائه دهید",

View File

@@ -4,8 +4,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.",
"Audio": "Audio", "Audio": "Audio",
"Avatar": "Avatar", "Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Commencer » vous acceptez nos <2>conditions dutilisation</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Rejoindre lappel » vous acceptez nos <2>conditions dutilisation</2>",
"Call link copied": "Lien de lappel copié", "Call link copied": "Lien de lappel copié",
"Call type menu": "Menu de type dappel", "Call type menu": "Menu de type dappel",
"Camera": "Caméra", "Camera": "Caméra",
@@ -88,7 +86,6 @@
"Unmute microphone": "Allumer le micro", "Unmute microphone": "Allumer le micro",
"Turn on camera": "Allumer la caméra", "Turn on camera": "Allumer la caméra",
"Turn off camera": "Couper la caméra", "Turn off camera": "Couper la caméra",
"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>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions dutilisation</6> de Google sappliquent.<9></9>En cliquant sur « Senregistrer » vous acceptez également nos <12>conditions dutilisation</12>",
"Speaker": "Intervenant", "Speaker": "Intervenant",
"Invite": "Inviter", "Invite": "Inviter",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>", "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
@@ -115,5 +112,9 @@
"<0>Thanks for your feedback!</0>": "<0>Merci pour votre commentaire !</0>", "<0>Thanks for your feedback!</0>": "<0>Merci pour votre commentaire !</0>",
"How did it go?": "Comment cela sest-il passé ?", "How did it go?": "Comment cela sest-il passé ?",
"{{displayName}} is presenting": "{{displayName}} est à lécran", "{{displayName}} is presenting": "{{displayName}} est à lécran",
"Show connection stats": "Afficher les statistiques de la connexion" "Show connection stats": "Afficher les statistiques de la connexion",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "En cliquant sur « Rejoindre lappel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "En cliquant sur « Commencer », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions dutilisation</6> de Google sappliquent.<9></9>En cliquant sur « Senregistrer » vous acceptez également notre <12>Contrat de Licence Utilisateur Final (CLUF)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nest temporairement plus chiffré de bout en bout le temps de tester lextensibilité."
} }

View File

@@ -5,8 +5,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.",
"Audio": "Audio", "Audio": "Audio",
"Avatar": "Avatar", "Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan</2> kami",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan</2> kami",
"Call link copied": "Tautan panggilan disalin", "Call link copied": "Tautan panggilan disalin",
"Call type menu": "Menu jenis panggilan", "Call type menu": "Menu jenis panggilan",
"Camera": "Kamera", "Camera": "Kamera",
@@ -75,7 +73,6 @@
"Take me Home": "Bawa saya ke Beranda", "Take me Home": "Bawa saya ke Beranda",
"Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.", "Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.",
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?", "This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
"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>": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan</12> kami",
"Turn off camera": "Matikan kamera", "Turn off camera": "Matikan kamera",
"Turn on camera": "Nyalakan kamera", "Turn on camera": "Nyalakan kamera",
"Unmute microphone": "Suarakan mikrofon", "Unmute microphone": "Suarakan mikrofon",
@@ -93,7 +90,7 @@
"Your recent calls": "Panggilan Anda terkini", "Your recent calls": "Panggilan Anda terkini",
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…", "Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>", "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung ke panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
"Element Call Home": "Beranda Element Call", "Element Call Home": "Beranda Element Call",
"Copy": "Salin", "Copy": "Salin",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.</0>", "<0>Submitting debug logs will help us track down the problem.</0>": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.</0>",
@@ -105,7 +102,7 @@
"Feedback": "Masukan", "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.", "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", "Submit": "Kirim",
"Submitting…": "Mengirim", "Submitting…": "Mengirim",
"Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!", "Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!",
"Your feedback": "Masukan Anda", "Your feedback": "Masukan Anda",
"{{displayName}}, your call has ended.": "{{displayName}}, panggilan Anda telah berakhir.", "{{displayName}}, your call has ended.": "{{displayName}}, panggilan Anda telah berakhir.",
@@ -115,5 +112,9 @@
"<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>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>",
"Show connection stats": "Tampilkan statistik koneksi", "Show connection stats": "Tampilkan statistik koneksi",
"{{displayName}} is presenting": "{{displayName}} sedang menampilkan", "{{displayName}} is presenting": "{{displayName}} sedang menampilkan",
"{{count}} stars|other": "{{count}} bintang" "{{count}} stars|other": "{{count}} bintang",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Situs ini dilindungi oleh reCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9></9>Dengan mengeklik \"Daftar\", Anda menyetujui <12>Perjanjian Lisensi Pengguna Akhir (EULA)</12> kami",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call sementara tidak dienkripsi secara ujung ke ujung selagi kami menguji skalabilitas."
} }

View File

@@ -5,8 +5,6 @@
"<0>Oops, something's gone wrong.</0>": "<0>何かがうまく行きませんでした。</0>", "<0>Oops, something's gone wrong.</0>": "<0>何かがうまく行きませんでした。</0>",
"Camera": "カメラ", "Camera": "カメラ",
"Call link copied": "通話リンクをコピーしました", "Call link copied": "通話リンクをコピーしました",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "「今すぐ通話に参加」をクリックすると、<2>利用規約</2>に同意したとみなされます",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "「続行」をクリックすると、 <2>利用規約</2>に同意したとみなされます",
"Avatar": "アバター", "Avatar": "アバター",
"Audio": "音声", "Audio": "音声",
"Confirm password": "パスワードを確認", "Confirm password": "パスワードを確認",

View File

@@ -1,7 +1,6 @@
{ {
"Login": "Zaloguj się", "Login": "Zaloguj się",
"Go": "Przejdź", "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>",
"Your recent calls": "Twoje ostatnie połączenia", "Your recent calls": "Twoje ostatnie połączenia",
"Yes, join call": "Tak, dołącz do połączenia", "Yes, join call": "Tak, dołącz do połączenia",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC jest niewspierane lub zablokowane w tej przeglądarce.", "WebRTC is not supported or is being blocked in this browser.": "WebRTC jest niewspierane lub zablokowane w tej przeglądarce.",
@@ -84,7 +83,6 @@
"Camera": "Kamera", "Camera": "Kamera",
"Call type menu": "Menu typu połączenia", "Call type menu": "Menu typu połączenia",
"Call link copied": "Skopiowano link do 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>",
"Avatar": "Awatar", "Avatar": "Awatar",
"Audio": "Dźwięk", "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.", "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.",
@@ -99,7 +97,6 @@
"Expose developer settings in the settings window.": "Wyświetl opcje programisty w oknie ustawień.", "Expose developer settings in the settings window.": "Wyświetl opcje programisty w oknie ustawień.",
"Element Call Home": "Strona główna Element Call", "Element Call Home": "Strona główna Element Call",
"Developer Settings": "Opcje programisty", "Developer Settings": "Opcje programisty",
"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.", "<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>.", "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.", "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.",

View File

@@ -4,7 +4,6 @@
"Logging in…": "Вход…", "Logging in…": "Вход…",
"{{names}}, {{name}}": "{{names}}, {{name}}", "{{names}}, {{name}}": "{{names}}, {{name}}",
"Waiting for other participants…": "Ожидание других участников…", "Waiting for other participants…": "Ожидание других участников…",
"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>": "Этот сайт защищён ReCAPTCHA от Google, ознакомьтесь с их <2>Политикой конфиденциальности</2> и <6>Пользовательским соглашением</6>.<9></9>Нажимая \"Зарегистрироваться\", вы также принимаете наши <12>Положения и условия</12>.",
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?", "This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.", "Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
"Submit feedback": "Отправить отзыв", "Submit feedback": "Отправить отзыв",
@@ -12,8 +11,6 @@
"Select an option": "Выберите вариант", "Select an option": "Выберите вариант",
"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>", "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>",
"Grid layout menu": "Меню \"Расположение сеткой\"", "Grid layout menu": "Меню \"Расположение сеткой\"",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Присоединиться сейчас\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>", "<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>", "<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>", "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>",

View File

@@ -65,7 +65,6 @@
"Unmute microphone": "Zrušiť stlmenie mikrofónu", "Unmute microphone": "Zrušiť stlmenie mikrofónu",
"Turn on camera": "Zapnúť kameru", "Turn on camera": "Zapnúť kameru",
"Turn off camera": "Vypnúť kameru", "Turn off camera": "Vypnúť kameru",
"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>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov</2> a <6>Podmienky poskytovania služieb</6> spoločnosti Google.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" vyjadrujete súhlas s našimi <12>Podmienkami poskytovania služieb</12>",
"This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?", "This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?",
"Speaker": "Reproduktor", "Speaker": "Reproduktor",
"Sign out": "Odhlásiť sa", "Sign out": "Odhlásiť sa",
@@ -86,8 +85,6 @@
"Camera": "Kamera", "Camera": "Kamera",
"Call type menu": "Ponuka typu hovoru", "Call type menu": "Ponuka typu hovoru",
"Call link copied": "Odkaz na hovor skopírovaný", "Call link copied": "Odkaz na hovor skopírovaný",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Kliknutím na \"Pripojiť sa k hovoru\" súhlasíte s našimi <2>Podmienkami</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Kliknutím na tlačidlo \"Prejsť\" súhlasíte s našimi <2>Podmienkami</2>",
"Avatar": "Obrázok", "Avatar": "Obrázok",
"Audio": "Audio", "Audio": "Audio",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.",
@@ -115,5 +112,9 @@
"<0>Thanks for your feedback!</0>": "<0> Ďakujeme za vašu spätnú väzbu!</0>", "<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>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>",
"{{displayName}} is presenting": "{{displayName}} prezentuje", "{{displayName}} is presenting": "{{displayName}} prezentuje",
"Show connection stats": "Zobraziť štatistiky pripojenia" "Show connection stats": "Zobraziť štatistiky pripojenia",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google</2> a <6>Podmienky poskytovania služieb</6>.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" súhlasíte s našou <12>Licenčnou zmluvou s koncovým používateľom (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nie je dočasne šifrovaný, kým testujeme škálovateľnosť."
} }

View File

@@ -3,8 +3,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.",
"Audio": "Ses", "Audio": "Ses",
"Avatar": "Avatar", "Avatar": "Avatar",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "\"Git\"e tıklayarak,<2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
"Call link copied": "Arama bağlantısı kopyalandı", "Call link copied": "Arama bağlantısı kopyalandı",
"Call type menu": "Arama tipi menüsü", "Call type menu": "Arama tipi menüsü",
"Camera": "Kamera", "Camera": "Kamera",

View File

@@ -15,7 +15,6 @@
"Unmute microphone": "Увімкнути мікрофон", "Unmute microphone": "Увімкнути мікрофон",
"Turn on camera": "Увімкнути камеру", "Turn on camera": "Увімкнути камеру",
"Turn off camera": "Вимкнути камеру", "Turn off camera": "Вимкнути камеру",
"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>": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи кнопку «Зареєструватися», ви погоджуєтеся з нашими <12>Умовами та положеннями</12>",
"This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?", "This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?",
"Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.", "Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.",
"Take me Home": "Перейти до Домівки", "Take me Home": "Перейти до Домівки",
@@ -84,8 +83,6 @@
"Camera": "Камера", "Camera": "Камера",
"Call type menu": "Меню типу виклику", "Call type menu": "Меню типу виклику",
"Call link copied": "Посилання на виклик скопійовано", "Call link copied": "Посилання на виклик скопійовано",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Приєднатися до виклику зараз», ви погодитеся з нашими <2>Умовами та положеннями</2>",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натиснувши «Далі», ви погодитеся з нашими <2>Умовами та положеннями</2>",
"Avatar": "Аватар", "Avatar": "Аватар",
"Audio": "Звук", "Audio": "Звук",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал налагодження.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал налагодження.",
@@ -115,5 +112,9 @@
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.</0>", "<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.</0>",
"How did it go?": "Вам усе сподобалось?", "How did it go?": "Вам усе сподобалось?",
"{{displayName}} is presenting": "{{displayName}} представляє", "{{displayName}} is presenting": "{{displayName}} представляє",
"Show connection stats": "Показати стан з'єднання" "Show connection stats": "Показати стан з'єднання",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Натискаючи \"Далі\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи \"Зареєструватися\", ви погоджуєтеся з нашою <12>Ліцензійною угодою з кінцевим користувачем (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Виклики Element тимчасово не захищаються наскрізним шифруванням, поки ми тестуємо масштабованість."
} }

View File

@@ -76,7 +76,6 @@
"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 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?",
"Recaptcha not loaded": "Chưa tải được Recaptcha", "Recaptcha not loaded": "Chưa tải được Recaptcha",
"Debug log request": "Yêu cầu nhật ký gỡ lỗi", "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.", "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.",
"<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>", "<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>",
"<0>Oops, something's gone wrong.</0>": "<0>Ối, có cái gì đó sai.</0>", "<0>Oops, something's gone wrong.</0>": "<0>Ối, có cái gì đó sai.</0>",

View File

@@ -14,7 +14,6 @@
"Unmute microphone": "取消麦克风静音", "Unmute microphone": "取消麦克风静音",
"Turn on camera": "开启摄像头", "Turn on camera": "开启摄像头",
"Turn off camera": "关闭摄像头", "Turn off camera": "关闭摄像头",
"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>": "本网站受reCaptcha保护并适用Google<2>隐私政策</2>和<6>服务条款</6>。<9></9>点击\"注册\"则表明您同意我们的<12>条款和条件</12>",
"This call already exists, would you like to join?": "该通话已存在,你想加入吗?", "This call already exists, would you like to join?": "该通话已存在,你想加入吗?",
"Thanks! We'll get right on it.": "谢谢!我们会马上去做的。", "Thanks! We'll get right on it.": "谢谢!我们会马上去做的。",
"Take me Home": "返回主页", "Take me Home": "返回主页",
@@ -89,12 +88,10 @@
"Copied!": "已复制!", "Copied!": "已复制!",
"Confirm password": "确认密码", "Confirm password": "确认密码",
"Close": "关闭", "Close": "关闭",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "点击开始则代表同意我们的<2>条款和条件<2>",
"Change layout": "更改布局", "Change layout": "更改布局",
"Camera": "摄像头", "Camera": "摄像头",
"Call type menu": "通话类型菜单", "Call type menu": "通话类型菜单",
"Call link copied": "链接已复制", "Call link copied": "链接已复制",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "点击“现在加入”则表示同意我们的<2>条款与条件<2>",
"Avatar": "头像", "Avatar": "头像",
"<0>Oops, something's gone wrong.</0>": "<0>哎哟,出问题了。</0>", "<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.": "" "<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.": ""

View File

@@ -22,7 +22,6 @@
"Unmute microphone": "取消麥克風靜音", "Unmute microphone": "取消麥克風靜音",
"Turn on camera": "開啟相機", "Turn on camera": "開啟相機",
"Turn off camera": "關閉相機", "Turn off camera": "關閉相機",
"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>": "此網站使用Google 驗證碼技術保護,適用<2>隱私條款</2> 與<6>條款與細則</6> 。<9></9>按下「註冊」,表示您同意我們的<12>條款與細則</12>",
"This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?", "This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?",
"Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。", "Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。",
"Take me Home": "帶我回主畫面", "Take me Home": "帶我回主畫面",
@@ -94,8 +93,6 @@
"Camera": "相機", "Camera": "相機",
"Call type menu": "通話類型選單", "Call type menu": "通話類型選單",
"Call link copied": "已複製通話連結", "Call link copied": "已複製通話連結",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "當您按下「加入通話」,您也同時同意了我們的條款與細則",
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "當您按下「前往」,你也同意了我們的條款與細則",
"Avatar": "大頭照", "Avatar": "大頭照",
"Audio": "語音", "Audio": "語音",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。",
@@ -115,5 +112,9 @@
"{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。", "{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。",
"How did it go?": "進展如何?", "How did it go?": "進展如何?",
"{{displayName}} is presenting": "{{displayName}} 正在展示", "{{displayName}} is presenting": "{{displayName}} 正在展示",
"Show connection stats": "顯示連線統計資料" "Show connection stats": "顯示連線統計資料",
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "點擊「前往」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
"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>End User Licensing Agreement (EULA)</12>": "此網站被 ReCAPTCHA 保護,並適用 Google 的<2>隱私權政策</2>與<6>服務條款</6>。<9></9>點擊「註冊」即表示您同意我們的<12>終端使用者授權協議 (EULA)</12>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "在我們測試可擴展性時Element Call 暫時未進行端到端加密。"
} }

View File

@@ -18,6 +18,7 @@ import { Suspense, useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { OverlayProvider } from "@react-aria/overlays"; import { OverlayProvider } from "@react-aria/overlays";
import { History } from "history";
import { HomePage } from "./home/HomePage"; import { HomePage } from "./home/HomePage";
import { LoginPage } from "./auth/LoginPage"; import { LoginPage } from "./auth/LoginPage";
@@ -50,6 +51,8 @@ export default function App({ history }: AppProps) {
const errorPage = <CrashView />; const errorPage = <CrashView />;
return ( return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Router history={history}> <Router history={history}>
{loaded ? ( {loaded ? (
<Suspense fallback={null}> <Suspense fallback={null}>

View File

@@ -16,7 +16,6 @@ limitations under the License.
import { useMemo, CSSProperties, HTMLAttributes, FC } from "react"; import { useMemo, CSSProperties, HTMLAttributes, FC } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { getAvatarUrl } from "./matrix-utils"; import { getAvatarUrl } from "./matrix-utils";
import { useClient } from "./ClientContext"; import { useClient } from "./ClientContext";
@@ -59,9 +58,6 @@ function hashStringToArrIndex(str: string, arrLength: number) {
return sum % arrLength; return sum % arrLength;
} }
const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
interface Props extends HTMLAttributes<HTMLDivElement> { interface Props extends HTMLAttributes<HTMLDivElement> {
bgKey?: string; bgKey?: string;
src?: string; src?: string;
@@ -99,10 +95,10 @@ export const Avatar: FC<Props> = ({
[size] [size]
); );
const resolvedSrc = useMemo( const resolvedSrc = useMemo(() => {
() => resolveAvatarSrc(client, src, sizePx), if (!client || !src || !sizePx) return undefined;
[client, src, sizePx] return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
); }, [client, src, sizePx]);
const backgroundColor = useMemo(() => { const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex( const index = hashStringToArrIndex(

22
src/Banner.module.css Normal file
View File

@@ -0,0 +1,22 @@
/*
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 {
flex: 1;
border-radius: 8px;
padding: 16px;
background-color: var(--subtle-primary);
}

27
src/Banner.tsx Normal file
View File

@@ -0,0 +1,27 @@
/*
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 { ReactNode } from "react";
import styles from "./Banner.module.css";
interface Props {
children: ReactNode;
}
export const Banner = ({ children }: Props) => {
return <div className={styles.banner}>{children}</div>;
};

View File

@@ -20,9 +20,9 @@ import {
useEffect, useEffect,
useState, useState,
createContext, createContext,
useMemo,
useContext, useContext,
useRef, useRef,
useMemo,
} from "react"; } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
@@ -31,9 +31,9 @@ import { useTranslation } from "react-i18next";
import { ErrorView } from "./FullScreenView"; import { ErrorView } from "./FullScreenView";
import { import {
initClient,
CryptoStoreIntegrityError, CryptoStoreIntegrityError,
fallbackICEServerAllowed, fallbackICEServerAllowed,
initClient,
} from "./matrix-utils"; } from "./matrix-utils";
import { widget } from "./widget"; import { widget } from "./widget";
import { import {
@@ -47,140 +47,313 @@ import { Config } from "./config/Config";
declare global { declare global {
interface Window { interface Window {
matrixclient: MatrixClient; matrixclient: MatrixClient;
isPasswordlessUser: boolean; passwordlessUser: boolean;
} }
} }
export interface Session { export type ClientState = ValidClientState | ErrorState;
user_id: string;
device_id: string; export type ValidClientState = {
access_token: string; state: "valid";
authenticated?: AuthenticatedClient;
setClient: (params?: SetClientParams) => void;
};
export type AuthenticatedClient = {
client: MatrixClient;
isPasswordlessUser: boolean;
changePassword: (password: string) => Promise<void>;
logout: () => void;
};
export type ErrorState = {
state: "error";
error: Error;
};
export type SetClientParams = {
client: MatrixClient;
session: Session;
};
const ClientContext = createContext<ClientState | undefined>(undefined);
export const useClientState = () => useContext(ClientContext);
export function useClient(): {
client?: MatrixClient;
setClient?: (params?: SetClientParams) => void;
} {
let client;
let setClient;
const clientState = useClientState();
if (clientState?.state === "valid") {
client = clientState.authenticated?.client;
setClient = clientState.setClient;
}
return { client, setClient };
}
// Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields.
export function useClientLegacy(): {
client?: MatrixClient;
setClient?: (params?: SetClientParams) => void;
passwordlessUser: boolean; passwordlessUser: boolean;
tempPassword?: string; loading: boolean;
authenticated: boolean;
logout?: () => void;
error?: Error;
} {
const clientState = useClientState();
let client;
let setClient;
let passwordlessUser = false;
let loading = true;
let error;
let authenticated = false;
let logout;
if (clientState?.state === "valid") {
client = clientState.authenticated?.client;
setClient = clientState.setClient;
passwordlessUser = clientState.authenticated?.isPasswordlessUser ?? false;
loading = false;
authenticated = client !== undefined;
logout = clientState.authenticated?.logout;
} else if (clientState?.state === "error") {
error = clientState.error;
loading = false;
}
return {
client,
setClient,
passwordlessUser,
loading,
authenticated,
logout,
error,
};
} }
const loadChannel = const loadChannel =
"BroadcastChannel" in window ? new BroadcastChannel("load") : null; "BroadcastChannel" in window ? new BroadcastChannel("load") : null;
const loadSession = (): Session => {
const data = localStorage.getItem("matrix-auth-store");
if (data) return JSON.parse(data);
return null;
};
const saveSession = (session: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
const clearSession = () => localStorage.removeItem("matrix-auth-store");
interface ClientState {
loading: boolean;
isAuthenticated: boolean;
isPasswordlessUser: boolean;
client: MatrixClient;
userName: string;
changePassword: (password: string) => Promise<void>;
logout: () => void;
setClient: (client: MatrixClient, session: Session) => void;
error?: Error;
}
const ClientContext = createContext<ClientState>(null);
type ClientProviderState = Omit<
ClientState,
"changePassword" | "logout" | "setClient"
> & { error?: Error };
interface Props { interface Props {
children: JSX.Element; children: JSX.Element;
} }
export const ClientProvider: FC<Props> = ({ children }) => { export const ClientProvider: FC<Props> = ({ children }) => {
const history = useHistory(); const history = useHistory();
const initializing = useRef(false);
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState,
] = useState<ClientProviderState>({
loading: true,
isAuthenticated: false,
isPasswordlessUser: false,
client: undefined,
userName: null,
error: undefined,
});
const [initClientState, setInitClientState] = useState<
InitResult | undefined
>(undefined);
const initializing = useRef(false);
useEffect(() => { useEffect(() => {
// In case the component is mounted, unmounted, and remounted quickly (as // 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 // React does in strict mode), we need to make sure not to doubly initialize
// the client // the client.
if (initializing.current) return; if (initializing.current) return;
initializing.current = true; initializing.current = true;
const init = async (): Promise< loadClient()
Pick<ClientProviderState, "client" | "isPasswordlessUser"> .then((maybeClient) => {
> => { if (!maybeClient) {
logger.error("Failed to initialize client");
return;
}
setInitClientState(maybeClient);
})
.catch((err) => logger.error(err))
.finally(() => (initializing.current = false));
}, []);
const changePassword = useCallback(
async (password: string) => {
const session = loadSession();
if (!initClientState?.client || !session) {
return;
}
await initClientState.client.setPassword(
{
type: "m.login.password",
identifier: {
type: "m.id.user",
user: session.user_id,
},
user: session.user_id,
password: session.tempPassword,
},
password
);
saveSession({ ...session, passwordlessUser: false });
setInitClientState({
client: initClientState.client,
passwordlessUser: false,
});
},
[initClientState?.client]
);
const setClient = useCallback(
(clientParams?: SetClientParams) => {
const oldClient = initClientState?.client;
const newClient = clientParams?.client;
if (oldClient && oldClient !== newClient) {
oldClient.stopClient();
}
if (clientParams) {
saveSession(clientParams.session);
setInitClientState({
client: clientParams.client,
passwordlessUser: clientParams.session.passwordlessUser,
});
} else {
clearSession();
setInitClientState(undefined);
}
},
[initClientState?.client]
);
const logout = useCallback(async () => {
const client = initClientState?.client;
if (!client) {
return;
}
await client.logout(true);
await client.clearStores();
clearSession();
setInitClientState(undefined);
history.push("/");
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
}, [history, initClientState?.client]);
const { t } = useTranslation();
// To protect against multiple sessions writing to the same storage
// simultaneously, we send a broadcast message that shuts down all other
// running instances of the app. This isn't necessary if the app is running in
// a widget though, since then it'll be mostly stateless.
useEffect(() => {
if (!widget) loadChannel?.postMessage({});
}, []);
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
undefined
);
useEventTarget(
loadChannel,
"message",
useCallback(() => {
initClientState?.client.stopClient();
setAlreadyOpenedErr(
translatedError("This application has been opened in another tab.", t)
);
}, [initClientState?.client, setAlreadyOpenedErr, t])
);
const state: ClientState = useMemo(() => {
if (alreadyOpenedErr) {
return { state: "error", error: alreadyOpenedErr };
}
let authenticated = undefined;
if (initClientState) {
authenticated = {
client: initClientState.client,
isPasswordlessUser: initClientState.passwordlessUser,
changePassword,
logout,
};
}
return { state: "valid", authenticated, setClient };
}, [alreadyOpenedErr, changePassword, initClientState, logout, setClient]);
useEffect(() => {
if (!initClientState) {
return;
}
window.matrixclient = initClientState.client;
window.passwordlessUser = initClientState.passwordlessUser;
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
}, [initClientState]);
if (alreadyOpenedErr) {
return <ErrorView error={alreadyOpenedErr} />;
}
return (
<ClientContext.Provider value={state}>{children}</ClientContext.Provider>
);
};
type InitResult = {
client: MatrixClient;
passwordlessUser: boolean;
};
async function loadClient(): Promise<InitResult> {
if (widget) { if (widget) {
// We're inside a widget, so let's engage *matryoshka mode* // We're inside a widget, so let's engage *matryoshka mode*
logger.log("Using a matryoshka client"); logger.log("Using a matryoshka client");
const client = await widget.client;
return { return {
client: await widget.client, client,
isPasswordlessUser: false, passwordlessUser: false,
}; };
} else { } else {
// We're running as a standalone application // We're running as a standalone application
try { try {
const session = loadSession(); const session = loadSession();
if (!session) return { client: undefined, isPasswordlessUser: false }; if (!session) {
throw new Error("No session stored");
}
logger.log("Using a standalone client"); logger.log("Using a standalone client");
/* eslint-disable camelcase */ /* eslint-disable camelcase */
const { user_id, device_id, access_token, passwordlessUser } = const { user_id, device_id, access_token, passwordlessUser } = session;
session; const initClientParams = {
baseUrl: Config.defaultHomeserverUrl()!,
const livekit = Config.get().livekit;
const foci = livekit
? [
{
livekitServiceUrl: livekit.livekit_service_url,
},
]
: undefined;
try {
return {
client: await initClient(
{
baseUrl: Config.defaultHomeserverUrl(),
accessToken: access_token, accessToken: access_token,
userId: user_id, userId: user_id,
deviceId: device_id, deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed, fallbackICEServerAllowed: fallbackICEServerAllowed,
foci, livekitServiceURL: Config.get().livekit!.livekit_service_url,
}, };
true
), try {
isPasswordlessUser: passwordlessUser, const client = await initClient(initClientParams, true);
return {
client,
passwordlessUser,
}; };
} catch (err) { } catch (err) {
if (err instanceof CryptoStoreIntegrityError) { if (err instanceof CryptoStoreIntegrityError) {
// We can't use this session anymore, so let's log it out // We can't use this session anymore, so let's log it out
try { try {
const client = await initClient( const client = await initClient(initClientParams, false); // Don't need the crypto store just to log out)
{
baseUrl: Config.defaultHomeserverUrl(),
accessToken: access_token,
userId: user_id,
deviceId: device_id,
fallbackICEServerAllowed: fallbackICEServerAllowed,
foci,
},
false // Don't need the crypto store just to log out
);
await client.logout(true); await client.logout(true);
} catch (err_) { } catch (err) {
logger.warn( logger.warn(
"The previous session was lost, and we couldn't log it out, " + "The previous session was lost, and we couldn't log it out, " +
err +
"either" "either"
); );
} }
@@ -193,178 +366,24 @@ export const ClientProvider: FC<Props> = ({ children }) => {
throw err; throw err;
} }
} }
}
export interface Session {
user_id: string;
device_id: string;
access_token: string;
passwordlessUser: boolean;
tempPassword?: string;
}
const clearSession = () => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
const loadSession = (): Session | undefined => {
const data = localStorage.getItem("matrix-auth-store");
if (!data) {
return undefined;
}
return JSON.parse(data);
}; };
init()
.then(({ client, isPasswordlessUser }) => {
setState({
client,
loading: false,
isAuthenticated: Boolean(client),
isPasswordlessUser,
userName: client?.getUserIdLocalpart(),
error: undefined,
});
})
.catch((err) => {
logger.error(err);
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
error: undefined,
});
})
.finally(() => (initializing.current = false));
}, []);
const changePassword = useCallback(
async (password: string) => {
const { tempPassword, ...session } = loadSession();
await client.setPassword(
{
type: "m.login.password",
identifier: {
type: "m.id.user",
user: session.user_id,
},
user: session.user_id,
password: tempPassword,
},
password
);
saveSession({ ...session, passwordlessUser: false });
setState({
client,
loading: false,
isAuthenticated: true,
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
error: undefined,
});
},
[client]
);
const setClient = useCallback(
(newClient: MatrixClient, session: Session) => {
if (client && client !== newClient) {
client.stopClient();
}
if (newClient) {
saveSession(session);
setState({
client: newClient,
loading: false,
isAuthenticated: true,
isPasswordlessUser: session.passwordlessUser,
userName: newClient.getUserIdLocalpart(),
error: undefined,
});
} else {
clearSession();
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
error: undefined,
});
}
},
[client]
);
const logout = useCallback(async () => {
await client.logout(true);
await client.clearStores();
clearSession();
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: true,
userName: "",
error: undefined,
});
history.push("/");
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
}, [history, client]);
const { t } = useTranslation();
// To protect against multiple sessions writing to the same storage
// simultaneously, we send a broadcast message that shuts down all other
// running instances of the app. This isn't necessary if the app is running in
// a widget though, since then it'll be mostly stateless.
useEffect(() => {
if (!widget) loadChannel?.postMessage({});
}, []);
useEventTarget(
loadChannel,
"message",
useCallback(() => {
client?.stopClient();
setState((prev) => ({
...prev,
error: translatedError(
"This application has been opened in another tab.",
t
),
}));
}, [client, setState, t])
);
const context = useMemo<ClientState>(
() => ({
loading,
isAuthenticated,
isPasswordlessUser,
client,
changePassword,
logout,
userName,
setClient,
error: undefined,
}),
[
loading,
isAuthenticated,
isPasswordlessUser,
client,
changePassword,
logout,
userName,
setClient,
]
);
useEffect(() => {
window.matrixclient = client;
window.isPasswordlessUser = isPasswordlessUser;
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
}, [client, isPasswordlessUser]);
if (error) {
return <ErrorView error={error} />;
}
return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
);
};
export const useClient = () => useContext(ClientContext);

23
src/E2EEBanner.module.css Normal file
View File

@@ -0,0 +1,23 @@
/*
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.
*/
.e2eeBanner {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: var(--font-size-caption);
}

35
src/E2EEBanner.tsx Normal file
View File

@@ -0,0 +1,35 @@
/*
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 { Trans } from "react-i18next";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
export const E2EEBanner = () => {
return (
<Banner>
<div className={styles.e2eeBanner}>
<LockOffIcon width={24} height={24} />
<Trans>
Element Call is temporarily not end-to-end encrypted while we test
scalability.
</Trans>
</div>
</Banner>
);
};

28
src/E2EELock.module.css Normal file
View File

@@ -0,0 +1,28 @@
/*
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.
*/
.e2eeLock {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 8px;
border-radius: 100%;
background-color: var(--subtle-primary);
}

58
src/E2EELock.tsx Normal file
View File

@@ -0,0 +1,58 @@
/*
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 { useTranslation } from "react-i18next";
import { useCallback } from "react";
import { useObjectRef } from "@react-aria/utils";
import { useButton } from "@react-aria/button";
import styles from "./E2EELock.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { TooltipTrigger } from "./Tooltip";
export const E2EELock = () => {
const { t } = useTranslation();
const tooltip = useCallback(
() =>
t(
"Element Call is temporarily not end-to-end encrypted while we test scalability."
),
[t]
);
return (
<TooltipTrigger placement="right" tooltip={tooltip}>
<Icon />
</TooltipTrigger>
);
};
/**
* This component is a bit of hack - for some reason for the TooltipTrigger to
* work, it needs to contain a component which uses the useButton hook; please
* note that for some reason this also needs to be a separate component and we
* cannot just use the useButton hook inside the E2EELock.
*/
const Icon = () => {
const buttonRef = useObjectRef<HTMLDivElement>();
const { buttonProps } = useButton({}, buttonRef);
return (
<div ref={buttonRef} className={styles.e2eeLock} {...buttonProps}>
<LockOffIcon />
</div>
);
};

View File

@@ -47,8 +47,8 @@ export function Facepile({
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const _size = sizes.get(size); const _size = sizes.get(size)!;
const _overlap = overlapMap[size]; const _overlap = overlapMap[size]!;
const title = useMemo(() => { const title = useMemo(() => {
return members.reduce<string | null>( return members.reduce<string | null>(

View File

@@ -36,15 +36,16 @@ export function ListBox<T>({
listBoxRef, listBoxRef,
...rest ...rest
}: ListBoxProps<T>) { }: ListBoxProps<T>) {
const ref = useRef<HTMLUListElement>(); const ref = useRef<HTMLUListElement>(null);
if (!listBoxRef) listBoxRef = ref;
const { listBoxProps } = useListBox(rest, state, listBoxRef); const listRef = listBoxRef ?? ref;
const { listBoxProps } = useListBox(rest, state, listRef);
return ( return (
<ul <ul
{...listBoxProps} {...listBoxProps}
ref={listBoxRef} ref={listRef}
className={classNames(styles.listBox, className)} className={classNames(styles.listBox, className)}
> >
{[...state.collection].map((item) => ( {[...state.collection].map((item) => (
@@ -66,7 +67,7 @@ interface OptionProps<T> {
} }
function Option<T>({ item, state, className }: OptionProps<T>) { function Option<T>({ item, state, className }: OptionProps<T>) {
const ref = useRef(); const ref = useRef(null);
const { optionProps, isSelected, isFocused, isDisabled } = useOption( const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key }, { key: item.key },
state, state,
@@ -83,7 +84,11 @@ function Option<T>({ item, state, className }: OptionProps<T>) {
const origPointerUp = optionProps.onPointerUp; const origPointerUp = optionProps.onPointerUp;
delete optionProps.onPointerUp; delete optionProps.onPointerUp;
optionProps.onClick = useCallback( optionProps.onClick = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(e) => { (e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
origPointerUp(e as unknown as PointerEvent<HTMLElement>); origPointerUp(e as unknown as PointerEvent<HTMLElement>);
}, },
[origPointerUp] [origPointerUp]

View File

@@ -26,7 +26,7 @@ import styles from "./Menu.module.css";
interface MenuProps<T> extends AriaMenuOptions<T> { interface MenuProps<T> extends AriaMenuOptions<T> {
className?: String; className?: String;
onClose?: () => void; onClose: () => void;
onAction: (value: Key) => void; onAction: (value: Key) => void;
label?: string; label?: string;
} }
@@ -39,7 +39,7 @@ export function Menu<T extends object>({
...rest ...rest
}: MenuProps<T>) { }: MenuProps<T>) {
const state = useTreeState<T>({ ...rest, selectionMode: "none" }); const state = useTreeState<T>({ ...rest, selectionMode: "none" });
const menuRef = useRef(); const menuRef = useRef(null);
const { menuProps } = useMenu<T>(rest, state, menuRef); const { menuProps } = useMenu<T>(rest, state, menuRef);
return ( return (
@@ -69,7 +69,7 @@ interface MenuItemProps<T> {
} }
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) { function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
const ref = useRef(); const ref = useRef(null);
const { menuItemProps } = useMenuItem( const { menuItemProps } = useMenuItem(
{ {
key: item.key, key: item.key,

View File

@@ -55,7 +55,7 @@ export function Modal({
...rest ...rest
}: ModalProps) { }: ModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const modalRef = useRef(); const modalRef = useRef(null);
const { overlayProps, underlayProps } = useOverlay( const { overlayProps, underlayProps } = useOverlay(
{ ...rest, onClose }, { ...rest, onClose },
modalRef modalRef
@@ -63,7 +63,7 @@ export function Modal({
usePreventScroll(); usePreventScroll();
const { modalProps } = useModal(); const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog(rest, modalRef); const { dialogProps, titleProps } = useDialog(rest, modalRef);
const closeButtonRef = useRef(); const closeButtonRef = useRef(null);
const { buttonProps: closeButtonProps } = useButton( const { buttonProps: closeButtonProps } = useButton(
{ {
onPress: () => onClose(), onPress: () => onClose(),

View File

@@ -36,6 +36,9 @@ export function SequenceDiagramViewerPage() {
const [debugLog, setDebugLog] = useState<DebugLog>(); const [debugLog, setDebugLog] = useState<DebugLog>();
const [selectedUserId, setSelectedUserId] = useState<string>(); const [selectedUserId, setSelectedUserId] = useState<string>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const onChangeDebugLog = useCallback((e) => { const onChangeDebugLog = useCallback((e) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
e.target.files[0].text().then((text: string) => { e.target.files[0].text().then((text: string) => {
@@ -55,7 +58,7 @@ export function SequenceDiagramViewerPage() {
onChange={onChangeDebugLog} onChange={onChangeDebugLog}
/> />
</FieldRow> </FieldRow>
{debugLog && ( {debugLog && selectedUserId && (
<SequenceDiagramViewer <SequenceDiagramViewer
localUserId={debugLog.localUserId} localUserId={debugLog.localUserId}
selectedUserId={selectedUserId} selectedUserId={selectedUserId}

View File

@@ -74,7 +74,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
const tooltipTriggerProps = { delay: 250, ...rest }; const tooltipTriggerProps = { delay: 250, ...rest };
const tooltipState = useTooltipTriggerState(tooltipTriggerProps); const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
const triggerRef = useObjectRef<HTMLElement>(ref); const triggerRef = useObjectRef<HTMLElement>(ref);
const overlayRef = useRef(); const overlayRef = useRef<HTMLDivElement>(null);
const { triggerProps, tooltipProps } = useTooltipTrigger( const { triggerProps, tooltipProps } = useTooltipTrigger(
tooltipTriggerProps, tooltipTriggerProps,
tooltipState, tooltipState,

View File

@@ -36,7 +36,7 @@ interface UserMenuProps {
isAuthenticated: boolean; isAuthenticated: boolean;
isPasswordlessUser: boolean; isPasswordlessUser: boolean;
displayName: string; displayName: string;
avatarUrl: string; avatarUrl?: string;
onAction: (value: string) => void; onAction: (value: string) => void;
} }
@@ -119,7 +119,9 @@ export function UserMenu({
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
{(props) => ( {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(props: any) => (
<Menu {...props} label={t("User menu")} onAction={onAction}> <Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label, dataTestid }) => ( {items.map(({ key, icon: Icon, label, dataTestid }) => (
<Item key={key} textValue={label}> <Item key={key} textValue={label}>
@@ -133,7 +135,8 @@ export function UserMenu({
</Item> </Item>
))} ))}
</Menu> </Menu>
)} )
}
</PopoverMenuTrigger> </PopoverMenuTrigger>
); );
} }

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { useClient } from "./ClientContext"; import { useClientLegacy } from "./ClientContext";
import { useProfile } from "./profile/useProfile"; import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal"; import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./settings/SettingsModal"; import { SettingsModal } from "./settings/SettingsModal";
@@ -30,8 +30,7 @@ interface Props {
export function UserMenuContainer({ preventNavigation = false }: Props) { export function UserMenuContainer({ preventNavigation = false }: Props) {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const { isAuthenticated, isPasswordlessUser, logout, userName, client } = const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
useClient();
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState(); const { modalState, modalProps } = useModalTriggerState();
@@ -49,7 +48,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
modalState.open(); modalState.open();
break; break;
case "logout": case "logout":
logout(); logout?.();
break; break;
case "login": case "login":
history.push("/login", { state: { from: location } }); history.push("/login", { state: { from: location } });
@@ -59,19 +58,18 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
[history, location, logout, modalState] [history, location, logout, modalState]
); );
const userName = client?.getUserIdLocalpart() ?? "";
return ( return (
<> <>
<UserMenu <UserMenu
preventNavigation={preventNavigation} preventNavigation={preventNavigation}
isAuthenticated={isAuthenticated} isAuthenticated={authenticated}
isPasswordlessUser={isPasswordlessUser} isPasswordlessUser={passwordlessUser}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
onAction={onAction} onAction={onAction}
displayName={ displayName={displayName || (userName ? userName.replace("@", "") : "")}
displayName || (userName ? userName.replace("@", "") : undefined)
}
/> />
{modalState.isOpen && ( {modalState.isOpen && client && (
<SettingsModal <SettingsModal
client={client} client={client}
defaultTab={defaultSettingsTab} defaultTab={defaultSettingsTab}

View File

@@ -98,7 +98,7 @@ export class PosthogAnalytics {
// set true during the constructor if posthog config is present, otherwise false // set true during the constructor if posthog config is present, otherwise false
private static internalInstance: PosthogAnalytics | null = null; private static internalInstance: PosthogAnalytics | null = null;
private identificationPromise: Promise<void>; private identificationPromise?: Promise<void>;
private readonly enabled: boolean = false; private readonly enabled: boolean = false;
private anonymity = Anonymity.Disabled; private anonymity = Anonymity.Disabled;
private platformSuperProperties = {}; private platformSuperProperties = {};
@@ -255,7 +255,9 @@ export class PosthogAnalytics {
} catch (e) { } catch (e) {
// The above could fail due to network requests, but not essential to starting the application, // The above could fail due to network requests, but not essential to starting the application,
// so swallow it. // so swallow it.
logger.log("Unable to identify user for tracking" + e.toString()); logger.log(
"Unable to identify user for tracking" + (e as Error)?.toString()
);
} }
if (analyticsID) { if (analyticsID) {
this.posthog.identify(analyticsID); this.posthog.identify(analyticsID);
@@ -366,7 +368,7 @@ export class PosthogAnalytics {
if (anonymity === Anonymity.Pseudonymous) { if (anonymity === Anonymity.Pseudonymous) {
this.setRegistrationType( this.setRegistrationType(
window.matrixclient.isGuest() || window.isPasswordlessUser window.matrixclient.isGuest() || window.passwordlessUser
? RegistrationType.Guest ? RegistrationType.Guest
: RegistrationType.Registered : RegistrationType.Registered
); );

View File

@@ -22,10 +22,10 @@ limitations under the License.
// Array.prototype.findLastIndex // Array.prototype.findLastIndex
export function findLastIndex<T>( export function findLastIndex<T>(
array: T[], array: T[],
predicate: (item: T) => boolean predicate: (item: T, index: number) => boolean
): number | null { ): number | null {
for (let i = array.length - 1; i >= 0; i--) { for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i; if (predicate(array[i], i)) return i;
} }
return null; return null;
@@ -34,5 +34,11 @@ export function findLastIndex<T>(
/** /**
* Counts the number of elements in an array that satsify the given predicate. * Counts the number of elements in an array that satsify the given predicate.
*/ */
export const count = <T>(array: T[], predicate: (item: T) => boolean): number => export const count = <T>(
array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0); array: T[],
predicate: (item: T, index: number) => boolean
): number =>
array.reduce(
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
0
);

View File

@@ -35,8 +35,8 @@ export const LoginPage: FC = () => {
const { setClient } = useClient(); const { setClient } = useClient();
const login = useInteractiveLogin(); const login = useInteractiveLogin();
const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable
const usernameRef = useRef<HTMLInputElement>(); const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(); const passwordRef = useRef<HTMLInputElement>(null);
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -49,12 +49,27 @@ export const LoginPage: FC = () => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
if (!homeserver || !usernameRef.current || !passwordRef.current) {
setError(Error("Login parameters are undefined"));
setLoading(false);
return;
}
login(homeserver, usernameRef.current.value, passwordRef.current.value) login(homeserver, usernameRef.current.value, passwordRef.current.value)
.then(([client, session]) => { .then(([client, session]) => {
setClient(client, session); if (!setClient) {
return;
}
if (location.state && location.state.from) { setClient({ client, session });
history.push(location.state.from);
const locationState = location.state;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (locationState && locationState.from) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
history.push(locationState.from);
} else { } else {
history.push("/"); history.push("/");
} }

View File

@@ -30,7 +30,7 @@ import { Trans, useTranslation } from "react-i18next";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button"; import { Button } from "../button";
import { useClient } from "../ClientContext"; import { useClientLegacy } from "../ClientContext";
import { useInteractiveRegistration } from "./useInteractiveRegistration"; import { useInteractiveRegistration } from "./useInteractiveRegistration";
import styles from "./LoginPage.module.css"; import styles from "./LoginPage.module.css";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg"; import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
@@ -45,17 +45,17 @@ export const RegisterPage: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
usePageTitle(t("Register")); usePageTitle(t("Register"));
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } = const { loading, authenticated, passwordlessUser, client, setClient } =
useClient(); useClientLegacy();
const confirmPasswordRef = useRef<HTMLInputElement>();
const confirmPasswordRef = useRef<HTMLInputElement>(null);
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const [registering, setRegistering] = useState(false); const [registering, setRegistering] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [privacyPolicyUrl, recaptchaKey, register] = const { recaptchaKey, register } = useInteractiveRegistration();
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const onSubmitRegisterForm = useCallback( const onSubmitRegisterForm = useCallback(
@@ -76,10 +76,15 @@ export const RegisterPage: FC = () => {
userName, userName,
password, password,
userName, userName,
recaptchaResponse recaptchaResponse,
passwordlessUser
); );
if (client && isPasswordlessUser) { if (!client || !client.groupCallEventHandler || !setClient) {
return;
}
if (passwordlessUser) {
// Migrate the user's rooms // Migrate the user's rooms
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) { for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
const roomId = groupCall.room.roomId; const roomId = groupCall.room.roomId;
@@ -87,7 +92,11 @@ export const RegisterPage: FC = () => {
try { try {
await newClient.joinRoom(roomId); await newClient.joinRoom(roomId);
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (error.errcode === "M_LIMIT_EXCEEDED") { if (error.errcode === "M_LIMIT_EXCEEDED") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await sleep(error.data.retry_after_ms); await sleep(error.data.retry_after_ms);
await newClient.joinRoom(roomId); await newClient.joinRoom(roomId);
} else { } else {
@@ -98,13 +107,17 @@ export const RegisterPage: FC = () => {
} }
} }
setClient(newClient, session); setClient({ client: newClient, session });
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date()); PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
}; };
submit() submit()
.then(() => { .then(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (location.state?.from) { if (location.state?.from) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
history.push(location.state?.from); history.push(location.state?.from);
} else { } else {
history.push("/"); history.push("/");
@@ -120,7 +133,7 @@ export const RegisterPage: FC = () => {
register, register,
location, location,
history, history,
isPasswordlessUser, passwordlessUser,
reset, reset,
execute, execute,
client, client,
@@ -137,10 +150,10 @@ export const RegisterPage: FC = () => {
}, [password, passwordConfirmation, t]); }, [password, passwordConfirmation, t]);
useEffect(() => { useEffect(() => {
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) { if (!loading && authenticated && !passwordlessUser && !registering) {
history.push("/"); history.push("/");
} }
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]); }, [loading, history, authenticated, passwordlessUser, registering]);
if (loading) { if (loading) {
return <LoadingView />; return <LoadingView />;
@@ -211,7 +224,7 @@ export const RegisterPage: FC = () => {
apply. apply.
<br /> <br />
By clicking "Register", you agree to our{" "} By clicking "Register", you agree to our{" "}
<Link href={privacyPolicyUrl}> <Link href={Config.get().eula}>
End User Licensing Agreement (EULA) End User Licensing Agreement (EULA)
</Link> </Link>
</Trans> </Trans>

View File

@@ -41,8 +41,10 @@ export const useInteractiveLogin = () =>
}, },
password, password,
}), }),
stateUpdated: null, stateUpdated: (...args) => {},
requestEmailToken: null, requestEmailToken: (...args): Promise<{ sid: string }> => {
return Promise.resolve({ sid: "" });
},
}); });
// XXX: This claims to return an IAuthData which contains none of these // XXX: This claims to return an IAuthData which contains none of these

View File

@@ -22,29 +22,33 @@ import { initClient } from "../matrix-utils";
import { Session } from "../ClientContext"; import { Session } from "../ClientContext";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
export const useInteractiveRegistration = (): [ export const useInteractiveRegistration = (): {
string, privacyPolicyUrl?: string;
string, recaptchaKey?: string;
( register: (
username: string, username: string,
password: string, password: string,
displayName: string, displayName: string,
recaptchaResponse: string, recaptchaResponse: string,
passwordlessUser?: boolean passwordlessUser: boolean
) => Promise<[MatrixClient, Session]> ) => Promise<[MatrixClient, Session]>;
] => { } => {
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>(); const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
const [recaptchaKey, setRecaptchaKey] = useState<string>(); undefined
);
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
undefined
);
const authClient = useRef<MatrixClient>(); const authClient = useRef<MatrixClient>();
if (!authClient.current) { if (!authClient.current) {
authClient.current = createClient({ authClient.current = createClient({
baseUrl: Config.defaultHomeserverUrl(), baseUrl: Config.defaultHomeserverUrl()!,
}); });
} }
useEffect(() => { useEffect(() => {
authClient.current.registerRequest({}).catch((error) => { authClient.current!.registerRequest({}).catch((error) => {
setPrivacyPolicyUrl( setPrivacyPolicyUrl(
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
); );
@@ -58,12 +62,12 @@ export const useInteractiveRegistration = (): [
password: string, password: string,
displayName: string, displayName: string,
recaptchaResponse: string, recaptchaResponse: string,
passwordlessUser?: boolean passwordlessUser: boolean
): Promise<[MatrixClient, Session]> => { ): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({ const interactiveAuth = new InteractiveAuth({
matrixClient: authClient.current, matrixClient: authClient.current!,
doRequest: (auth) => doRequest: (auth) =>
authClient.current.registerRequest({ authClient.current!.registerRequest({
username, username,
password, password,
auth: auth || undefined, auth: auth || undefined,
@@ -84,7 +88,9 @@ export const useInteractiveRegistration = (): [
}); });
} }
}, },
requestEmailToken: null, requestEmailToken: (...args) => {
return Promise.resolve({ sid: "dummy" });
},
}); });
// XXX: This claims to return an IAuthData which contains none of these // XXX: This claims to return an IAuthData which contains none of these
@@ -95,7 +101,7 @@ export const useInteractiveRegistration = (): [
const client = await initClient( const client = await initClient(
{ {
baseUrl: Config.defaultHomeserverUrl(), baseUrl: Config.defaultHomeserverUrl()!,
accessToken: access_token, accessToken: access_token,
userId: user_id, userId: user_id,
deviceId: device_id, deviceId: device_id,
@@ -117,7 +123,7 @@ export const useInteractiveRegistration = (): [
session.tempPassword = password; session.tempPassword = password;
} }
const user = client.getUser(client.getUserId()); const user = client.getUser(client.getUserId()!)!;
user.setRawDisplayName(displayName); user.setRawDisplayName(displayName);
user.setDisplayName(displayName); user.setDisplayName(displayName);
@@ -126,5 +132,5 @@ export const useInteractiveRegistration = (): [
[] []
); );
return [privacyPolicyUrl, recaptchaKey, register]; return { privacyPolicyUrl, recaptchaKey, register };
}; };

View File

@@ -34,7 +34,7 @@ interface RecaptchaPromiseRef {
reject: (error: Error) => void; reject: (error: Error) => void;
} }
export const useRecaptcha = (sitekey: string) => { export const useRecaptcha = (sitekey?: string) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16)); const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>(); const promiseRef = useRef<RecaptchaPromiseRef>();
@@ -68,9 +68,9 @@ export const useRecaptcha = (sitekey: string) => {
} }
}, [recaptchaId, sitekey]); }, [recaptchaId, sitekey]);
const execute = useCallback(() => { const execute = useCallback((): Promise<string> => {
if (!sitekey) { if (!sitekey) {
return Promise.resolve(null); return Promise.resolve("");
} }
if (!window.grecaptcha) { if (!window.grecaptcha) {

View File

@@ -23,19 +23,23 @@ import { generateRandomName } from "../auth/generateRandomName";
import { useRecaptcha } from "../auth/useRecaptcha"; import { useRecaptcha } from "../auth/useRecaptcha";
interface UseRegisterPasswordlessUserType { interface UseRegisterPasswordlessUserType {
privacyPolicyUrl: string; privacyPolicyUrl?: string;
registerPasswordlessUser: (displayName: string) => Promise<void>; registerPasswordlessUser: (displayName: string) => Promise<void>;
recaptchaId: string; recaptchaId?: string;
} }
export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
const { setClient } = useClient(); const { setClient } = useClient();
const [privacyPolicyUrl, recaptchaKey, register] = const { privacyPolicyUrl, recaptchaKey, register } =
useInteractiveRegistration(); useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const registerPasswordlessUser = useCallback( const registerPasswordlessUser = useCallback(
async (displayName: string) => { async (displayName: string) => {
if (!setClient) {
throw new Error("No client context");
}
try { try {
const recaptchaResponse = await execute(); const recaptchaResponse = await execute();
const userName = generateRandomName(); const userName = generateRandomName();
@@ -46,7 +50,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
recaptchaResponse, recaptchaResponse,
true true
); );
setClient(client, session); setClient({ client, session });
} catch (e) { } catch (e) {
reset(); reset();
throw e; throw e;

View File

@@ -61,6 +61,10 @@ limitations under the License.
outline: auto; outline: auto;
} }
.toolbarButton:disabled {
opacity: 0.55;
}
.toolbarButton, .toolbarButton,
.toolbarButtonSecondary { .toolbarButtonSecondary {
width: 50px; width: 50px;

View File

@@ -77,6 +77,7 @@ interface Props {
children: Element[]; children: Element[];
onPress: (e: PressEvent) => void; onPress: (e: PressEvent) => void;
onPressStart: (e: PressEvent) => void; onPressStart: (e: PressEvent) => void;
disabled: boolean;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
} }

View File

@@ -46,7 +46,7 @@ export function LinkButton({
<Link <Link
className={classNames( className={classNames(
variantToClassName[variant || "secondary"], variantToClassName[variant || "secondary"],
sizeToClassName[size], size ? sizeToClassName[size] : [],
className className
)} )}
to={to} to={to}

View File

@@ -45,11 +45,11 @@ export class Config {
// Convenience accessors // Convenience accessors
public static defaultHomeserverUrl(): string | undefined { public static defaultHomeserverUrl(): string | undefined {
return Config.get().default_server_config["m.homeserver"].base_url; return Config.get().default_server_config?.["m.homeserver"].base_url;
} }
public static defaultServerName(): string | undefined { public static defaultServerName(): string | undefined {
return Config.get().default_server_config["m.homeserver"].server_name; return Config.get().default_server_config?.["m.homeserver"].server_name;
} }
public config?: ResolvedConfigOptions; public config?: ResolvedConfigOptions;

View File

@@ -66,6 +66,11 @@ export interface ConfigOptions {
features?: { features?: {
feature_group_calls_without_video_and_audio: boolean; feature_group_calls_without_video_and_audio: boolean;
}; };
/**
* A link to the end-user license agreement (EULA)
*/
eula: string;
} }
// Overrides members from ConfigOptions that are always provided by the // Overrides members from ConfigOptions that are always provided by the
@@ -86,4 +91,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
server_name: "localhost", server_name: "localhost",
}, },
}, },
eula: "https://static.element.io/legal/online-EULA.pdf",
}; };

View File

@@ -42,6 +42,12 @@ interface Props {
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => { export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const onAction = (key: React.Key) => {
setCallType(key.toString() as CallType);
};
const onClose = () => {};
return ( return (
<PopoverMenuTrigger placement="bottom"> <PopoverMenuTrigger placement="bottom">
<Button variant="dropdown" className={commonStyles.headline}> <Button variant="dropdown" className={commonStyles.headline}>
@@ -52,7 +58,12 @@ export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
</Headline> </Headline>
</Button> </Button>
{(props: JSX.IntrinsicAttributes) => ( {(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label={t("Call type menu")} onAction={setCallType}> <Menu
{...props}
label={t("Call type menu")}
onAction={onAction}
onClose={onClose}
>
<Item key={CallType.Video} textValue={t("Video call")}> <Item key={CallType.Video} textValue={t("Video call")}>
<VideoIcon /> <VideoIcon />
<span>{t("Video call")}</span> <span>{t("Video call")}</span>

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useClient } from "../ClientContext"; import { useClientState } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
import { UnauthenticatedView } from "./UnauthenticatedView"; import { UnauthenticatedView } from "./UnauthenticatedView";
import { RegisteredView } from "./RegisteredView"; import { RegisteredView } from "./RegisteredView";
@@ -26,16 +26,18 @@ export function HomePage() {
const { t } = useTranslation(); const { t } = useTranslation();
usePageTitle(t("Home")); usePageTitle(t("Home"));
const { isAuthenticated, isPasswordlessUser, loading, error, client } = const clientState = useClientState();
useClient();
if (loading) { if (!clientState) {
return <LoadingView />; return <LoadingView />;
} else if (error) { } else if (clientState.state === "error") {
return <ErrorView error={error} />; return <ErrorView error={clientState.error} />;
} else { } else {
return isAuthenticated ? ( return clientState.authenticated ? (
<RegisteredView isPasswordlessUser={isPasswordlessUser} client={client} /> <RegisteredView
isPasswordlessUser={clientState.authenticated.isPasswordlessUser}
client={clientState.authenticated.client}
/>
) : ( ) : (
<UnauthenticatedView /> <UnauthenticatedView />
); );

View File

@@ -39,6 +39,7 @@ import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import { useOptInAnalytics } from "../settings/useSetting"; import { useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
@@ -146,6 +147,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
<AnalyticsNotice /> <AnalyticsNotice />
</Caption> </Caption>
)} )}
<E2EEBanner />
{error && ( {error && (
<FieldRow className={styles.fieldRow}> <FieldRow className={styles.fieldRow}>
<ErrorMessage error={error} /> <ErrorMessage error={error} />

View File

@@ -41,6 +41,8 @@ import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName"; import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useOptInAnalytics } from "../settings/useSetting"; import { useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
export const UnauthenticatedView: FC = () => { export const UnauthenticatedView: FC = () => {
const { setClient } = useClient(); const { setClient } = useClient();
@@ -48,8 +50,7 @@ export const UnauthenticatedView: FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics(); const [optInAnalytics] = useOptInAnalytics();
const [privacyPolicyUrl, recaptchaKey, register] = const { recaptchaKey, register } = useInteractiveRegistration();
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const { modalState, modalProps } = useModalTriggerState(); const { modalState, modalProps } = useModalTriggerState();
@@ -82,9 +83,15 @@ export const UnauthenticatedView: FC = () => {
try { try {
[roomAlias] = await createRoom(client, roomName, ptt); [roomAlias] = await createRoom(client, roomName, ptt);
} catch (error) { } catch (error) {
if (!setClient) {
throw error;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (error.errcode === "M_ROOM_IN_USE") { if (error.errcode === "M_ROOM_IN_USE") {
setOnFinished(() => { setOnFinished(() => {
setClient(client, session); setClient({ client, session });
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName); const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
history.push(`/${aliasLocalpart}`); history.push(`/${aliasLocalpart}`);
}); });
@@ -98,7 +105,11 @@ export const UnauthenticatedView: FC = () => {
} }
// Only consider the registration successful if we managed to create the room, too // Only consider the registration successful if we managed to create the room, too
setClient(client, session); if (!setClient) {
throw new Error("setClient is undefined");
}
setClient({ client, session });
history.push(`/${roomAlias.substring(1).split(":")[0]}`); history.push(`/${roomAlias.substring(1).split(":")[0]}`);
} }
@@ -164,11 +175,12 @@ export const UnauthenticatedView: FC = () => {
<Caption className={styles.notice}> <Caption className={styles.notice}>
<Trans> <Trans>
By clicking "Go", you agree to our{" "} By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}> <Link href={Config.get().eula}>
End User Licensing Agreement (EULA) End User Licensing Agreement (EULA)
</Link> </Link>
</Trans> </Trans>
</Caption> </Caption>
<E2EEBanner />
{error && ( {error && (
<FieldRow> <FieldRow>
<ErrorMessage error={error} /> <ErrorMessage error={error} />
@@ -201,7 +213,7 @@ export const UnauthenticatedView: FC = () => {
</Body> </Body>
</footer> </footer>
</div> </div>
{modalState.isOpen && ( {modalState.isOpen && onFinished && (
<JoinExistingCallModal onJoin={onFinished} {...modalProps} /> <JoinExistingCallModal onJoin={onFinished} {...modalProps} />
)} )}
</> </>

View File

@@ -42,7 +42,7 @@ function getLastTs(client: MatrixClient, r: Room) {
return ts; return ts;
} }
const myUserId = client.getUserId(); const myUserId = client.getUserId()!;
if (r.getMyMembership() !== "join") { if (r.getMyMembership() !== "join") {
const membershipEvent = r.currentState.getStateEvents( const membershipEvent = r.currentState.getStateEvents(
@@ -83,23 +83,28 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
useEffect(() => { useEffect(() => {
function updateRooms() { function updateRooms() {
if (!client.groupCallEventHandler) {
return;
}
const groupCalls = client.groupCallEventHandler.groupCalls.values(); const groupCalls = client.groupCallEventHandler.groupCalls.values();
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room); const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias
const sortedRooms = sortRooms(client, filteredRooms); const sortedRooms = sortRooms(client, filteredRooms);
const items: GroupCallRoom[] = sortedRooms.map((room) => { const items = sortedRooms.map((room) => {
const groupCall = client.getGroupCallForRoom(room.roomId); const groupCall = client.getGroupCallForRoom(room.roomId)!;
return { return {
roomAlias: room.getCanonicalAlias(), roomAlias: room.getCanonicalAlias(),
roomName: room.name, roomName: room.name,
avatarUrl: room.getMxcAvatarUrl(), avatarUrl: room.getMxcAvatarUrl()!,
room, room,
groupCall, groupCall,
participants: [...groupCall.participants.keys()], participants: [...groupCall!.participants.keys()],
}; };
}); });
setRooms(items);
setRooms(items as GroupCallRoom[]);
} }
updateRooms(); updateRooms();

4
src/icons/LockOff.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.00003 14.6665C3.63336 14.6665 3.31947 14.5359 3.05836 14.2748C2.79725 14.0137 2.6667 13.6998 2.6667 13.3332V6.6665C2.6667 6.29984 2.79725 5.98595 3.05836 5.72484C3.19921 5.58399 3.35541 5.48113 3.52697 5.41626L0.888621 2.77791C0.628272 2.51756 0.628272 2.09545 0.888622 1.8351C1.14897 1.57475 1.57108 1.57475 1.83143 1.8351L4.6667 4.67037V4.66267L13.3334 13.3293V13.3332L13.3334 13.337L14.1648 14.1685C14.4251 14.4288 14.4251 14.8509 14.1648 15.1113C13.9044 15.3716 13.4823 15.3716 13.222 15.1113L12.6247 14.514C12.437 14.6157 12.2288 14.6665 12 14.6665H4.00003Z" fill="#808994"/>
<path d="M13.3334 11.4437V6.6665C13.3334 6.29984 13.2028 5.98595 12.9417 5.72484C12.6806 5.46373 12.3667 5.33317 12 5.33317H11.3334V3.99984C11.3334 3.07762 11.0084 2.2915 10.3584 1.6415C9.70836 0.991504 8.92225 0.666504 8.00003 0.666504C7.07781 0.666504 6.2917 0.991504 5.6417 1.6415C5.25683 2.02637 4.9859 2.45896 4.82892 2.93927L6.00003 4.11038V3.99984C6.00003 3.44428 6.19447 2.97206 6.58336 2.58317C6.97225 2.19428 7.44447 1.99984 8.00003 1.99984C8.55559 1.99984 9.02781 2.19428 9.4167 2.58317C9.80558 2.97206 10 3.44428 10 3.99984V5.33317H7.22282L13.3334 11.4437Z" fill="#808994"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -52,6 +52,7 @@ limitations under the License.
--background: #15191e; --background: #15191e;
--background-85: rgba(23, 25, 28, 0.85); --background-85: rgba(23, 25, 28, 0.85);
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */ --bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
--subtle-primary: #26282d;
} }
@font-face { @font-face {

View File

@@ -133,6 +133,10 @@ export class Initializer {
"--background-85", "--background-85",
import.meta.env.VITE_THEME_BACKGROUND_85 as string import.meta.env.VITE_THEME_BACKGROUND_85 as string
); );
style.setProperty(
"--subtle-primary",
import.meta.env.VITE_THEME_SUBTLE_PRIMARY as string
);
} }
// Custom fonts // Custom fonts
@@ -225,5 +229,6 @@ export class Initializer {
resolve(); resolve();
} }
} }
private initPromise: Promise<void> | null;
private initPromise?: Promise<void>;
} }

View File

@@ -15,10 +15,14 @@ limitations under the License.
*/ */
import { useObjectRef } from "@react-aria/utils"; import { useObjectRef } from "@react-aria/utils";
import { AllHTMLAttributes, ChangeEvent, useEffect } from "react"; import {
import { useCallback } from "react"; AllHTMLAttributes,
import { useState } from "react"; useEffect,
import { forwardRef } from "react"; useCallback,
useState,
forwardRef,
ChangeEvent,
} from "react";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -43,7 +47,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
const { t } = useTranslation(); const { t } = useTranslation();
const [removed, setRemoved] = useState(false); const [removed, setRemoved] = useState(false);
const [objUrl, setObjUrl] = useState<string>(null); const [objUrl, setObjUrl] = useState<string | undefined>(undefined);
const fileInputRef = useObjectRef(ref); const fileInputRef = useObjectRef(ref);
@@ -52,11 +56,11 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
const onChange = (e: Event) => { const onChange = (e: Event) => {
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>; const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
if (inputEvent.target.files.length > 0) { if (inputEvent.target.files && inputEvent.target.files.length > 0) {
setObjUrl(URL.createObjectURL(inputEvent.target.files[0])); setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
setRemoved(false); setRemoved(false);
} else { } else {
setObjUrl(null); setObjUrl(undefined);
} }
}; };
@@ -77,7 +81,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
<div className={styles.avatarContainer}> <div className={styles.avatarContainer}>
<Avatar <Avatar
size={Size.XL} size={Size.XL}
src={removed ? null : objUrl || avatarUrl} src={removed ? undefined : objUrl || avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()} fallback={displayName.slice(0, 1).toUpperCase()}
/> />
<input <input

View File

@@ -82,7 +82,7 @@ interface InputFieldProps {
defaultValue?: string; defaultValue?: string;
placeholder?: string; placeholder?: string;
defaultChecked?: boolean; defaultChecked?: boolean;
onChange?: (event: ChangeEvent) => void; onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
} }
export const InputField = forwardRef< export const InputField = forwardRef<
@@ -119,6 +119,8 @@ export const InputField = forwardRef<
> >
{prefix && <span>{prefix}</span>} {prefix && <span>{prefix}</span>}
{type === "textarea" ? ( {type === "textarea" ? (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<textarea <textarea
id={id} id={id}
ref={ref as ForwardedRef<HTMLTextAreaElement>} ref={ref as ForwardedRef<HTMLTextAreaElement>}

View File

@@ -34,7 +34,7 @@ export function SelectInput(props: Props): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const state = useSelectState(props); const state = useSelectState(props);
const ref = useRef(); const ref = useRef(null);
const { labelProps, triggerProps, valueProps, menuProps } = useSelect( const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
props, props,
state, state,

View File

@@ -22,6 +22,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { GroupCall } from "matrix-js-sdk";
import { import {
OpenIDClientParts, OpenIDClientParts,
@@ -32,21 +33,16 @@ import { ErrorView, LoadingView } from "../FullScreenView";
interface Props { interface Props {
client: OpenIDClientParts; client: OpenIDClientParts;
livekitServiceURL: string; groupCall: GroupCall;
roomName: string; roomName: string;
children: ReactNode; children: ReactNode;
} }
const SFUConfigContext = createContext<SFUConfig>(undefined); const SFUConfigContext = createContext<SFUConfig | undefined>(undefined);
export const useSFUConfig = () => useContext(SFUConfigContext); export const useSFUConfig = () => useContext(SFUConfigContext);
export function OpenIDLoader({ export function OpenIDLoader({ client, groupCall, roomName, children }: Props) {
client,
livekitServiceURL,
roomName,
children,
}: Props) {
const [state, setState] = useState< const [state, setState] = useState<
SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed
>({ kind: "loading" }); >({ kind: "loading" });
@@ -56,16 +52,16 @@ export function OpenIDLoader({
try { try {
const result = await getSFUConfigWithOpenID( const result = await getSFUConfigWithOpenID(
client, client,
livekitServiceURL, groupCall,
roomName roomName
); );
setState({ kind: "loaded", sfuConfig: result }); setState({ kind: "loaded", sfuConfig: result });
} catch (e) { } catch (e) {
logger.error("Failed to fetch SFU config: ", e); logger.error("Failed to fetch SFU config: ", e);
setState({ kind: "failed", error: e }); setState({ kind: "failed", error: e as Error });
} }
})(); })();
}, [client, livekitServiceURL, roomName]); }, [client, groupCall, roomName]);
switch (state.kind) { switch (state.kind) {
case "loading": case "loading":

View File

@@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from "matrix-js-sdk"; import { GroupCall, IOpenIDToken, MatrixClient } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Config } from "../config/Config";
export interface SFUConfig { export interface SFUConfig {
url: string; url: string;
jwt: string; jwt: string;
@@ -30,12 +32,68 @@ export type OpenIDClientParts = Pick<
export async function getSFUConfigWithOpenID( export async function getSFUConfigWithOpenID(
client: OpenIDClientParts, client: OpenIDClientParts,
livekitServiceURL: string, groupCall: GroupCall,
roomName: string roomName: string
): Promise<SFUConfig> { ): Promise<SFUConfig> {
const openIdToken = await client.getOpenIdToken(); const openIdToken = await client.getOpenIdToken();
logger.debug("Got openID token", openIdToken); logger.debug("Got openID token", openIdToken);
// if the call has a livekit service URL, try it.
if (groupCall.livekitServiceURL) {
try {
logger.info(`Trying to get JWT from ${groupCall.livekitServiceURL}...`);
const sfuConfig = await getLiveKitJWT(
client,
groupCall.livekitServiceURL,
roomName,
openIdToken
);
return sfuConfig;
} catch (e) {
logger.warn(
`Failed to get JWT from group call's configured URL of ${groupCall.livekitServiceURL}.`,
e
);
}
}
// otherwise, try our configured one and, if it works, update the call's service URL in the state event
// NB. This wuill update it for everyone so we may end up with multiple clients updating this when they
// join at similar times, but we don't have a huge number of options here.
const urlFromConf = Config.get().livekit!.livekit_service_url;
logger.info(`Trying livekit service URL from our config: ${urlFromConf}...`);
try {
const sfuConfig = await getLiveKitJWT(
client,
urlFromConf,
roomName,
openIdToken
);
logger.info(`Updating call livekit service URL with: ${urlFromConf}...`);
try {
await groupCall.updateLivekitServiceURL(urlFromConf);
} catch (e) {
logger.warn(
`Failed to update call livekit service URL: continuing anyway.`
);
}
return sfuConfig;
} catch (e) {
logger.error("Failed to get JWT from URL defined in Config.", e);
throw e;
}
}
async function getLiveKitJWT(
client: OpenIDClientParts,
livekitServiceURL: string,
roomName: string,
openIDToken: IOpenIDToken
): Promise<SFUConfig> {
try {
const res = await fetch(livekitServiceURL + "/sfu/get", { const res = await fetch(livekitServiceURL + "/sfu/get", {
method: "POST", method: "POST",
headers: { headers: {
@@ -43,12 +101,15 @@ export async function getSFUConfigWithOpenID(
}, },
body: JSON.stringify({ body: JSON.stringify({
room: roomName, room: roomName,
openid_token: openIdToken, openid_token: openIDToken,
device_id: client.getDeviceId(), device_id: client.getDeviceId(),
}), }),
}); });
if (!res.ok) { if (!res.ok) {
throw new Error("SFO Config fetch failed with status code " + res.status); throw new Error("SFU Config fetch failed with status code " + res.status);
} }
return await res.json(); return await res.json();
} catch (e) {
throw new Error("SFU Config fetch failed with exception " + e);
}
} }

View File

@@ -14,16 +14,12 @@ const defaultLiveKitPublishOptions: TrackPublishDefaults = {
red: true, red: true,
forceStereo: false, forceStereo: false,
simulcast: true, simulcast: true,
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h216] as VideoPreset[], videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360] as VideoPreset[],
screenShareEncoding: ScreenSharePresets.h1080fps30.encoding, screenShareEncoding: ScreenSharePresets.h1080fps30.encoding,
screenShareSimulcastLayers: [
new VideoPreset(1920, 1080, 1_500_000, 5, "medium"),
ScreenSharePresets.h1080fps15,
] as VideoPreset[],
stopMicTrackOnMute: false, stopMicTrackOnMute: false,
videoCodec: "vp8", videoCodec: "vp8",
videoEncoding: VideoPresets.h360.encoding, videoEncoding: VideoPresets.h720.encoding,
backupCodec: { codec: "vp8", encoding: VideoPresets.h360.encoding }, backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding },
} as const; } as const;
export const defaultLiveKitOptions: RoomOptions = { export const defaultLiveKitOptions: RoomOptions = {
@@ -35,7 +31,7 @@ export const defaultLiveKitOptions: RoomOptions = {
// capture settings // capture settings
videoCaptureDefaults: { videoCaptureDefaults: {
resolution: VideoPresets.h360.resolution, resolution: VideoPresets.h720.resolution,
}, },
// publish settings // publish settings

View File

@@ -17,7 +17,7 @@ export type DeviceChoices = {
export function useLiveKit( export function useLiveKit(
userChoices: UserChoices, userChoices: UserChoices,
sfuConfig: SFUConfig sfuConfig?: SFUConfig
): Room | undefined { ): Room | undefined {
const roomOptions = useMemo((): RoomOptions => { const roomOptions = useMemo((): RoomOptions => {
const options = defaultLiveKitOptions; const options = defaultLiveKitOptions;
@@ -33,8 +33,8 @@ export function useLiveKit(
}, [userChoices.video, userChoices.audio]); }, [userChoices.video, userChoices.audio]);
const { room } = useLiveKitRoom({ const { room } = useLiveKitRoom({
token: sfuConfig.jwt, token: sfuConfig?.jwt,
serverUrl: sfuConfig.url, serverUrl: sfuConfig?.url,
audio: userChoices.audio?.enabled ?? false, audio: userChoices.audio?.enabled ?? false,
video: userChoices.video?.enabled ?? false, video: userChoices.video?.enabled ?? false,
options: roomOptions, options: roomOptions,

View File

@@ -1,5 +1,5 @@
import { useMediaDeviceSelect } from "@livekit/components-react"; import { useMediaDeviceSelect } from "@livekit/components-react";
import { Room } from "livekit-client"; import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client";
import { useEffect } from "react"; import { useEffect } from "react";
import { useDefaultDevices } from "../settings/useSetting"; import { useDefaultDevices } from "../settings/useSetting";
@@ -17,12 +17,21 @@ export type MediaDevicesState = {
}; };
// if a room is passed this only affects the device selection inside a call. Without room it changes what we see in the lobby // if a room is passed this only affects the device selection inside a call. Without room it changes what we see in the lobby
export function useMediaDevices(room?: Room): MediaDevicesState { export function useMediaDevicesSwitcher(
room?: Room,
tracks?: { videoTrack?: LocalVideoTrack; audioTrack?: LocalAudioTrack },
requestPermissions = true
): MediaDevicesState {
const { const {
devices: videoDevices, devices: videoDevices,
activeDeviceId: activeVideoDevice, activeDeviceId: activeVideoDevice,
setActiveMediaDevice: setActiveVideoDevice, setActiveMediaDevice: setActiveVideoDevice,
} = useMediaDeviceSelect({ kind: "videoinput", room }); } = useMediaDeviceSelect({
kind: "videoinput",
room,
track: tracks?.videoTrack,
requestPermissions,
});
const { const {
devices: audioDevices, devices: audioDevices,
@@ -31,6 +40,8 @@ export function useMediaDevices(room?: Room): MediaDevicesState {
} = useMediaDeviceSelect({ } = useMediaDeviceSelect({
kind: "audioinput", kind: "audioinput",
room, room,
track: tracks?.audioTrack,
requestPermissions,
}); });
const { const {

View File

@@ -19,8 +19,7 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store"; import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store"; import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
import { createClient } from "matrix-js-sdk/src/matrix"; import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
import { ClientEvent } from "matrix-js-sdk/src/client"; import { ClientEvent } from "matrix-js-sdk/src/client";
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
@@ -57,8 +56,8 @@ function waitForSync(client: MatrixClient) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const onSync = ( const onSync = (
state: SyncState, state: SyncState,
_old: SyncState, _old: SyncState | null,
data: ISyncStateData data?: ISyncStateData
) => { ) => {
if (state === "PREPARED") { if (state === "PREPARED") {
resolve(); resolve();
@@ -87,7 +86,7 @@ export async function initClient(
): Promise<MatrixClient> { ): Promise<MatrixClient> {
await loadOlm(); await loadOlm();
let indexedDB: IDBFactory; let indexedDB: IDBFactory | undefined;
try { try {
indexedDB = window.indexedDB; indexedDB = window.indexedDB;
} catch (e) {} } catch (e) {}
@@ -247,7 +246,7 @@ export function sanitiseRoomNameInput(input: string): string {
*/ */
export function roomNameFromRoomId(roomId: string): string { export function roomNameFromRoomId(roomId: string): string {
return roomId return roomId
.match(/([^:]+):.*$/)[1] .match(/([^:]+):.*$/)![1]
.substring(1) .substring(1)
.split("-") .split("-")
.map((part) => .map((part) =>
@@ -262,7 +261,7 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
return false; return false;
} }
const parts = roomId.match(/[^:]+:(.*)$/); const parts = roomId.match(/[^:]+:(.*)$/)!;
if (parts.length < 2) { if (parts.length < 2) {
return false; return false;
@@ -302,7 +301,7 @@ export async function createRoom(
"org.matrix.msc3401.call.member": 0, "org.matrix.msc3401.call.member": 0,
}, },
users: { users: {
[client.getUserId()]: 100, [client.getUserId()!]: 100,
}, },
}, },
}); });

View File

@@ -61,46 +61,46 @@ export class OTelCall {
} }
public dispose() { public dispose() {
this.call.peerConn.removeEventListener( this.call.peerConn?.removeEventListener(
"connectionstatechange", "connectionstatechange",
this.onCallConnectionStateChanged this.onCallConnectionStateChanged
); );
this.call.peerConn.removeEventListener( this.call.peerConn?.removeEventListener(
"signalingstatechange", "signalingstatechange",
this.onCallSignalingStateChanged this.onCallSignalingStateChanged
); );
this.call.peerConn.removeEventListener( this.call.peerConn?.removeEventListener(
"iceconnectionstatechange", "iceconnectionstatechange",
this.onIceConnectionStateChanged this.onIceConnectionStateChanged
); );
this.call.peerConn.removeEventListener( this.call.peerConn?.removeEventListener(
"icegatheringstatechange", "icegatheringstatechange",
this.onIceGatheringStateChanged this.onIceGatheringStateChanged
); );
this.call.peerConn.removeEventListener( this.call.peerConn?.removeEventListener(
"icecandidateerror", "icecandidateerror",
this.onIceCandidateError this.onIceCandidateError
); );
} }
private addCallPeerConnListeners = (): void => { private addCallPeerConnListeners = (): void => {
this.call.peerConn.addEventListener( this.call.peerConn?.addEventListener(
"connectionstatechange", "connectionstatechange",
this.onCallConnectionStateChanged this.onCallConnectionStateChanged
); );
this.call.peerConn.addEventListener( this.call.peerConn?.addEventListener(
"signalingstatechange", "signalingstatechange",
this.onCallSignalingStateChanged this.onCallSignalingStateChanged
); );
this.call.peerConn.addEventListener( this.call.peerConn?.addEventListener(
"iceconnectionstatechange", "iceconnectionstatechange",
this.onIceConnectionStateChanged this.onIceConnectionStateChanged
); );
this.call.peerConn.addEventListener( this.call.peerConn?.addEventListener(
"icegatheringstatechange", "icegatheringstatechange",
this.onIceGatheringStateChanged this.onIceGatheringStateChanged
); );
this.call.peerConn.addEventListener( this.call.peerConn?.addEventListener(
"icecandidateerror", "icecandidateerror",
this.onIceCandidateError this.onIceCandidateError
); );
@@ -108,25 +108,25 @@ export class OTelCall {
public onCallConnectionStateChanged = (): void => { public onCallConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.callConnectionStateChange", { this.span.addEvent("matrix.call.callConnectionStateChange", {
callConnectionState: this.call.peerConn.connectionState, callConnectionState: this.call.peerConn?.connectionState,
}); });
}; };
public onCallSignalingStateChanged = (): void => { public onCallSignalingStateChanged = (): void => {
this.span.addEvent("matrix.call.callSignalingStateChange", { this.span.addEvent("matrix.call.callSignalingStateChange", {
callSignalingState: this.call.peerConn.signalingState, callSignalingState: this.call.peerConn?.signalingState,
}); });
}; };
public onIceConnectionStateChanged = (): void => { public onIceConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.iceConnectionStateChange", { this.span.addEvent("matrix.call.iceConnectionStateChange", {
iceConnectionState: this.call.peerConn.iceConnectionState, iceConnectionState: this.call.peerConn?.iceConnectionState,
}); });
}; };
public onIceGatheringStateChanged = (): void => { public onIceGatheringStateChanged = (): void => {
this.span.addEvent("matrix.call.iceGatheringStateChange", { this.span.addEvent("matrix.call.iceGatheringStateChange", {
iceGatheringState: this.call.peerConn.iceGatheringState, iceGatheringState: this.call.peerConn?.iceGatheringState,
}); });
}; };

View File

@@ -172,7 +172,7 @@ export class OTelGroupCallMembership {
if ( if (
!userCalls || !userCalls ||
!userCalls.has(callTrackingInfo.deviceId) || !userCalls.has(callTrackingInfo.deviceId) ||
userCalls.get(callTrackingInfo.deviceId).callId !== userCalls.get(callTrackingInfo.deviceId)?.callId !==
callTrackingInfo.call.callId callTrackingInfo.call.callId
) { ) {
callTrackingInfo.end(); callTrackingInfo.end();
@@ -420,7 +420,7 @@ export class OTelGroupCallMembership {
ctx ctx
); );
span.setAttribute("matrix.callId", callId); span.setAttribute("matrix.callId", callId ?? "unknown");
span.setAttribute( span.setAttribute(
"matrix.opponentMemberId", "matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown" report.opponentMemberId ? report.opponentMemberId : "unknown"

View File

@@ -23,7 +23,6 @@ import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor"; import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
import { Anonymity } from "../analytics/PosthogAnalytics";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor"; import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
@@ -34,8 +33,7 @@ let sharedInstance: ElementCallOpenTelemetry;
export class ElementCallOpenTelemetry { export class ElementCallOpenTelemetry {
private _provider: WebTracerProvider; private _provider: WebTracerProvider;
private _tracer: Tracer; private _tracer: Tracer;
private _anonymity: Anonymity; private otlpExporter?: OTLPTraceExporter;
private otlpExporter: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor; public readonly rageshakeProcessor?: RageshakeSpanProcessor;
static globalInit(): void { static globalInit(): void {
@@ -100,7 +98,7 @@ export class ElementCallOpenTelemetry {
} }
public dispose(): void { public dispose(): void {
opentelemetry.trace.setGlobalTracerProvider(null); opentelemetry.trace.disable();
this._provider?.shutdown(); this._provider?.shutdown();
} }
@@ -115,8 +113,4 @@ export class ElementCallOpenTelemetry {
public get provider(): WebTracerProvider { public get provider(): WebTracerProvider {
return this._provider; return this._provider;
} }
public get anonymity(): Anonymity {
return this._anonymity;
}
} }

View File

@@ -46,7 +46,7 @@ export const PopoverMenuTrigger = forwardRef<
buttonRef buttonRef
); );
const popoverRef = useRef(); const popoverRef = useRef(null);
const { overlayProps } = useOverlayPosition({ const { overlayProps } = useOverlayPosition({
targetRef: buttonRef, targetRef: buttonRef,

View File

@@ -21,10 +21,10 @@ import { FileType } from "matrix-js-sdk/src/http-api";
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
interface ProfileLoadState { interface ProfileLoadState {
success?: boolean; success: boolean;
loading?: boolean; loading: boolean;
displayName: string; displayName?: string;
avatarUrl: string; avatarUrl?: string;
error?: Error; error?: Error;
} }
@@ -38,23 +38,26 @@ type ProfileSaveCallback = ({
removeAvatar: boolean; removeAvatar: boolean;
}) => Promise<void>; }) => Promise<void>;
export function useProfile(client: MatrixClient) { export function useProfile(client: MatrixClient | undefined) {
const [{ loading, displayName, avatarUrl, error, success }, setState] = const [{ success, loading, displayName, avatarUrl, error }, setState] =
useState<ProfileLoadState>(() => { useState<ProfileLoadState>(() => {
const user = client?.getUser(client.getUserId()); let user: User | undefined = undefined;
if (client) {
user = client.getUser(client.getUserId()!) ?? undefined;
}
return { return {
success: false, success: false,
loading: false, loading: false,
displayName: user?.rawDisplayName, displayName: user?.rawDisplayName,
avatarUrl: user?.avatarUrl, avatarUrl: user?.avatarUrl,
error: null, error: undefined,
}; };
}); });
useEffect(() => { useEffect(() => {
const onChangeUser = ( const onChangeUser = (
_event: MatrixEvent, _event: MatrixEvent | undefined,
{ displayName, avatarUrl }: User { displayName, avatarUrl }: User
) => { ) => {
setState({ setState({
@@ -62,17 +65,16 @@ export function useProfile(client: MatrixClient) {
loading: false, loading: false,
displayName, displayName,
avatarUrl, avatarUrl,
error: null, error: undefined,
}); });
}; };
let user: User; let user: User | null;
if (client) { if (client) {
const userId = client.getUserId(); const userId = client.getUserId()!;
user = client.getUser(userId); user = client.getUser(userId);
user.on(UserEvent.DisplayName, onChangeUser); user?.on(UserEvent.DisplayName, onChangeUser);
user.on(UserEvent.AvatarUrl, onChangeUser); user?.on(UserEvent.AvatarUrl, onChangeUser);
} }
return () => { return () => {
@@ -89,7 +91,7 @@ export function useProfile(client: MatrixClient) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
loading: true, loading: true,
error: null, error: undefined,
success: false, success: false,
})); }));
@@ -110,7 +112,9 @@ export function useProfile(client: MatrixClient) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
displayName, displayName,
avatarUrl: removeAvatar ? null : mxcAvatarUrl ?? prev.avatarUrl, avatarUrl: removeAvatar
? undefined
: mxcAvatarUrl ?? prev.avatarUrl,
loading: false, loading: false,
success: true, success: true,
})); }));

View File

@@ -38,6 +38,15 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const tooltip = useCallback(() => t("Change layout"), [t]); const tooltip = useCallback(() => t("Change layout"), [t]);
const onAction = useCallback(
(key: React.Key) => {
setLayout(key.toString() as Layout);
},
[setLayout]
);
const onClose = useCallback(() => {}, []);
return ( return (
<PopoverMenuTrigger placement="bottom right"> <PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={tooltip}> <TooltipTrigger tooltip={tooltip}>
@@ -46,7 +55,12 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => ( {(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}> <Menu
{...props}
label={t("Grid layout menu")}
onAction={onAction}
onClose={onClose}
>
<Item key="freedom" textValue={t("Freedom")}> <Item key="freedom" textValue={t("Freedom")}>
<FreedomIcon /> <FreedomIcon />
<span>Freedom</span> <span>Freedom</span>

View File

@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/* /*
Copyright 2022 New Vector Ltd Copyright 2022 New Vector Ltd
@@ -70,7 +73,7 @@ const defaultCollapsedFields = [
]; ];
function shouldCollapse({ name }: CollapsedFieldProps) { function shouldCollapse({ name }: CollapsedFieldProps) {
return defaultCollapsedFields.includes(name); return name ? defaultCollapsedFields.includes(name) : false;
} }
function getUserName(userId: string) { function getUserName(userId: string) {
@@ -196,7 +199,7 @@ export function SequenceDiagramViewer({
onSelectUserId, onSelectUserId,
events, events,
}: SequenceDiagramViewerProps) { }: SequenceDiagramViewerProps) {
const mermaidElRef = useRef<HTMLDivElement>(); const mermaidElRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
mermaid.initialize({ mermaid.initialize({
@@ -217,6 +220,7 @@ export function SequenceDiagramViewer({
`; `;
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => { mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
if (!mermaidElRef.current) return;
mermaidElRef.current.innerHTML = svgCode; mermaidElRef.current.innerHTML = svgCode;
}); });
}, [events, localUserId, selectedUserId]); }, [events, localUserId, selectedUserId]);
@@ -228,7 +232,7 @@ export function SequenceDiagramViewer({
className={styles.selectInput} className={styles.selectInput}
label="Remote User" label="Remote User"
selectedKey={selectedUserId} selectedKey={selectedUserId}
onSelectionChange={onSelectUserId} onSelectionChange={(key) => onSelectUserId(key.toString())}
> >
{remoteUserIds.map((userId) => ( {remoteUserIds.map((userId) => (
<Item key={userId}>{userId}</Item> <Item key={userId}>{userId}</Item>
@@ -498,7 +502,7 @@ export function GroupCallInspector({
return ( return (
<Resizable <Resizable
enable={{ top: true }} enable={{ top: true }}
defaultSize={{ height: 200, width: undefined }} defaultSize={{ height: 200, width: 0 }}
className={styles.inspector} className={styles.inspector}
> >
<div className={styles.toolbar}> <div className={styles.toolbar}>
@@ -507,7 +511,11 @@ export function GroupCallInspector({
</button> </button>
<button onClick={() => setCurrentTab("inspector")}>Inspector</button> <button onClick={() => setCurrentTab("inspector")}>Inspector</button>
</div> </div>
{currentTab === "sequence-diagrams" && ( {currentTab === "sequence-diagrams" &&
state.localUserId &&
selectedUserId &&
state.eventsByUserId &&
state.remoteUserIds && (
<SequenceDiagramViewer <SequenceDiagramViewer
localUserId={state.localUserId} localUserId={state.localUserId}
selectedUserId={selectedUserId} selectedUserId={selectedUserId}

View File

@@ -21,7 +21,6 @@ import { useTranslation } from "react-i18next";
import { useLoadGroupCall } from "./useLoadGroupCall"; import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
import { usePageTitle } from "../usePageTitle";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
@@ -39,26 +38,23 @@ export function GroupCallLoader({
createPtt, createPtt,
}: Props): JSX.Element { }: Props): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const { loading, error, groupCall } = useLoadGroupCall( const groupCallState = useLoadGroupCall(
client, client,
roomIdOrAlias, roomIdOrAlias,
viaServers, viaServers,
createPtt createPtt
); );
usePageTitle(groupCall ? groupCall.room.name : t("Loading…")); switch (groupCallState.kind) {
case "loading":
if (loading) {
return ( return (
<FullScreenView> <FullScreenView>
<h1>{t("Loading…")}</h1> <h1>{t("Loading…")}</h1>
</FullScreenView> </FullScreenView>
); );
case "loaded":
return <>{children(groupCallState.groupCall)}</>;
case "failed":
return <ErrorView error={groupCallState.error} />;
} }
if (error) {
return <ErrorView error={error} />;
}
return <>{children(groupCall)}</>;
} }

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall"; import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
@@ -49,7 +49,6 @@ interface Props {
isEmbedded: boolean; isEmbedded: boolean;
preload: boolean; preload: boolean;
hideHeader: boolean; hideHeader: boolean;
roomIdOrAlias: string;
groupCall: GroupCall; groupCall: GroupCall;
} }
@@ -59,7 +58,6 @@ export function GroupCallView({
isEmbedded, isEmbedded,
preload, preload,
hideHeader, hideHeader,
roomIdOrAlias,
groupCall, groupCall,
}: Props) { }: Props) {
const { const {
@@ -82,13 +80,14 @@ export function GroupCallView({
}, [groupCall]); }, [groupCall]);
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const matrixInfo = useMemo((): MatrixInfo => {
const matrixInfo: MatrixInfo = { return {
displayName, displayName: displayName!,
avatarUrl, avatarUrl: avatarUrl!,
roomId: groupCall.room.roomId,
roomName: groupCall.room.name, roomName: groupCall.room.name,
roomIdOrAlias,
}; };
}, [displayName, avatarUrl, groupCall]);
useEffect(() => { useEffect(() => {
if (widget && preload) { if (widget && preload) {
@@ -139,14 +138,14 @@ export function GroupCallView({
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
await Promise.all([ await Promise.all([
widget.api.setAlwaysOnScreen(true), widget!.api.setAlwaysOnScreen(true),
widget.api.transport.reply(ev.detail, {}), widget!.api.transport.reply(ev.detail, {}),
]); ]);
}; };
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin); widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
return () => { return () => {
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
}; };
} }
}, [groupCall, preload, enter]); }, [groupCall, preload, enter]);
@@ -205,12 +204,12 @@ export function GroupCallView({
if (widget && state === GroupCallState.Entered) { if (widget && state === GroupCallState.Entered) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => { const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leave(); leave();
await widget.api.transport.reply(ev.detail, {}); await widget!.api.transport.reply(ev.detail, {});
widget.api.setAlwaysOnScreen(false); widget!.api.setAlwaysOnScreen(false);
}; };
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup); widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
return () => { return () => {
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup); widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
}; };
} }
}, [groupCall, state, leave]); }, [groupCall, state, leave]);
@@ -219,26 +218,14 @@ export function GroupCallView({
undefined undefined
); );
const [livekitServiceURL, setLivekitServiceURL] = useState<
string | undefined
>(groupCall.foci[0]?.livekitServiceUrl);
useEffect(() => {
setLivekitServiceURL(groupCall.foci[0]?.livekitServiceUrl);
}, [setLivekitServiceURL, groupCall]);
if (!livekitServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
if (error) { if (error) {
return <ErrorView error={error} />; return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered && userChoices) { } else if (state === GroupCallState.Entered && userChoices) {
return ( return (
<OpenIDLoader <OpenIDLoader
client={client} client={client}
livekitServiceURL={livekitServiceURL} groupCall={groupCall}
roomName={matrixInfo.roomName} roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
> >
<ActiveCall <ActiveCall
client={client} client={client}
@@ -247,7 +234,6 @@ export function GroupCallView({
onLeave={onLeave} onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers} unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader} hideHeader={hideHeader}
matrixInfo={matrixInfo}
userChoices={userChoices} userChoices={userChoices}
otelGroupCallMembership={otelGroupCallMembership} otelGroupCallMembership={otelGroupCallMembership}
/> />

View File

@@ -68,22 +68,21 @@ import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu"; import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector"; import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css"; import styles from "./InCallView.module.css";
import { MatrixInfo } from "./VideoPreview";
import { useJoinRule } from "./useJoinRule"; import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall"; import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent } from "../video-grid/VideoTile"; import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid"; import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal"; import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal"; import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { VideoTile } from "../video-grid/VideoTile";
import { UserChoices, useLiveKit } from "../livekit/useLiveKit"; import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { useMediaDevices } from "../livekit/useMediaDevices"; import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
import { useFullscreen } from "./useFullscreen"; import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout"; import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader"; import { useSFUConfig } from "../livekit/OpenIDLoader";
import { E2EELock } from "../E2EELock";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -99,12 +98,14 @@ export function ActiveCall(props: ActiveCallProps) {
const sfuConfig = useSFUConfig(); const sfuConfig = useSFUConfig();
const livekitRoom = useLiveKit(props.userChoices, sfuConfig); const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
if (!livekitRoom) {
return null;
}
return ( return (
livekitRoom && (
<RoomContext.Provider value={livekitRoom}> <RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} /> <InCallView {...props} livekitRoom={livekitRoom} />
</RoomContext.Provider> </RoomContext.Provider>
)
); );
} }
@@ -116,8 +117,7 @@ export interface InCallViewProps {
onLeave: () => void; onLeave: () => void;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean; hideHeader: boolean;
matrixInfo: MatrixInfo; otelGroupCallMembership?: OTelGroupCallMembership;
otelGroupCallMembership: OTelGroupCallMembership;
} }
export function InCallView({ export function InCallView({
@@ -128,7 +128,6 @@ export function InCallView({
onLeave, onLeave,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
hideHeader, hideHeader,
matrixInfo,
otelGroupCallMembership, otelGroupCallMembership,
}: InCallViewProps) { }: InCallViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -147,7 +146,7 @@ export function InCallView({
); );
// Managed media devices state coupled with an active room. // Managed media devices state coupled with an active room.
const roomMediaDevices = useMediaDevices(livekitRoom); const roomMediaSwitcher = useMediaDevicesSwitcher(livekitRoom);
const screenSharingTracks = useTracks( const screenSharingTracks = useTracks(
[{ source: Track.Source.ScreenShare, withPlaceholder: false }], [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
@@ -202,11 +201,11 @@ export function InCallView({
if (widget) { if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => { const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("freedom"); setLayout("freedom");
await widget.api.transport.reply(ev.detail, {}); await widget!.api.transport.reply(ev.detail, {});
}; };
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => { const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("spotlight"); setLayout("spotlight");
await widget.api.transport.reply(ev.detail, {}); await widget!.api.transport.reply(ev.detail, {});
}; };
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout); widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
@@ -216,8 +215,8 @@ export function InCallView({
); );
return () => { return () => {
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout); widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.off( widget!.lazyActions.off(
ElementWidgetActions.SpotlightLayout, ElementWidgetActions.SpotlightLayout,
onSpotlightLayout onSpotlightLayout
); );
@@ -340,7 +339,12 @@ export function InCallView({
const toggleScreensharing = useCallback(async () => { const toggleScreensharing = useCallback(async () => {
exitFullscreen(); exitFullscreen();
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled); await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
audio: true,
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
});
}, [localParticipant, isScreenShareEnabled, exitFullscreen]); }, [localParticipant, isScreenShareEnabled, exitFullscreen]);
let footer: JSX.Element | null; let footer: JSX.Element | null;
@@ -390,11 +394,12 @@ export function InCallView({
{!hideHeader && maximisedParticipant === null && ( {!hideHeader && maximisedParticipant === null && (
<Header> <Header>
<LeftNav> <LeftNav>
<RoomHeaderInfo roomName={matrixInfo.roomName} /> <RoomHeaderInfo roomName={groupCall.room.name} />
<VersionMismatchWarning <VersionMismatchWarning
users={unencryptedEventsFromUsers} users={unencryptedEventsFromUsers}
room={groupCall.room} room={groupCall.room}
/> />
<E2EELock />
</LeftNav> </LeftNav>
<RightNav> <RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} /> <GridLayoutMenu layout={layout} setLayout={setLayout} />
@@ -409,31 +414,30 @@ export function InCallView({
{renderContent()} {renderContent()}
{footer} {footer}
</div> </div>
{otelGroupCallMembership && (
<GroupCallInspector <GroupCallInspector
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership} otelGroupCallMembership={otelGroupCallMembership}
show={showInspector} show={showInspector}
/> />
)}
{rageshakeRequestModalState.isOpen && !noControls && ( {rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal <RageshakeRequestModal
{...rageshakeRequestModalProps} {...rageshakeRequestModalProps}
roomIdOrAlias={matrixInfo.roomIdOrAlias} roomId={groupCall.room.roomId}
/> />
)} )}
{settingsModalState.isOpen && ( {settingsModalState.isOpen && (
<SettingsModal <SettingsModal
client={client} client={client}
roomId={groupCall.room.roomId} roomId={groupCall.room.roomId}
mediaDevices={roomMediaDevices} mediaDevicesSwitcher={roomMediaSwitcher}
{...settingsModalProps} {...settingsModalProps}
/> />
)} )}
{inviteModalState.isOpen && ( {inviteModalState.isOpen && (
<InviteModal <InviteModal roomId={groupCall.room.roomId} {...inviteModalProps} />
roomIdOrAlias={matrixInfo.roomIdOrAlias}
{...inviteModalProps}
/>
)} )}
</div> </div>
); );

View File

@@ -23,10 +23,10 @@ import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css"; import styles from "./InviteModal.module.css";
interface Props extends Omit<ModalProps, "title" | "children"> { interface Props extends Omit<ModalProps, "title" | "children"> {
roomIdOrAlias: string; roomId: string;
} }
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => { export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
<p>{t("Copy and share this call link")}</p> <p>{t("Copy and share this call link")}</p>
<CopyButton <CopyButton
className={styles.copyButton} className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)} value={getRoomUrl(roomId)}
data-testid="modal_inviteLink" data-testid="modal_inviteLink"
/> />
</ModalContent> </ModalContent>

View File

@@ -39,7 +39,7 @@ export function LobbyView(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
useLocationNavigation(); useLocationNavigation();
const joinCallButtonRef = useRef<HTMLButtonElement>(); const joinCallButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => { useEffect(() => {
if (joinCallButtonRef.current) { if (joinCallButtonRef.current) {
joinCallButtonRef.current.focus(); joinCallButtonRef.current.focus();
@@ -81,7 +81,7 @@ export function LobbyView(props: Props) {
<Body>Or</Body> <Body>Or</Body>
<CopyButton <CopyButton
variant="secondaryCopy" variant="secondaryCopy"
value={getRoomUrl(props.matrixInfo.roomName)} value={getRoomUrl(props.matrixInfo.roomId)}
className={styles.copyButton} className={styles.copyButton}
copiedMessage={t("Call link copied")} copiedMessage={t("Call link copied")}
data-testid="lobby_inviteLink" data-testid="lobby_inviteLink"

View File

@@ -25,13 +25,13 @@ import { Body } from "../typography/Typography";
interface Props extends Omit<ModalProps, "title" | "children"> { interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId: string; rageshakeRequestId: string;
roomIdOrAlias: string; roomId: string;
onClose: () => void; onClose: () => void;
} }
export const RageshakeRequestModal: FC<Props> = ({ export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId, rageshakeRequestId,
roomIdOrAlias, roomId,
...rest ...rest
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -57,7 +57,7 @@ export const RageshakeRequestModal: FC<Props> = ({
submitRageshake({ submitRageshake({
sendLogs: true, sendLogs: true,
rageshakeRequestId, rageshakeRequestId,
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well roomId,
}) })
} }
disabled={sending} disabled={sending}

View File

@@ -26,15 +26,18 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Form } from "../form/Form"; import { Form } from "../form/Form";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { Config } from "../config/Config";
export function RoomAuthView() { export function RoomAuthView() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } = const { registerPasswordlessUser, recaptchaId } =
useRegisterPasswordlessUser(); useRegisterPasswordlessUser();
const onSubmit = useCallback( const onSubmit = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
const data = new FormData(e.target); const data = new FormData(e.target);
@@ -83,7 +86,7 @@ export function RoomAuthView() {
<Caption> <Caption>
<Trans> <Trans>
By clicking "Join call now", you agree to our{" "} By clicking "Join call now", you agree to our{" "}
<Link href={privacyPolicyUrl}> <Link href={Config.get().eula}>
End User Licensing Agreement (EULA) End User Licensing Agreement (EULA)
</Link> </Link>
</Trans> </Trans>

View File

@@ -18,7 +18,7 @@ import { FC, useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useClient } from "../ClientContext"; import { useClientLegacy } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallLoader } from "./GroupCallLoader";
@@ -30,8 +30,6 @@ import { useOptInAnalytics } from "../settings/useSetting";
export const RoomPage: FC = () => { export const RoomPage: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
const { const {
roomAlias, roomAlias,
@@ -52,39 +50,41 @@ export const RoomPage: FC = () => {
useEffect(() => { useEffect(() => {
// During the beta, opt into analytics by default // During the beta, opt into analytics by default
if (optInAnalytics === null) setOptInAnalytics(true); if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
}, [optInAnalytics, setOptInAnalytics]); }, [optInAnalytics, setOptInAnalytics]);
const { loading, authenticated, client, error, passwordlessUser } =
useClientLegacy();
useEffect(() => { useEffect(() => {
// If we've finished loading, are not already authed and we've been given a display name as // If we've finished loading, are not already authed and we've been given a display name as
// a URL param, automatically register a passwordless user // a URL param, automatically register a passwordless user
if (!loading && !isAuthenticated && displayName) { if (!loading && !authenticated && displayName) {
setIsRegistering(true); setIsRegistering(true);
registerPasswordlessUser(displayName).finally(() => { registerPasswordlessUser(displayName).finally(() => {
setIsRegistering(false); setIsRegistering(false);
}); });
} }
}, [ }, [
isAuthenticated, loading,
authenticated,
displayName, displayName,
setIsRegistering, setIsRegistering,
registerPasswordlessUser, registerPasswordlessUser,
loading,
]); ]);
const groupCallView = useCallback( const groupCallView = useCallback(
(groupCall: GroupCall) => ( (groupCall: GroupCall) => (
<GroupCallView <GroupCallView
client={client} client={client!}
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall} groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser} isPasswordlessUser={passwordlessUser}
isEmbedded={isEmbedded} isEmbedded={isEmbedded}
preload={preload} preload={preload}
hideHeader={hideHeader} hideHeader={hideHeader}
/> />
), ),
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader] [client, passwordlessUser, isEmbedded, preload, hideHeader]
); );
if (loading || isRegistering) { if (loading || isRegistering) {
@@ -95,7 +95,7 @@ export const RoomPage: FC = () => {
return <ErrorView error={error} />; return <ErrorView error={error} />;
} }
if (!isAuthenticated) { if (!client) {
return <RoomAuthView />; return <RoomAuthView />;
} }

View File

@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect, useCallback, useRef } from "react";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
import { OverlayTriggerState } from "@react-stately/overlays"; import { OverlayTriggerState } from "@react-stately/overlays";
import { usePreviewDevice } from "@livekit/components-react"; import { usePreviewTracks } from "@livekit/components-react";
import { LocalAudioTrack, LocalVideoTrack, Track } from "livekit-client";
import { MicButton, SettingsButton, VideoButton } from "../button"; import { MicButton, SettingsButton, VideoButton } from "../button";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
@@ -26,15 +27,15 @@ import styles from "./VideoPreview.module.css";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal"; import { SettingsModal } from "../settings/SettingsModal";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { useMediaDevices } from "../livekit/useMediaDevices"; import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
import { DeviceChoices, UserChoices } from "../livekit/useLiveKit"; import { UserChoices } from "../livekit/useLiveKit";
import { useDefaultDevices } from "../settings/useSetting"; import { useDefaultDevices } from "../settings/useSetting";
export type MatrixInfo = { export type MatrixInfo = {
displayName: string; displayName: string;
avatarUrl: string; avatarUrl: string;
roomId: string;
roomName: string; roomName: string;
roomIdOrAlias: string;
}; };
interface Props { interface Props {
@@ -61,85 +62,111 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
settingsModalState.open(); settingsModalState.open();
}, [settingsModalState]); }, [settingsModalState]);
// Fetch user media devices.
const mediaDevices = useMediaDevices();
// Create local media tracks. // Create local media tracks.
const [videoEnabled, setVideoEnabled] = useState<boolean>(true); const [videoEnabled, setVideoEnabled] = useState<boolean>(true);
const [audioEnabled, setAudioEnabled] = useState<boolean>(true); const [audioEnabled, setAudioEnabled] = useState<boolean>(true);
const [videoId, audioId] = [
mediaDevices.videoIn.selectedId,
mediaDevices.audioIn.selectedId,
];
const [defaultDevices] = useDefaultDevices();
const video = usePreviewDevice(
videoEnabled,
videoId != "" ? videoId : defaultDevices.videoinput,
"videoinput"
);
const audio = usePreviewDevice(
audioEnabled,
audioId != "" ? audioId : defaultDevices.audioinput,
"audioinput"
);
const activeVideoId = video?.selectedDevice?.deviceId; // The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value.
const activeAudioId = audio?.selectedDevice?.deviceId; // Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks.
useEffect(() => { const initialDefaultDevices = useRef(useDefaultDevices()[0]);
const createChoices = ( const tracks = usePreviewTracks(
enabled: boolean, {
deviceId?: string audio: { deviceId: initialDefaultDevices.current.audioinput },
): DeviceChoices | undefined => { video: { deviceId: initialDefaultDevices.current.videoinput },
if (deviceId === undefined) { },
return undefined; (error) => {
console.error("Error while creating preview Tracks:", error);
} }
);
const videoTrack = React.useMemo(
() =>
tracks?.filter((t) => t.kind === Track.Kind.Video)[0] as LocalVideoTrack,
[tracks]
);
const audioTrack = React.useMemo(
() =>
tracks?.filter((t) => t.kind === Track.Kind.Audio)[0] as LocalAudioTrack,
[tracks]
);
return { // Only let the MediaDeviceSwitcher request permissions if a video track is already available.
selectedId: deviceId, // Otherwise we would end up asking for permissions in usePreviewTracks and in useMediaDevicesSwitcher.
enabled, const requestPermissions = !!audioTrack && !!videoTrack;
}; const mediaSwitcher = useMediaDevicesSwitcher(
}; undefined,
{ videoTrack, audioTrack },
requestPermissions
);
const { videoIn, audioIn } = mediaSwitcher;
const videoEl = React.useRef(null);
useEffect(() => {
// Effect to update the settings
onUserChoicesChanged({ onUserChoicesChanged({
video: createChoices(videoEnabled, activeVideoId), video: {
audio: createChoices(audioEnabled, activeAudioId), selectedId: videoIn.selectedId,
enabled: videoEnabled && !!videoTrack,
},
audio: {
selectedId: audioIn.selectedId,
enabled: audioEnabled && !!audioTrack,
},
}); });
}, [ }, [
onUserChoicesChanged, onUserChoicesChanged,
activeVideoId, videoIn.selectedId,
videoEnabled, videoEnabled,
activeAudioId, audioIn.selectedId,
audioEnabled, audioEnabled,
videoTrack,
audioTrack,
]); ]);
const [selectVideo, selectAudio] = [
mediaDevices.videoIn.setSelected,
mediaDevices.audioIn.setSelected,
];
useEffect(() => { useEffect(() => {
if (activeVideoId && activeVideoId !== "") { // Effect to update the initial device selection for the ui elements based on the current preview track.
selectVideo(activeVideoId); if (!videoIn.selectedId || videoIn.selectedId == "") {
videoTrack?.getDeviceId().then((videoId) => {
videoIn.setSelected(videoId ?? "default");
});
} }
if (activeAudioId && activeAudioId !== "") { if (!audioIn.selectedId || audioIn.selectedId == "") {
selectAudio(activeAudioId); audioTrack?.getDeviceId().then((audioId) => {
// getDeviceId() can return undefined for audio devices. This happens if
// the devices list uses "default" as the device id for the current
// device and the device set on the track also uses the deviceId
// "default". Check `normalizeDeviceId` in `getDeviceId` for more
// details.
audioIn.setSelected(audioId ?? "default");
});
} }
}, [selectVideo, selectAudio, activeVideoId, activeAudioId]); }, [videoIn, audioIn, videoTrack, audioTrack]);
const mediaElement = useRef(null);
useEffect(() => { useEffect(() => {
if (mediaElement.current) { // Effect to connect the videoTrack with the video element.
video?.localTrack?.attach(mediaElement.current); if (videoEl.current) {
videoTrack?.unmute();
videoTrack?.attach(videoEl.current);
} }
return () => { return () => {
video?.localTrack?.detach(); videoTrack?.detach();
}; };
}, [video?.localTrack, mediaElement]); }, [videoTrack]);
useEffect(() => {
// Effect to mute/unmute video track. (This has to be done, so that the hardware camera indicator does not confuse the user)
if (videoTrack && videoEnabled) {
videoTrack?.unmute();
} else if (videoTrack) {
videoTrack?.mute();
}
}, [videoEnabled, videoTrack]);
return ( return (
<div className={styles.preview} ref={previewRef}> <div className={styles.preview} ref={previewRef}>
<video ref={mediaElement} muted playsInline disablePictureInPicture /> <video ref={videoEl} muted playsInline disablePictureInPicture />
<> <>
{(video ? !videoEnabled : true) && ( {(videoTrack ? !videoEnabled : true) && (
<div className={styles.avatarContainer}> <div className={styles.avatarContainer}>
<Avatar <Avatar
size={(previewBounds.height - 66) / 2} size={(previewBounds.height - 66) / 2}
@@ -149,25 +176,23 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
</div> </div>
)} )}
<div className={styles.previewButtons}> <div className={styles.previewButtons}>
{audio.localTrack && (
<MicButton <MicButton
muted={!audioEnabled} muted={!audioEnabled}
onPress={() => setAudioEnabled(!audioEnabled)} onPress={() => setAudioEnabled(!audioEnabled)}
disabled={!audioTrack}
/> />
)}
{video.localTrack && (
<VideoButton <VideoButton
muted={!videoEnabled} muted={!videoEnabled}
onPress={() => setVideoEnabled(!videoEnabled)} onPress={() => setVideoEnabled(!videoEnabled)}
disabled={!videoTrack}
/> />
)}
<SettingsButton onPress={openSettings} /> <SettingsButton onPress={openSettings} />
</div> </div>
</> </>
{settingsModalState.isOpen && ( {settingsModalState.isOpen && client && (
<SettingsModal <SettingsModal
client={client} client={client}
mediaDevices={mediaDevices} mediaDevicesSwitcher={mediaSwitcher}
{...settingsModalProps} {...settingsModalProps}
/> />
)} )}

View File

@@ -58,12 +58,12 @@ export interface ParticipantInfo {
interface UseGroupCallReturnType { interface UseGroupCallReturnType {
state: GroupCallState; state: GroupCallState;
localCallFeed: CallFeed; localCallFeed?: CallFeed;
activeSpeaker: CallFeed | null; activeSpeaker?: CallFeed;
userMediaFeeds: CallFeed[]; userMediaFeeds: CallFeed[];
microphoneMuted: boolean; microphoneMuted: boolean;
localVideoMuted: boolean; localVideoMuted: boolean;
error: TranslatedError | null; error?: TranslatedError;
initLocalCallFeed: () => void; initLocalCallFeed: () => void;
enter: () => Promise<void>; enter: () => Promise<void>;
leave: () => void; leave: () => void;
@@ -74,23 +74,21 @@ interface UseGroupCallReturnType {
requestingScreenshare: boolean; requestingScreenshare: boolean;
isScreensharing: boolean; isScreensharing: boolean;
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
localDesktopCapturerSourceId: string; // XXX: This looks unused?
participants: Map<RoomMember, Map<string, ParticipantInfo>>; participants: Map<RoomMember, Map<string, ParticipantInfo>>;
hasLocalParticipant: boolean; hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
otelGroupCallMembership: OTelGroupCallMembership; otelGroupCallMembership?: OTelGroupCallMembership;
} }
interface State { interface State {
state: GroupCallState; state: GroupCallState;
localCallFeed: CallFeed; localCallFeed?: CallFeed;
activeSpeaker: CallFeed | null; activeSpeaker?: CallFeed;
userMediaFeeds: CallFeed[]; userMediaFeeds: CallFeed[];
error: TranslatedError | null; error?: TranslatedError;
microphoneMuted: boolean; microphoneMuted: boolean;
localVideoMuted: boolean; localVideoMuted: boolean;
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
localDesktopCapturerSourceId: string;
isScreensharing: boolean; isScreensharing: boolean;
requestingScreenshare: boolean; requestingScreenshare: boolean;
participants: Map<RoomMember, Map<string, ParticipantInfo>>; participants: Map<RoomMember, Map<string, ParticipantInfo>>;
@@ -101,7 +99,7 @@ interface State {
// level so that it doesn't pop in & out of existence as react mounts & unmounts // level so that it doesn't pop in & out of existence as react mounts & unmounts
// components. The right solution is probably for this to live in the js-sdk and have // components. The right solution is probably for this to live in the js-sdk and have
// the same lifetime as groupcalls themselves. // the same lifetime as groupcalls themselves.
let groupCallOTelMembership: OTelGroupCallMembership; let groupCallOTelMembership: OTelGroupCallMembership | undefined;
let groupCallOTelMembershipGroupCallId: string; let groupCallOTelMembershipGroupCallId: string;
function getParticipants( function getParticipants(
@@ -159,7 +157,6 @@ export function useGroupCall(
localVideoMuted, localVideoMuted,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localDesktopCapturerSourceId,
participants, participants,
hasLocalParticipant, hasLocalParticipant,
requestingScreenshare, requestingScreenshare,
@@ -167,15 +164,11 @@ export function useGroupCall(
setState, setState,
] = useState<State>({ ] = useState<State>({
state: GroupCallState.LocalCallFeedUninitialized, state: GroupCallState.LocalCallFeedUninitialized,
localCallFeed: null,
activeSpeaker: null,
userMediaFeeds: [], userMediaFeeds: [],
error: null,
microphoneMuted: false, microphoneMuted: false,
localVideoMuted: false, localVideoMuted: false,
isScreensharing: false, isScreensharing: false,
screenshareFeeds: [], screenshareFeeds: [],
localDesktopCapturerSourceId: null,
requestingScreenshare: false, requestingScreenshare: false,
participants: new Map(), participants: new Map(),
hasLocalParticipant: false, hasLocalParticipant: false,
@@ -248,12 +241,11 @@ export function useGroupCall(
updateState({ updateState({
state: groupCall.state, state: groupCall.state,
localCallFeed: groupCall.localCallFeed, localCallFeed: groupCall.localCallFeed,
activeSpeaker: groupCall.activeSpeaker ?? null, activeSpeaker: groupCall.activeSpeaker,
userMediaFeeds: [...groupCall.userMediaFeeds], userMediaFeeds: [...groupCall.userMediaFeeds],
microphoneMuted: groupCall.isMicrophoneMuted(), microphoneMuted: groupCall.isMicrophoneMuted(),
localVideoMuted: groupCall.isLocalVideoMuted(), localVideoMuted: groupCall.isLocalVideoMuted(),
isScreensharing: groupCall.isScreensharing(), isScreensharing: groupCall.isScreensharing(),
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
screenshareFeeds: [...groupCall.screenshareFeeds], screenshareFeeds: [...groupCall.screenshareFeeds],
}); });
} }
@@ -303,7 +295,7 @@ export function useGroupCall(
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void { function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
updateState({ updateState({
activeSpeaker: activeSpeaker ?? null, activeSpeaker: activeSpeaker,
}); });
} }
@@ -319,12 +311,11 @@ export function useGroupCall(
function onLocalScreenshareStateChanged( function onLocalScreenshareStateChanged(
isScreensharing: boolean, isScreensharing: boolean,
_localScreenshareFeed: CallFeed, _localScreenshareFeed?: CallFeed,
localDesktopCapturerSourceId: string localDesktopCapturerSourceId?: string
): void { ): void {
updateState({ updateState({
isScreensharing, isScreensharing,
localDesktopCapturerSourceId,
}); });
} }
@@ -405,15 +396,14 @@ export function useGroupCall(
); );
updateState({ updateState({
error: null, error: undefined,
state: groupCall.state, state: groupCall.state,
localCallFeed: groupCall.localCallFeed, localCallFeed: groupCall.localCallFeed,
activeSpeaker: groupCall.activeSpeaker ?? null, activeSpeaker: groupCall.activeSpeaker,
userMediaFeeds: [...groupCall.userMediaFeeds], userMediaFeeds: [...groupCall.userMediaFeeds],
microphoneMuted: groupCall.isMicrophoneMuted(), microphoneMuted: groupCall.isMicrophoneMuted(),
localVideoMuted: groupCall.isLocalVideoMuted(), localVideoMuted: groupCall.isLocalVideoMuted(),
isScreensharing: groupCall.isScreensharing(), isScreensharing: groupCall.isScreensharing(),
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
screenshareFeeds: [...groupCall.screenshareFeeds], screenshareFeeds: [...groupCall.screenshareFeeds],
participants: getParticipants(groupCall), participants: getParticipants(groupCall),
hasLocalParticipant: groupCall.hasLocalParticipant(), hasLocalParticipant: groupCall.hasLocalParticipant(),
@@ -516,7 +506,7 @@ export function useGroupCall(
}, [groupCall]); }, [groupCall]);
const setMicrophoneMuted = useCallback( const setMicrophoneMuted = useCallback(
(setMuted) => { (setMuted: boolean) => {
groupCall.setMicrophoneMuted(setMuted); groupCall.setMicrophoneMuted(setMuted);
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted); groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
PosthogAnalytics.instance.eventMuteMicrophone.track( PosthogAnalytics.instance.eventMuteMicrophone.track(
@@ -575,7 +565,7 @@ export function useGroupCall(
desktopCapturerSourceId: data.desktopCapturerSourceId as string, desktopCapturerSourceId: data.desktopCapturerSourceId as string,
audio: !data.desktopCapturerSourceId, audio: !data.desktopCapturerSourceId,
}); });
await widget.api.transport.reply(ev.detail, {}); await widget?.api.transport.reply(ev.detail, {});
}, },
[groupCall, updateState] [groupCall, updateState]
); );
@@ -584,7 +574,7 @@ export function useGroupCall(
async (ev: CustomEvent<IWidgetApiRequest>) => { async (ev: CustomEvent<IWidgetApiRequest>) => {
updateState({ requestingScreenshare: false }); updateState({ requestingScreenshare: false });
await groupCall.setScreensharingEnabled(false); await groupCall.setScreensharingEnabled(false);
await widget.api.transport.reply(ev.detail, {}); await widget?.api.transport.reply(ev.detail, {});
}, },
[groupCall, updateState] [groupCall, updateState]
); );
@@ -601,11 +591,11 @@ export function useGroupCall(
); );
return () => { return () => {
widget.lazyActions.off( widget?.lazyActions.off(
ElementWidgetActions.ScreenshareStart, ElementWidgetActions.ScreenshareStart,
onScreenshareStart onScreenshareStart
); );
widget.lazyActions.off( widget?.lazyActions.off(
ElementWidgetActions.ScreenshareStop, ElementWidgetActions.ScreenshareStop,
onScreenshareStop onScreenshareStop
); );
@@ -644,7 +634,6 @@ export function useGroupCall(
requestingScreenshare, requestingScreenshare,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localDesktopCapturerSourceId,
participants, participants,
hasLocalParticipant, hasLocalParticipant,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,

View File

@@ -34,8 +34,26 @@ import { widget } from "../widget";
const STATS_COLLECT_INTERVAL_TIME_MS = 10000; const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
export type GroupCallLoaded = {
kind: "loaded";
groupCall: GroupCall;
};
export type GroupCallLoadFailed = {
kind: "failed";
error: Error;
};
export type GroupCallLoading = {
kind: "loading";
};
export type GroupCallStatus =
| GroupCallLoaded
| GroupCallLoadFailed
| GroupCallLoading;
export interface GroupCallLoadState { export interface GroupCallLoadState {
loading: boolean;
error?: Error; error?: Error;
groupCall?: GroupCall; groupCall?: GroupCall;
} }
@@ -45,13 +63,11 @@ export const useLoadGroupCall = (
roomIdOrAlias: string, roomIdOrAlias: string,
viaServers: string[], viaServers: string[],
createPtt: boolean createPtt: boolean
): GroupCallLoadState => { ): GroupCallStatus => {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState<GroupCallLoadState>({ loading: true }); const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
useEffect(() => { useEffect(() => {
setState({ loading: true });
const fetchOrCreateRoom = async (): Promise<Room> => { const fetchOrCreateRoom = async (): Promise<Room> => {
try { try {
// We lowercase the localpart when we create the room, so we must lowercase // We lowercase the localpart when we create the room, so we must lowercase
@@ -74,8 +90,14 @@ export const useLoadGroupCall = (
} catch (error) { } catch (error) {
if ( if (
isLocalRoomId(roomIdOrAlias, client) && isLocalRoomId(roomIdOrAlias, client) &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(error.errcode === "M_NOT_FOUND" || (error.errcode === "M_NOT_FOUND" ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(error.message && (error.message &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
error.message.indexOf("Failed to fetch alias") !== -1)) error.message.indexOf("Failed to fetch alias") !== -1))
) { ) {
// The room doesn't exist, but we can create it // The room doesn't exist, but we can create it
@@ -86,7 +108,7 @@ export const useLoadGroupCall = (
); );
// likewise, wait for the room // likewise, wait for the room
await client.waitUntilRoomReadyForGroupCalls(roomId); await client.waitUntilRoomReadyForGroupCalls(roomId);
return client.getRoom(roomId); return client.getRoom(roomId)!;
} else { } else {
throw error; throw error;
} }
@@ -170,12 +192,8 @@ export const useLoadGroupCall = (
waitForClientSyncing() waitForClientSyncing()
.then(fetchOrCreateGroupCall) .then(fetchOrCreateGroupCall)
.then((groupCall) => .then((groupCall) => setState({ kind: "loaded", groupCall }))
setState((prevState) => ({ ...prevState, loading: false, groupCall })) .catch((error) => setState({ kind: "failed", error }));
)
.catch((error) =>
setState((prevState) => ({ ...prevState, loading: false, error }))
);
}, [client, roomIdOrAlias, viaServers, createPtt, t]); }, [client, roomIdOrAlias, viaServers, createPtt, t]);
return state; return state;

View File

@@ -55,14 +55,22 @@ export function usePageUnload(callback: () => void) {
// iOS doesn't fire beforeunload event, so leave the call when you hide the page. // iOS doesn't fire beforeunload event, so leave the call when you hide the page.
if (isIOS()) { if (isIOS()) {
window.addEventListener("pagehide", onBeforeUnload); window.addEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
document.addEventListener("visibilitychange", onBeforeUnload); document.addEventListener("visibilitychange", onBeforeUnload);
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.addEventListener("beforeunload", onBeforeUnload); window.addEventListener("beforeunload", onBeforeUnload);
return () => { return () => {
window.removeEventListener("pagehide", onBeforeUnload); window.removeEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
document.removeEventListener("visibilitychange", onBeforeUnload); document.removeEventListener("visibilitychange", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.removeEventListener("beforeunload", onBeforeUnload); window.removeEventListener("beforeunload", onBeforeUnload);
clearTimeout(pageVisibilityTimeout); clearTimeout(pageVisibilityTimeout);
}; };

View File

@@ -35,6 +35,8 @@ export function FeedbackSettingsTab({ roomId }: Props) {
const sendRageshakeRequest = useRageshakeRequest(); const sendRageshakeRequest = useRageshakeRequest();
const onSubmitFeedback = useCallback( const onSubmitFeedback = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
const data = new FormData(e.target); const data = new FormData(e.target);

View File

@@ -59,8 +59,14 @@ export function ProfileSettingsTab({ client }: Props) {
? displayNameDataEntry ? displayNameDataEntry
: displayNameDataEntry?.name ?? null; : displayNameDataEntry?.name ?? null;
if (!displayName) {
return;
}
saveProfile({ saveProfile({
displayName, displayName,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
avatar: avatar && avatarSize > 0 ? avatar : undefined, avatar: avatar && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0), removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
}); });
@@ -71,6 +77,7 @@ export function ProfileSettingsTab({ client }: Props) {
return ( return (
<form onChange={onFormChange} ref={formRef} className={styles.content}> <form onChange={onFormChange} ref={formRef} className={styles.content}>
<FieldRow className={styles.avatarFieldRow}> <FieldRow className={styles.avatarFieldRow}>
{avatarUrl && displayName && (
<AvatarInputField <AvatarInputField
id="avatar" id="avatar"
name="avatar" name="avatar"
@@ -79,6 +86,7 @@ export function ProfileSettingsTab({ client }: Props) {
displayName={displayName} displayName={displayName}
onRemoveAvatar={onRemoveAvatar} onRemoveAvatar={onRemoveAvatar}
/> />
)}
</FieldRow> </FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ChangeEvent, useCallback, useState } from "react"; import { ChangeEvent, Key, useCallback, useState } from "react";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk"; import { MatrixClient } from "matrix-js-sdk";
@@ -42,10 +42,14 @@ import { Body, Caption } from "../typography/Typography";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { ProfileSettingsTab } from "./ProfileSettingsTab"; import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import { MediaDevices, MediaDevicesState } from "../livekit/useMediaDevices"; import {
MediaDevices,
MediaDevicesState,
} from "../livekit/useMediaDevicesSwitcher";
import { useUrlParams } from "../UrlParams";
interface Props { interface Props {
mediaDevices?: MediaDevicesState; mediaDevicesSwitcher?: MediaDevicesState;
isOpen: boolean; isOpen: boolean;
client: MatrixClient; client: MatrixClient;
roomId?: string; roomId?: string;
@@ -56,6 +60,8 @@ interface Props {
export const SettingsModal = (props: Props) => { export const SettingsModal = (props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isEmbedded } = useUrlParams();
const [showInspector, setShowInspector] = useShowInspector(); const [showInspector, setShowInspector] = useShowInspector();
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const [developerSettingsTab, setDeveloperSettingsTab] = const [developerSettingsTab, setDeveloperSettingsTab] =
@@ -72,7 +78,11 @@ export const SettingsModal = (props: Props) => {
return ( return (
<SelectInput <SelectInput
label={caption} label={caption}
selectedKey={devices.selectedId} selectedKey={
devices.selectedId === "" || !devices.selectedId
? "default"
: devices.selectedId
}
onSelectionChange={(id) => devices.setSelected(id.toString())} onSelectionChange={(id) => devices.setSelected(id.toString())}
> >
{devices.available.map(({ deviceId, label }, index) => ( {devices.available.map(({ deviceId, label }, index) => (
@@ -89,8 +99,8 @@ export const SettingsModal = (props: Props) => {
const [selectedTab, setSelectedTab] = useState<string | undefined>(); const [selectedTab, setSelectedTab] = useState<string | undefined>();
const onSelectedTabChanged = useCallback( const onSelectedTabChanged = useCallback(
(tab) => { (tab: Key) => {
setSelectedTab(tab); setSelectedTab(tab.toString());
}, },
[setSelectedTab] [setSelectedTab]
); );
@@ -106,21 +116,9 @@ export const SettingsModal = (props: Props) => {
</Caption> </Caption>
); );
const devices = props.mediaDevices; const devices = props.mediaDevicesSwitcher;
return ( const tabs = [
<Modal
title={t("Settings")}
isDismissable
mobileFullScreen
className={styles.settingsModal}
{...props}
>
<TabContainer
onSelectionChange={onSelectedTabChanged}
selectedKey={selectedTab ?? props.defaultTab ?? "audio"}
className={styles.tabContainer}
>
<TabItem <TabItem
key="audio" key="audio"
title={ title={
@@ -132,7 +130,7 @@ export const SettingsModal = (props: Props) => {
> >
{devices && generateDeviceSelection(devices.audioIn, t("Microphone"))} {devices && generateDeviceSelection(devices.audioIn, t("Microphone"))}
{devices && generateDeviceSelection(devices.audioOut, t("Speaker"))} {devices && generateDeviceSelection(devices.audioOut, t("Speaker"))}
</TabItem> </TabItem>,
<TabItem <TabItem
key="video" key="video"
title={ title={
@@ -143,7 +141,58 @@ export const SettingsModal = (props: Props) => {
} }
> >
{devices && generateDeviceSelection(devices.videoIn, t("Camera"))} {devices && generateDeviceSelection(devices.videoIn, t("Camera"))}
</TabItem> </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} />
<span>{t("More")}</span>
</>
}
>
<h4>Developer</h4>
<p>Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}</p>
<FieldRow>
<InputField
id="developerSettingsTab"
type="checkbox"
checked={developerSettingsTab}
label={t("Developer Settings")}
description={t("Expose developer settings in the settings window.")}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setDeveloperSettingsTab(event.target.checked)
}
/>
</FieldRow>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics ?? undefined}
description={optInDescription}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setOptInAnalytics?.(event.target.checked);
}}
/>
</FieldRow>
</TabItem>,
];
if (!isEmbedded) {
tabs.push(
<TabItem <TabItem
key="profile" key="profile"
title={ title={
@@ -155,58 +204,11 @@ export const SettingsModal = (props: Props) => {
> >
<ProfileSettingsTab client={props.client} /> <ProfileSettingsTab client={props.client} />
</TabItem> </TabItem>
<TabItem );
key="feedback"
title={
<>
<FeedbackIcon width={16} height={16} />
<span>{t("Feedback")}</span>
</>
} }
>
<FeedbackSettingsTab roomId={props.roomId} /> if (developerSettingsTab) {
</TabItem> tabs.push(
<TabItem
key="more"
title={
<>
<OverflowIcon width={16} height={16} />
<span>{t("More")}</span>
</>
}
>
<h4>Developer</h4>
<p>
Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}
</p>
<FieldRow>
<InputField
id="developerSettingsTab"
type="checkbox"
checked={developerSettingsTab}
label={t("Developer Settings")}
description={t(
"Expose developer settings in the settings window."
)}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setDeveloperSettingsTab(event.target.checked)
}
/>
</FieldRow>
<h4>Analytics</h4>
<FieldRow>
<InputField
id="optInAnalytics"
type="checkbox"
checked={optInAnalytics}
description={optInDescription}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setOptInAnalytics(event.target.checked)
}
/>
</FieldRow>
</TabItem>
{developerSettingsTab && (
<TabItem <TabItem
key="developer" key="developer"
title={ title={
@@ -248,12 +250,26 @@ export const SettingsModal = (props: Props) => {
/> />
</FieldRow> </FieldRow>
<FieldRow> <FieldRow>
<Button onPress={downloadDebugLog}> <Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
{t("Download debug logs")}
</Button>
</FieldRow> </FieldRow>
</TabItem> </TabItem>
)} );
}
return (
<Modal
title={t("Settings")}
isDismissable
mobileFullScreen
className={styles.settingsModal}
{...props}
>
<TabContainer
onSelectionChange={onSelectedTabChanged}
selectedKey={selectedTab ?? props.defaultTab ?? "audio"}
className={styles.tabContainer}
>
{tabs}
</TabContainer> </TabContainer>
</Modal> </Modal>
); );

View File

@@ -79,11 +79,17 @@ class ConsoleLogger extends EventEmitter {
warn: "W", warn: "W",
error: "E", error: "E",
}; };
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
const level = consoleFunctionsToLevels[fnName]; Object.entries(consoleFunctionsToLevels).forEach(([name, level]) => {
const originalFn = consoleObj[fnName].bind(consoleObj); // eslint-disable-next-line @typescript-eslint/ban-ts-comment
this.originalFunctions[fnName] = originalFn; // @ts-ignore
consoleObj[fnName] = (...args) => { const originalFn = consoleObj[name].bind(consoleObj);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.originalFunctions[name] = originalFn;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
consoleObj[name] = (...args) => {
this.log(level, ...args); this.log(level, ...args);
originalFn(...args); originalFn(...args);
}; };
@@ -147,9 +153,9 @@ class ConsoleLogger extends EventEmitter {
// A class which stores log lines in an IndexedDB instance. // A class which stores log lines in an IndexedDB instance.
class IndexedDBLogStore { class IndexedDBLogStore {
private index = 0; private index = 0;
private db: IDBDatabase = null; private db?: IDBDatabase;
private flushPromise: Promise<void> = null; private flushPromise?: Promise<void>;
private flushAgainPromise: Promise<void> = null; private flushAgainPromise?: Promise<void>;
private id: string; private id: string;
constructor( constructor(
@@ -175,7 +181,7 @@ class IndexedDBLogStore {
}; };
req.onerror = () => { req.onerror = () => {
const err = "Failed to open log database: " + req.error.name; const err = "Failed to open log database: " + req?.error?.name;
logger.error(err); logger.error(err);
reject(new Error(err)); reject(new Error(err));
}; };
@@ -264,7 +270,7 @@ class IndexedDBLogStore {
return this.flush(); return this.flush();
}) })
.then(() => { .then(() => {
this.flushAgainPromise = null; this.flushAgainPromise = undefined;
}); });
return this.flushAgainPromise; return this.flushAgainPromise;
} }
@@ -288,13 +294,13 @@ class IndexedDBLogStore {
}; };
txn.onerror = (event) => { txn.onerror = (event) => {
logger.error("Failed to flush logs : ", event); logger.error("Failed to flush logs : ", event);
reject(new Error("Failed to write logs: " + txn.error.message)); reject(new Error("Failed to write logs: " + txn?.error?.message));
}; };
objStore.add(this.generateLogEntry(lines)); objStore.add(this.generateLogEntry(lines));
const lastModStore = txn.objectStore("logslastmod"); const lastModStore = txn.objectStore("logslastmod");
lastModStore.put(this.generateLastModifiedTime()); lastModStore.put(this.generateLastModifiedTime());
}).then(() => { }).then(() => {
this.flushPromise = null; this.flushPromise = undefined;
}); });
return this.flushPromise; return this.flushPromise;
}; };
@@ -311,11 +317,14 @@ class IndexedDBLogStore {
*/ */
public async consume(): Promise<LogEntry[]> { public async consume(): Promise<LogEntry[]> {
const db = this.db; const db = this.db;
if (!db) {
return Promise.reject(new Error("No connected database"));
}
// Returns: a string representing the concatenated logs for this ID. // Returns: a string representing the concatenated logs for this ID.
// Stops adding log fragments when the size exceeds maxSize // Stops adding log fragments when the size exceeds maxSize
function fetchLogs(id: string, maxSize: number): Promise<string> { function fetchLogs(id: string, maxSize: number): Promise<string> {
const objectStore = db const objectStore = db!
.transaction("logs", "readonly") .transaction("logs", "readonly")
.objectStore("logs"); .objectStore("logs");
@@ -325,7 +334,7 @@ class IndexedDBLogStore {
.openCursor(IDBKeyRange.only(id), "prev"); .openCursor(IDBKeyRange.only(id), "prev");
let lines = ""; let lines = "";
query.onerror = () => { query.onerror = () => {
reject(new Error("Query failed: " + query.error.message)); reject(new Error("Query failed: " + query?.error?.message));
}; };
query.onsuccess = () => { query.onsuccess = () => {
const cursor = query.result; const cursor = query.result;
@@ -346,7 +355,7 @@ class IndexedDBLogStore {
// Returns: A sorted array of log IDs. (newest first) // Returns: A sorted array of log IDs. (newest first)
function fetchLogIds(): Promise<string[]> { function fetchLogIds(): Promise<string[]> {
// To gather all the log IDs, query for all records in logslastmod. // To gather all the log IDs, query for all records in logslastmod.
const o = db const o = db!
.transaction("logslastmod", "readonly") .transaction("logslastmod", "readonly")
.objectStore("logslastmod"); .objectStore("logslastmod");
return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => { return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
@@ -366,7 +375,7 @@ class IndexedDBLogStore {
function deleteLogs(id: number): Promise<void> { function deleteLogs(id: number): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const txn = db.transaction(["logs", "logslastmod"], "readwrite"); const txn = db!.transaction(["logs", "logslastmod"], "readwrite");
const o = txn.objectStore("logs"); const o = txn.objectStore("logs");
// only load the key path, not the data which may be huge // only load the key path, not the data which may be huge
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id)); const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
@@ -384,7 +393,7 @@ class IndexedDBLogStore {
txn.onerror = () => { txn.onerror = () => {
reject( reject(
new Error( new Error(
"Failed to delete logs for " + `'${id}' : ${txn.error.message}` "Failed to delete logs for " + `'${id}' : ${txn?.error?.message}`
) )
); );
}; };
@@ -395,7 +404,7 @@ class IndexedDBLogStore {
} }
const allLogIds = await fetchLogIds(); const allLogIds = await fetchLogIds();
let removeLogIds = []; let removeLogIds: number[] = [];
const logs: LogEntry[] = []; const logs: LogEntry[] = [];
let size = 0; let size = 0;
for (let i = 0; i < allLogIds.length; i++) { for (let i = 0; i < allLogIds.length; i++) {
@@ -414,7 +423,7 @@ class IndexedDBLogStore {
if (size >= MAX_LOG_SIZE) { if (size >= MAX_LOG_SIZE) {
// the remaining log IDs should be removed. If we go out of // the remaining log IDs should be removed. If we go out of
// bounds this is just [] // bounds this is just []
removeLogIds = allLogIds.slice(i + 1); removeLogIds = allLogIds.slice(i + 1).map((id) => parseInt(id, 10));
break; break;
} }
} }
@@ -462,14 +471,14 @@ class IndexedDBLogStore {
*/ */
function selectQuery<T>( function selectQuery<T>(
store: IDBObjectStore, store: IDBObjectStore,
keyRange: IDBKeyRange, keyRange: IDBKeyRange | undefined,
resultMapper: (cursor: IDBCursorWithValue) => T resultMapper: (cursor: IDBCursorWithValue) => T
): Promise<T[]> { ): Promise<T[]> {
const query = store.openCursor(keyRange); const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results = []; const results: T[] = [];
query.onerror = () => { query.onerror = () => {
reject(new Error("Query failed: " + query.error.message)); reject(new Error("Query failed: " + query?.error?.message));
}; };
// collect results // collect results
query.onsuccess = () => { query.onsuccess = () => {

View File

@@ -15,10 +15,12 @@ limitations under the License.
*/ */
import { useCallback, useContext, useEffect, useState } from "react"; import { useCallback, useContext, useEffect, useState } from "react";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import pako from "pako"; import pako from "pako";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { OverlayTriggerState } from "@react-stately/overlays"; import { OverlayTriggerState } from "@react-stately/overlays";
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; import { ClientEvent } from "matrix-js-sdk/src/client";
import { getLogsForReport } from "./rageshake"; import { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
@@ -46,20 +48,27 @@ export function useSubmitRageshake(): {
submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>; submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>;
sending: boolean; sending: boolean;
sent: boolean; sent: boolean;
error: Error; error?: Error;
} { } {
const client: MatrixClient = useClient().client; const { client } = useClient();
// The value of the context is the whole tuple returned from setState, // The value of the context is the whole tuple returned from setState,
// so we just want the current state. // so we just want the current state.
const [inspectorState] = useContext(InspectorContext); const [inspectorState] = useContext(InspectorContext);
const [{ sending, sent, error }, setState] = useState({ const [{ sending, sent, error }, setState] = useState<{
sending: boolean;
sent: boolean;
error?: Error;
}>({
sending: false, sending: false,
sent: false, sent: false,
error: null, error: undefined,
}); });
const submitRageshake = useCallback( const submitRageshake = useCallback(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async (opts) => { async (opts) => {
if (!Config.get().rageshake?.submit_url) { if (!Config.get().rageshake?.submit_url) {
throw new Error("No rageshake URL is configured"); throw new Error("No rageshake URL is configured");
@@ -70,7 +79,7 @@ export function useSubmitRageshake(): {
} }
try { try {
setState({ sending: true, sent: false, error: null }); setState({ sending: true, sent: false, error: undefined });
let userAgent = "UNKNOWN"; let userAgent = "UNKNOWN";
if (window.navigator && window.navigator.userAgent) { if (window.navigator && window.navigator.userAgent) {
@@ -104,11 +113,11 @@ export function useSubmitRageshake(): {
body.append("call_backend", "livekit"); body.append("call_backend", "livekit");
if (client) { if (client) {
const userId = client.getUserId(); const userId = client.getUserId()!;
const user = client.getUser(userId); const user = client.getUser(userId);
body.append("display_name", user?.displayName); body.append("display_name", user?.displayName ?? "");
body.append("user_id", client.credentials.userId); body.append("user_id", client.credentials.userId ?? "");
body.append("device_id", client.deviceId); body.append("device_id", client.deviceId ?? "");
if (opts.roomId) { if (opts.roomId) {
body.append("room_id", opts.roomId); body.append("room_id", opts.roomId);
@@ -120,11 +129,11 @@ export function useSubmitRageshake(): {
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`); keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
} }
body.append("device_keys", keys.join(", ")); body.append("device_keys", keys.join(", "));
body.append("cross_signing_key", client.getCrossSigningId()); body.append("cross_signing_key", client.getCrossSigningId()!);
// add cross-signing status information // add cross-signing status information
const crossSigning = client.crypto.crossSigningInfo; const crossSigning = client.crypto!.crossSigningInfo;
const secretStorage = client.crypto.secretStorage; const secretStorage = client.crypto!.secretStorage;
body.append( body.append(
"cross_signing_ready", "cross_signing_ready",
@@ -138,7 +147,7 @@ export function useSubmitRageshake(): {
) )
) )
); );
body.append("cross_signing_key", crossSigning.getId()); body.append("cross_signing_key", crossSigning.getId()!);
body.append( body.append(
"cross_signing_privkey_in_secret_storage", "cross_signing_privkey_in_secret_storage",
String( String(
@@ -150,14 +159,17 @@ export function useSubmitRageshake(): {
body.append( body.append(
"cross_signing_master_privkey_cached", "cross_signing_master_privkey_cached",
String( String(
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master"))) !!(
pkCache?.getCrossSigningKeyCache &&
(await pkCache.getCrossSigningKeyCache("master"))
)
) )
); );
body.append( body.append(
"cross_signing_self_signing_privkey_cached", "cross_signing_self_signing_privkey_cached",
String( String(
!!( !!(
pkCache && pkCache?.getCrossSigningKeyCache &&
(await pkCache.getCrossSigningKeyCache("self_signing")) (await pkCache.getCrossSigningKeyCache("self_signing"))
) )
) )
@@ -166,7 +178,7 @@ export function useSubmitRageshake(): {
"cross_signing_user_signing_privkey_cached", "cross_signing_user_signing_privkey_cached",
String( String(
!!( !!(
pkCache && pkCache?.getCrossSigningKeyCache &&
(await pkCache.getCrossSigningKeyCache("user_signing")) (await pkCache.getCrossSigningKeyCache("user_signing"))
) )
) )
@@ -186,7 +198,7 @@ export function useSubmitRageshake(): {
String(!!(await client.isKeyBackupKeyStored())) String(!!(await client.isKeyBackupKeyStored()))
); );
const sessionBackupKeyFromCache = const sessionBackupKeyFromCache =
await client.crypto.getSessionBackupPrivateKey(); await client.crypto!.getSessionBackupPrivateKey();
body.append( body.append(
"session_backup_key_cached", "session_backup_key_cached",
String(!!sessionBackupKeyFromCache) String(!!sessionBackupKeyFromCache)
@@ -233,7 +245,7 @@ export function useSubmitRageshake(): {
Object.keys(estimate.usageDetails).forEach((k) => { Object.keys(estimate.usageDetails).forEach((k) => {
body.append( body.append(
`storageManager_usage_${k}`, `storageManager_usage_${k}`,
String(estimate.usageDetails[k]) String(estimate.usageDetails![k])
); );
}); });
} }
@@ -271,14 +283,14 @@ export function useSubmitRageshake(): {
); );
} }
await fetch(Config.get().rageshake?.submit_url, { await fetch(Config.get().rageshake!.submit_url, {
method: "POST", method: "POST",
body, body,
}); });
setState({ sending: false, sent: true, error: null }); setState({ sending: false, sent: true, error: undefined });
} catch (error) { } catch (error) {
setState({ sending: false, sent: false, error }); setState({ sending: false, sent: false, error: error as Error });
console.error(error); console.error(error);
} }
}, },
@@ -307,7 +319,7 @@ export function useDownloadDebugLog(): () => void {
el.click(); el.click();
setTimeout(() => { setTimeout(() => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
el.parentNode.removeChild(el); el.parentNode!.removeChild(el);
}, 0); }, 0);
}, [json]); }, [json]);
@@ -321,8 +333,8 @@ export function useRageshakeRequest(): (
const { client } = useClient(); const { client } = useClient();
const sendRageshakeRequest = useCallback( const sendRageshakeRequest = useCallback(
(roomId, rageshakeRequestId) => { (roomId: string, rageshakeRequestId: string) => {
client.sendEvent(roomId, "org.matrix.rageshake_request", { client!.sendEvent(roomId, "org.matrix.rageshake_request", {
request_id: rageshakeRequestId, request_id: rageshakeRequestId,
}); });
}, },
@@ -347,10 +359,12 @@ export function useRageshakeRequestModal(roomId: string): {
modalState: OverlayTriggerState; modalState: OverlayTriggerState;
modalProps: ModalProps; modalProps: ModalProps;
}; };
const client: MatrixClient = useClient().client; const { client } = useClient();
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>(); const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
useEffect(() => { useEffect(() => {
if (!client) return;
const onEvent = (event: MatrixEvent) => { const onEvent = (event: MatrixEvent) => {
const type = event.getType(); const type = event.getType();
@@ -371,5 +385,8 @@ export function useRageshakeRequestModal(roomId: string): {
}; };
}, [modalState.open, roomId, client, modalState]); }, [modalState.open, roomId, client, modalState]);
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } }; return {
modalState,
modalProps: { ...modalProps, rageshakeRequestId: rageshakeRequestId ?? "" },
};
} }

View File

@@ -26,7 +26,7 @@ limitations under the License.
padding: 0; padding: 0;
margin: 0 auto 24px auto; margin: 0 auto 24px auto;
gap: 16px; gap: 16px;
overflow: scroll; overflow-y: auto;
max-width: 100%; max-width: 100%;
} }

View File

@@ -32,7 +32,7 @@ export function TabContainer<T extends object>(
props: TabContainerProps<T> props: TabContainerProps<T>
): JSX.Element { ): JSX.Element {
const state = useTabListState<T>(props); const state = useTabListState<T>(props);
const ref = useRef<HTMLUListElement>(); const ref = useRef<HTMLUListElement>(null);
const { tabListProps } = useTabList(props, state, ref); const { tabListProps } = useTabList(props, state, ref);
return ( return (
<div className={classNames(styles.tabContainer, props.className)}> <div className={classNames(styles.tabContainer, props.className)}>
@@ -53,7 +53,7 @@ interface TabProps<T> {
function Tab<T>({ item, state }: TabProps<T>): JSX.Element { function Tab<T>({ item, state }: TabProps<T>): JSX.Element {
const { key, rendered } = item; const { key, rendered } = item;
const ref = useRef<HTMLLIElement>(); const ref = useRef<HTMLLIElement>(null);
const { tabProps } = useTab({ key }, state, ref); const { tabProps } = useTab({ key }, state, ref);
return ( return (
@@ -75,7 +75,7 @@ interface TabPanelProps<T> extends AriaTabPanelProps {
} }
function TabPanel<T>({ state, ...props }: TabPanelProps<T>): JSX.Element { function TabPanel<T>({ state, ...props }: TabPanelProps<T>): JSX.Element {
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>(null);
const { tabPanelProps } = useTabPanel(props, state, ref); const { tabPanelProps } = useTabPanel(props, state, ref);
return ( return (
<div {...tabPanelProps} ref={ref} className={styles.tabPanel}> <div {...tabPanelProps} ref={ref} className={styles.tabPanel}>

View File

@@ -46,7 +46,7 @@ export const Headline = forwardRef<HTMLHeadingElement, TypographyProps>(
{ {
...rest, ...rest,
className: classNames( className: classNames(
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),
@@ -74,7 +74,7 @@ export const Title = forwardRef<HTMLHeadingElement, TypographyProps>(
{ {
...rest, ...rest,
className: classNames( className: classNames(
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),
@@ -102,7 +102,7 @@ export const Subtitle = forwardRef<HTMLParagraphElement, TypographyProps>(
{ {
...rest, ...rest,
className: classNames( className: classNames(
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),
@@ -130,7 +130,7 @@ export const Body = forwardRef<HTMLParagraphElement, TypographyProps>(
{ {
...rest, ...rest,
className: classNames( className: classNames(
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),
@@ -159,7 +159,7 @@ export const Caption = forwardRef<HTMLParagraphElement, TypographyProps>(
...rest, ...rest,
className: classNames( className: classNames(
styles.caption, styles.caption,
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),
@@ -188,7 +188,7 @@ export const Micro = forwardRef<HTMLParagraphElement, TypographyProps>(
...rest, ...rest,
className: classNames( className: classNames(
styles.micro, styles.micro,
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),
@@ -219,6 +219,8 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
}, },
ref ref
) => { ) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const Component: string | RouterLink = as || (to ? RouterLink : "a"); const Component: string | RouterLink = as || (to ? RouterLink : "a");
let externalLinkProps: { href: string; target: string; rel: string }; let externalLinkProps: { href: string; target: string; rel: string };
@@ -233,12 +235,16 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
return createElement( return createElement(
Component, Component,
{ {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...externalLinkProps, ...externalLinkProps,
...rest, ...rest,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
to: to, to: to,
className: classNames( className: classNames(
styles[color], styles[color],
styles[fontWeight], styles[fontWeight ?? ""],
{ [styles.overflowEllipsis]: overflowEllipsis }, { [styles.overflowEllipsis]: overflowEllipsis },
className className
), ),

View File

@@ -31,8 +31,13 @@ export const useEventTarget = <T extends Event>(
) => { ) => {
useEffect(() => { useEffect(() => {
if (target) { if (target) {
target.addEventListener(eventType, listener, options); target.addEventListener(eventType, listener as EventListener, options);
return () => target.removeEventListener(eventType, listener, options); return () =>
target.removeEventListener(
eventType,
listener as EventListener,
options
);
} }
}, [target, eventType, listener, options]); }, [target, eventType, listener, options]);
}; };

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