diff --git a/.env.example b/.env.example index f03e8821..4180a830 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,4 @@ LIVEKIT_SECRET="secret" # VITE_THEME_SYSTEM=#21262c # VITE_THEME_BACKGROUND=#15191e # VITE_THEME_BACKGROUND_85=#15191ed9 +# VITE_THEME_SUBTLE_PRIMARY=#26282D diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f4107103..c7803415 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,6 +1,16 @@ module.exports = { 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: { browser: true, node: true, diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a9298638..baa3a5f0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -19,6 +19,6 @@ jobs: - name: i18n run: "yarn run i18n:check" - name: ESLint - run: "yarn run lint:js" + run: "yarn run lint:eslint" - name: Type check run: "yarn run lint:types" diff --git a/config/config.sample.json b/config/config.sample.json index 7afb8f69..35ad1846 100644 --- a/config/config.sample.json +++ b/config/config.sample.json @@ -4,5 +4,6 @@ "base_url": "https://call.ems.host", "server_name": "call.ems.host" } - } + }, + "eula": "https://static.element.io/legal/online-EULA.pdf" } diff --git a/config/element_io_preview.json b/config/element_io_preview.json index 7d7eec2a..edd6ff1c 100644 --- a/config/element_io_preview.json +++ b/config/element_io_preview.json @@ -6,7 +6,7 @@ } }, "livekit": { - "livekit_service_url": "https://livekit.element.dev" + "livekit_service_url": "https://livekit-jwt.call.element.dev" }, "posthog": { "api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU", diff --git a/package.json b/package.json index f805baa3..7cf8eb1d 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "build-storybook": "build-storybook", "prettier:check": "prettier -c .", "prettier:format": "prettier -w .", - "lint": "yarn lint:types && yarn lint:js", - "lint:js": "eslint --max-warnings 0 src", + "lint": "yarn lint:types && yarn lint:eslint", + "lint:eslint": "eslint --max-warnings 0 src", + "lint:eslint-fix": "eslint --max-warnings 0 src --fix", "lint:types": "tsc", "i18n": "node_modules/i18next-parser/bin/cli.js", "i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update", @@ -19,7 +20,7 @@ }, "dependencies": { "@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", "@opentelemetry/api": "^1.4.0", "@opentelemetry/context-zone": "^1.9.1", @@ -46,8 +47,10 @@ "@sentry/react": "^6.13.3", "@sentry/tracing": "^6.13.3", "@types/grecaptcha": "^3.0.4", + "@types/react-router-dom": "^5.3.3", "@types/sdp-transform": "^2.4.5", "@use-gesture/react": "^10.2.11", + "@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-react": "^4.0.1", "classnames": "^2.3.1", "color-hash": "^2.0.1", @@ -55,9 +58,9 @@ "i18next": "^21.10.0", "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", - "livekit-client": "^1.9.7", + "livekit-client": "1.12.0", "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", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", @@ -69,13 +72,15 @@ "react-dom": "18", "react-i18next": "^11.18.6", "react-json-view": "^1.21.3", - "react-router": "6", "react-router-dom": "^5.2.0", "react-use-clipboard": "^1.0.7", "react-use-measure": "^2.1.1", "sdp-transform": "^2.14.1", "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": { "@babel/core": "^7.16.5", diff --git a/public/locales/bg/app.json b/public/locales/bg/app.json index 4cf46655..34561abd 100644 --- a/public/locales/bg/app.json +++ b/public/locales/bg/app.json @@ -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 логове.", "Audio": "Звук", "Avatar": "Аватар", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия", "Call link copied": "Връзка към разговора бе копирана", "Call type menu": "Меню \"тип на разговора\"", "Camera": "Камера", @@ -75,7 +73,6 @@ "Take me Home": "Отиди в Начало", "Thanks! We'll get right on it.": "Благодарим! Веднага ще се заемем.", "This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Този сайт се предпазва от ReCAPTCHA и важат <2>Политиката за поверителност и <6>Условията за ползване на услугата на Google.<9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия", "Turn off camera": "Изключи камерата", "Turn on camera": "Включи камерата", "Unmute microphone": "Включи микрофона", diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 40bcd5ab..3857c05f 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -62,7 +62,6 @@ "Inspector": "Insepktor", "Incompatible versions!": "Nekompatibilní verze!", "Incompatible versions": "Nekompatibilní verze", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Tato stárnka je chráněna pomocí ReCAPTCHA a Google <2>zásad ochrany osobních údajů a <6>podmínky služby platí.<9>Kliknutím na \"Registrovat\", souhlasíte s <12>Pravidly a podmínkami", "Walkie-talkie call name": "Jméno vysílačkového hovoru", "Walkie-talkie call": "Vysílačkový hovor", "{{names}}, {{name}}": "{{names}}, {{name}}", @@ -90,8 +89,6 @@ "Create account": "Vytvořit účet", "Copy": "Kopírovat", "Call type menu": "Menu typu hovoru", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions", "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?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?<1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory ", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Připojit se<1>Or<2>Zkopírovat odkaz a připojit se později", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 516ad796..99866dc5 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -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.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Wenn du auf „Los geht’s“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen", "Call link copied": "Anruflink kopiert", "Call type menu": "Anruftyp Menü", "Camera": "Kamera", @@ -74,7 +72,6 @@ "Take me Home": "Zurück zur Startseite", "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 site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Diese Website wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung und <6>Nutzungsbedingungen von Google.<9>Indem Du auf „Registrieren“ klickst, stimmst du unseren <12>Geschäftsbedingungen zu", "Turn off camera": "Kamera ausschalten", "Turn on camera": "Kamera einschalten", "Unmute microphone": "Mikrofon aktivieren", @@ -115,5 +112,9 @@ "{{count}} stars|one": "{{count}} Stern", "<0>Thanks for your feedback!": "<0>Danke für deine Rückmeldung!", "{{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)": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Mit einem Klick auf „Los geht’s“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung und <6>Nutzungsbedingungen. <9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)", + "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." } diff --git a/public/locales/el/app.json b/public/locales/el/app.json index d61ba142..91f5554e 100644 --- a/public/locales/el/app.json +++ b/public/locales/el/app.json @@ -11,7 +11,7 @@ "Remove": "Αφαίρεση", "Registering…": "Εγγραφή…", "Not registered yet? <2>Create an account": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό", - "Login to your account": "Συνδεθείτε στο λογαριασμό σας", + "Login to your account": "Συνδεθείτε στον λογαριασμό σας", "Logging in…": "Σύνδεση…", "Invite people": "Προσκαλέστε άτομα", "Invite": "Πρόσκληση", @@ -71,5 +71,46 @@ "Close": "Κλείσιμο", "Change layout": "Αλλαγή διάταξης", "Camera": "Κάμερα", - "Audio": "Ήχος" + "Audio": "Ήχος", + "Send debug logs": "Αποστολή αρχείων καταγραφής", + "Recaptcha dismissed": "Το recaptcha απορρίφθηκε", + "<0>Thanks for your feedback!": "<0>Ευχαριστώ για τα σχόλιά σας!", + "Call type menu": "Μενού είδους κλήσης", + "Local volume": "Τοπική ένταση", + "Home": "Αρχική", + "Show connection stats": "Εμφάνιση στατιστικών σύνδεσης", + "Unmute microphone": "Κατάργηση σίγασης μικροφώνου", + "Take me Home": "Μετάβαση στην Αρχική", + "{{displayName}} is presenting": "{{displayName}} παρουσιάζει", + "<0><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><1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Συμμετοχή στην κλήση τώρα<1>Or<2>Αντιγραφή συνδέσμου κλήσης και συμμετοχή αργότερα", + "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Θα θέλαμε να ακούσουμε τα σχόλιά σας ώστε να βελτιώσουμε την εμπειρία σας.", + "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Γιατί να μην ολοκληρώσετε με τη δημιουργία ενός κωδικού πρόσβασης για τη διατήρηση του λογαριασμού σας;<1>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.", + "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 and our <5>Cookie Policy.": "Συμμετέχοντας σε αυτή τη δοκιμαστική έκδοση, συναινείτε στη συλλογή ανώνυμων δεδομένων, τα οποία χρησιμοποιούμε για τη βελτίωση του προϊόντος. Μπορείτε να βρείτε περισσότερες πληροφορίες σχετικά με το ποια δεδομένα καταγράφουμε στην <2>Πολιτική απορρήτου και στην <5>Πολιτική cookies.", + "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}": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}", + "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>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.", + "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 Κεντρική Οθόνη Κλήσεων" } diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index ac948232..5fe1ffa8 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -37,6 +37,7 @@ "Display name": "Display name", "Download debug logs": "Download debug logs", "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", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Feedback": "Feedback", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index facea468..019f3a1a 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -6,8 +6,6 @@ "Register": "Registrarse", "Not registered yet? <2>Create an account": "¿No estás registrado todavía? <2>Crear una cuenta", "Login to your account": "Iniciar sesión en tu cuenta", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Al hacer clic en \"Unirse a la llamada ahora\", aceptarás nuestros <2>Términos y condiciones", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones", "Yes, join call": "Si, unirse a la llamada", "Walkie-talkie call name": "Nombre de la llamada Walkie-talkie", "Walkie-talkie call": "Llamada Walkie-talkie", @@ -21,7 +19,6 @@ "Unmute microphone": "Desilenciar el micrófono", "Turn on camera": "Encender la cámara", "Turn off camera": "Apagar la cámara", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Este sitio está protegido por ReCAPTCHA y se aplica <2>la Política de Privacidad y <6>los Términos de Servicio de Google.<9>Al hacer clic en \"Registrar\" aceptarás nuestros <12>Términos y condiciones", "Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.", "Take me Home": "Volver al inicio", "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.", "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 and our <5>Cookie Policy.": "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 y en nuestra <5>Política sobre Cookies.", - "<0><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><1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta." + "<0><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><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>¡Gracias por tus comentarios!", + "How did it go?": "¿Cómo ha ido?", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Este sitio está protegido por ReCAPTCHA y se aplican la <2>Política de Privacidad y los <6>Términos de Servicio de Google.<9>Al hacer clic en \"Registrar\", aceptas nuestro <12>Contrato de Licencia de Usuario Final (CLUF)", + "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)": "Al hacer clic en \"Comenzar\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)", + "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)": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)", + "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Nos encantaría conocer tu opinión para que podamos mejorar tu experiencia", + "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" } diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 8c6a4afc..d8659dbf 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -32,14 +32,12 @@ "Camera": "Kaamera", "Call type menu": "Kõnetüübi valik", "Call link copied": "Kõne link on kopeeritud", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Klõpsides „Liitu kõnega“nõustud sa meie <2>kasutustingimustega", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega", "Avatar": "Tunnuspilt", "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.", "Passwords must match": "Salasõnad ei klapi", "Password": "Salasõna", - "Not registered yet? <2>Create an account": "Pole veel registreerunud? <2>Loo kasutajakonto", + "Not registered yet? <2>Create an account": "Sa pole veel registreerunud? <2>Loo kasutajakonto", "Not now, return to home screen": "Mitte praegu, mine tagasi avalehele", "No": "Ei", "Mute microphone": "Summuta mikrofon", @@ -53,7 +51,7 @@ "Loading…": "Laadimine …", "Leave": "Lahku", "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", "Turn on camera": "Lülita kaamera sisse", "Turn off camera": "Lülita kaamera välja", @@ -93,7 +91,6 @@ "Walkie-talkie call": "Walkie-talkie stiilis kõne", "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.", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika ja <6>teenusetingimused.<9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega", "Element Call Home": "Element Call Home", "Copy": "Kopeeri", "<0>Submitting debug logs will help us track down the problem.": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.", @@ -115,5 +112,9 @@ "<0>Thanks for your feedback!": "<0>Täname Sind tagasiside eest!", "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Meie rakenduse paremaks muutmiseks me hea meelega ootame Sinu arvamusi.", "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)": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika ning <6>Teenusetingimused.<9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega", + "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." } diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index 0877ab78..4014d69c 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -45,8 +45,6 @@ "Camera": "دوربین", "Call type menu": "منوی نوع تماس", "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": "آواتار", "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.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیب‌یابی را جمع‌آوری کنیم.", @@ -88,7 +86,6 @@ "Version: {{version}}": "نسخه: {{نسخه}}", "User menu": "فهرست کاربر", "Unmute microphone": "ناخموشی میکروفون", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "این سایت توسط ReCAPTCHA محافظت می شود و <2>خط مشی رازداری و <6>شرایط خدمات Google اعمال می شود.<9>با کلیک کردن بر روی \"ثبت نام\"، شما با <12 >شرایط و ضوابط ما موافقت می کنید", "This call already exists, would you like to join?": "این تماس از قبل وجود دارد، می‌خواهید بپیوندید؟", "Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.", "Submit feedback": "بازخورد ارائه دهید", diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 837c677f..5ac60999 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -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.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "En cliquant sur « Commencer » vous acceptez nos <2>conditions d’utilisation", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "En cliquant sur « Rejoindre l’appel » vous acceptez nos <2>conditions d’utilisation", "Call link copied": "Lien de l’appel copié", "Call type menu": "Menu de type d’appel", "Camera": "Caméra", @@ -88,7 +86,6 @@ "Unmute microphone": "Allumer le micro", "Turn on camera": "Allumer la caméra", "Turn off camera": "Couper la caméra", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité et les <6>conditions d’utilisation de Google s’appliquent.<9>En cliquant sur « S’enregistrer » vous acceptez également nos <12>conditions d’utilisation", "Speaker": "Intervenant", "Invite": "Inviter", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Vous avez déjà un compte ?<1><0>Se connecter Ou <2>Accès invité", @@ -115,5 +112,9 @@ "<0>Thanks for your feedback!": "<0>Merci pour votre commentaire !", "How did it go?": "Comment cela s’est-il passé ?", "{{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)": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "En cliquant sur « Commencer », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité et les <6>conditions d’utilisation de Google s’appliquent.<9>En cliquant sur « S’enregistrer » vous acceptez également notre <12>Contrat de Licence Utilisateur Final (CLUF)", + "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call n’est temporairement plus chiffré de bout en bout le temps de tester l’extensibilité." } diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 1388daeb..e64c08b4 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -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.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan kami", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan kami", "Call link copied": "Tautan panggilan disalin", "Call type menu": "Menu jenis panggilan", "Camera": "Kamera", @@ -75,7 +73,6 @@ "Take me Home": "Bawa saya ke Beranda", "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 site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi dan <6>Ketentuan Layanan Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan kami", "Turn off camera": "Matikan kamera", "Turn on camera": "Nyalakan kamera", "Unmute microphone": "Suarakan mikrofon", @@ -93,7 +90,7 @@ "Your recent calls": "Panggilan Anda terkini", "{{names}}, {{name}}": "{{names}}, {{name}}", "Sending debug logs…": "Mengirimkan catatan pengawakutuan…", - "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Bergabung panggilan sekarang<1>Atau<2>Salin tautan dan bergabung nanti", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Bergabung ke panggilan sekarang<1>Atau<2>Salin tautan dan bergabung nanti", "Element Call Home": "Beranda Element Call", "Copy": "Salin", "<0>Submitting debug logs will help us track down the problem.": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.", @@ -105,7 +102,7 @@ "Feedback": "Masukan", "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.", "Submit": "Kirim", - "Submitting…": "Mengirim", + "Submitting…": "Mengirim…", "Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!", "Your feedback": "Masukan Anda", "{{displayName}}, your call has ended.": "{{displayName}}, panggilan Anda telah berakhir.", @@ -115,5 +112,9 @@ "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Kami ingin mendengar masukan Anda supaya kami bisa meningkatkan pengalaman Anda.", "Show connection stats": "Tampilkan statistik koneksi", "{{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)": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Situs ini dilindungi oleh reCAPTCHA dan <2>Kebijakan Privasi dan <6>Ketentuan Layanan Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda menyetujui <12>Perjanjian Lisensi Pengguna Akhir (EULA) kami", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA) 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." } diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 9aac3347..e1d01ea8 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -5,8 +5,6 @@ "<0>Oops, something's gone wrong.": "<0>何かがうまく行きませんでした。", "Camera": "カメラ", "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": "アバター", "Audio": "音声", "Confirm password": "パスワードを確認", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index b702178b..df3c9a71 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -1,7 +1,6 @@ { "Login": "Zaloguj się", "Go": "Przejdź", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Zasady i warunki", "Your recent calls": "Twoje ostatnie 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.", @@ -84,7 +83,6 @@ "Camera": "Kamera", "Call type menu": "Menu typu połączenia", "Call link copied": "Skopiowano link do połączenia", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Zasady i warunki", "Avatar": "Awatar", "Audio": "Dźwięk", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Inny użytkownik w tym połączeniu napotkał problem. Aby lepiej zdiagnozować tę usterkę, chcielibyśmy zebrać dzienniki debugowania.", @@ -99,7 +97,6 @@ "Expose developer settings in the settings window.": "Wyświetl opcje programisty w oknie ustawień.", "Element Call Home": "Strona główna Element Call", "Developer Settings": "Opcje programisty", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Ta strona jest chroniona przez ReCAPTCHA, więc obowiązują na niej <2>Polityka prywatności i <6>Warunki świadczenia usług Google.<9>Klikając \"Zarejestruj się\", zgadzasz się na nasze <12>Warunki świadczenia usług", "<0><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><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 and our <5>Cookie Policy.": "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 i <5>Polityce ciasteczek.", "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.", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index ad2b9340..e4dda885 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -4,7 +4,6 @@ "Logging in…": "Вход…", "{{names}}, {{name}}": "{{names}}, {{name}}", "Waiting for other participants…": "Ожидание других участников…", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Этот сайт защищён ReCAPTCHA от Google, ознакомьтесь с их <2>Политикой конфиденциальности и <6>Пользовательским соглашением.<9>Нажимая \"Зарегистрироваться\", вы также принимаете наши <12>Положения и условия.", "This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?", "Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.", "Submit feedback": "Отправить отзыв", @@ -12,8 +11,6 @@ "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>{userLis}", "Grid layout menu": "Меню \"Расположение сеткой\"", - "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>положениями и условиями", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?<1>Так вы можете оставить своё имя и задать аватар для будущих звонков.", "<0>Create an account Or <2>Access as a guest": "<0>Создать аккаунт или <2>Зайти как гость", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Уже есть аккаунт?<1><0>Войти с ним или <2>Зайти как гость", diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index 57b0e261..6f588a91 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -65,7 +65,6 @@ "Unmute microphone": "Zrušiť stlmenie mikrofónu", "Turn on camera": "Zapnúť kameru", "Turn off camera": "Vypnúť kameru", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov a <6>Podmienky poskytovania služieb spoločnosti Google.<9>Kliknutím na tlačidlo \"Registrovať sa\" vyjadrujete súhlas s našimi <12>Podmienkami poskytovania služieb", "This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?", "Speaker": "Reproduktor", "Sign out": "Odhlásiť sa", @@ -86,8 +85,6 @@ "Camera": "Kamera", "Call type menu": "Ponuka typu hovoru", "Call link copied": "Odkaz na hovor skopírovaný", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Kliknutím na \"Pripojiť sa k hovoru\" súhlasíte s našimi <2>Podmienkami", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Kliknutím na tlačidlo \"Prejsť\" súhlasíte s našimi <2>Podmienkami", "Avatar": "Obrázok", "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í.", @@ -115,5 +112,9 @@ "<0>Thanks for your feedback!": "<0> Ďakujeme za vašu spätnú väzbu!", "<0>We'd love to hear your feedback so we can improve your experience.": "<0> Radi si vypočujeme vašu spätnú väzbu, aby sme mohli zlepšiť vaše skúsenosti.", "{{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)": "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)", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google a <6>Podmienky poskytovania služieb.<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)", + "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ť." } diff --git a/public/locales/tr/app.json b/public/locales/tr/app.json index dad45eb2..b2494fe5 100644 --- a/public/locales/tr/app.json +++ b/public/locales/tr/app.json @@ -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.", "Audio": "Ses", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "\"Git\"e tıklayarak,<2>hükümler ve koşulları kabul etmiş sayılırsınız", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşulları kabul etmiş sayılırsınız", "Call link copied": "Arama bağlantısı kopyalandı", "Call type menu": "Arama tipi menüsü", "Camera": "Kamera", diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 6e5ab0c8..af192f88 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -15,7 +15,6 @@ "Unmute microphone": "Увімкнути мікрофон", "Turn on camera": "Увімкнути камеру", "Turn off camera": "Вимкнути камеру", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності і <6>Умови надання послуг Google.<9>Натискаючи кнопку «Зареєструватися», ви погоджуєтеся з нашими <12>Умовами та положеннями", "This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?", "Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.", "Take me Home": "Перейти до Домівки", @@ -84,8 +83,6 @@ "Camera": "Камера", "Call type menu": "Меню типу виклику", "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": "Аватар", "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.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал налагодження.", @@ -115,5 +112,9 @@ "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.", "How did it go?": "Вам усе сподобалось?", "{{displayName}} is presenting": "{{displayName}} представляє", - "Show connection stats": "Показати стан з'єднання" + "Show connection stats": "Показати стан з'єднання", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Натискаючи \"Далі\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності і <6>Умови надання послуг Google.<9>Натискаючи \"Зареєструватися\", ви погоджуєтеся з нашою <12>Ліцензійною угодою з кінцевим користувачем (EULA)", + "Element Call is temporarily not end-to-end encrypted while we test scalability.": "Виклики Element тимчасово не захищаються наскрізним шифруванням, поки ми тестуємо масштабованість." } diff --git a/public/locales/vi/app.json b/public/locales/vi/app.json index d67f05ae..6757c05e 100644 --- a/public/locales/vi/app.json +++ b/public/locales/vi/app.json @@ -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?", "Recaptcha not loaded": "Chưa tải được Recaptcha", "Debug log request": "Yêu cầu nhật ký gỡ lỗi", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "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 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.", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<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?<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.", "<0>Oops, something's gone wrong.": "<0>Ối, có cái gì đó sai.", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index a7126f8d..2673c2ee 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -14,7 +14,6 @@ "Unmute microphone": "取消麦克风静音", "Turn on camera": "开启摄像头", "Turn off camera": "关闭摄像头", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "本网站受reCaptcha保护,并适用Google<2>隐私政策和<6>服务条款。<9>点击\"注册\"则表明您同意我们的<12>条款和条件", "This call already exists, would you like to join?": "该通话已存在,你想加入吗?", "Thanks! We'll get right on it.": "谢谢!我们会马上去做的。", "Take me Home": "返回主页", @@ -89,12 +88,10 @@ "Copied!": "已复制!", "Confirm password": "确认密码", "Close": "关闭", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "点击开始则代表同意我们的<2>条款和条件<2>", "Change layout": "更改布局", "Camera": "摄像头", "Call type menu": "通话类型菜单", "Call link copied": "链接已复制", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "点击“现在加入”则表示同意我们的<2>条款与条件<2>", "Avatar": "头像", "<0>Oops, something's gone wrong.": "<0>哎哟,出问题了。", "<0><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.": "" diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index b507a755..28eed1a5 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -22,7 +22,6 @@ "Unmute microphone": "取消麥克風靜音", "Turn on camera": "開啟相機", "Turn off camera": "關閉相機", - "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "此網站使用Google 驗證碼技術保護,適用<2>隱私條款 與<6>條款與細則 。<9>按下「註冊」,表示您同意我們的<12>條款與細則", "This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?", "Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。", "Take me Home": "帶我回主畫面", @@ -94,8 +93,6 @@ "Camera": "相機", "Call type menu": "通話類型選單", "Call link copied": "已複製通話連結", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "當您按下「加入通話」,您也同時同意了我們的條款與細則", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "當您按下「前往」,你也同意了我們的條款與細則", "Avatar": "大頭照", "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.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。", @@ -115,5 +112,9 @@ "{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。", "How did it go?": "進展如何?", "{{displayName}} is presenting": "{{displayName}} 正在展示", - "Show connection stats": "顯示連線統計資料" + "Show connection stats": "顯示連線統計資料", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "點擊「前往」即表示您同意我們的<2>終端使用者授權協議 (EULA)", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)": "此網站被 ReCAPTCHA 保護,並適用 Google 的<2>隱私權政策與<6>服務條款。<9>點擊「註冊」即表示您同意我們的<12>終端使用者授權協議 (EULA)", + "Element Call is temporarily not end-to-end encrypted while we test scalability.": "在我們測試可擴展性時,Element Call 暫時未進行端到端加密。" } diff --git a/src/App.tsx b/src/App.tsx index e9a1129d..71cc9710 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { Suspense, useEffect, useState } from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import * as Sentry from "@sentry/react"; import { OverlayProvider } from "@react-aria/overlays"; +import { History } from "history"; import { HomePage } from "./home/HomePage"; import { LoginPage } from "./auth/LoginPage"; @@ -50,6 +51,8 @@ export default function App({ history }: AppProps) { const errorPage = ; return ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore {loaded ? ( diff --git a/src/Avatar.tsx b/src/Avatar.tsx index 9549cd48..c11d71d4 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -16,7 +16,6 @@ limitations under the License. import { useMemo, CSSProperties, HTMLAttributes, FC } from "react"; import classNames from "classnames"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { getAvatarUrl } from "./matrix-utils"; import { useClient } from "./ClientContext"; @@ -59,9 +58,6 @@ function hashStringToArrIndex(str: string, arrLength: number) { return sum % arrLength; } -const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) => - src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src; - interface Props extends HTMLAttributes { bgKey?: string; src?: string; @@ -99,10 +95,10 @@ export const Avatar: FC = ({ [size] ); - const resolvedSrc = useMemo( - () => resolveAvatarSrc(client, src, sizePx), - [client, src, sizePx] - ); + const resolvedSrc = useMemo(() => { + if (!client || !src || !sizePx) return undefined; + return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src; + }, [client, src, sizePx]); const backgroundColor = useMemo(() => { const index = hashStringToArrIndex( diff --git a/src/Banner.module.css b/src/Banner.module.css new file mode 100644 index 00000000..6acc9418 --- /dev/null +++ b/src/Banner.module.css @@ -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); +} diff --git a/src/Banner.tsx b/src/Banner.tsx new file mode 100644 index 00000000..fcc68a3b --- /dev/null +++ b/src/Banner.tsx @@ -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
{children}
; +}; diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index ba1b217b..69a9295e 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -20,9 +20,9 @@ import { useEffect, useState, createContext, - useMemo, useContext, useRef, + useMemo, } from "react"; import { useHistory } from "react-router-dom"; import { MatrixClient } from "matrix-js-sdk/src/client"; @@ -31,9 +31,9 @@ import { useTranslation } from "react-i18next"; import { ErrorView } from "./FullScreenView"; import { - initClient, CryptoStoreIntegrityError, fallbackICEServerAllowed, + initClient, } from "./matrix-utils"; import { widget } from "./widget"; import { @@ -47,184 +47,141 @@ import { Config } from "./config/Config"; declare global { interface Window { matrixclient: MatrixClient; - isPasswordlessUser: boolean; + passwordlessUser: boolean; } } -export interface Session { - user_id: string; - device_id: string; - access_token: string; +export type ClientState = ValidClientState | ErrorState; + +export type ValidClientState = { + state: "valid"; + authenticated?: AuthenticatedClient; + setClient: (params?: SetClientParams) => void; +}; + +export type AuthenticatedClient = { + client: MatrixClient; + isPasswordlessUser: boolean; + changePassword: (password: string) => Promise; + logout: () => void; +}; + +export type ErrorState = { + state: "error"; + error: Error; +}; + +export type SetClientParams = { + client: MatrixClient; + session: Session; +}; + +const ClientContext = createContext(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; - 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 = "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; - logout: () => void; - setClient: (client: MatrixClient, session: Session) => void; - error?: Error; -} - -const ClientContext = createContext(null); - -type ClientProviderState = Omit< - ClientState, - "changePassword" | "logout" | "setClient" -> & { error?: Error }; - interface Props { children: JSX.Element; } export const ClientProvider: FC = ({ children }) => { const history = useHistory(); - const initializing = useRef(false); - const [ - { loading, isAuthenticated, isPasswordlessUser, client, userName, error }, - setState, - ] = useState({ - loading: true, - isAuthenticated: false, - isPasswordlessUser: false, - client: undefined, - userName: null, - error: undefined, - }); + const [initClientState, setInitClientState] = useState< + InitResult | undefined + >(undefined); + + const initializing = useRef(false); useEffect(() => { // In case the component is mounted, unmounted, and remounted quickly (as // React does in strict mode), we need to make sure not to doubly initialize - // the client + // the client. if (initializing.current) return; initializing.current = true; - const init = async (): Promise< - Pick - > => { - if (widget) { - // We're inside a widget, so let's engage *matryoshka mode* - logger.log("Using a matryoshka client"); - return { - client: await widget.client, - isPasswordlessUser: false, - }; - } else { - // We're running as a standalone application - try { - const session = loadSession(); - if (!session) return { client: undefined, isPasswordlessUser: false }; - - logger.log("Using a standalone client"); - - /* eslint-disable camelcase */ - const { user_id, device_id, access_token, passwordlessUser } = - session; - - 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, - userId: user_id, - deviceId: device_id, - fallbackICEServerAllowed: fallbackICEServerAllowed, - foci, - }, - true - ), - isPasswordlessUser: passwordlessUser, - }; - } catch (err) { - if (err instanceof CryptoStoreIntegrityError) { - // We can't use this session anymore, so let's log it out - try { - const client = await initClient( - { - 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); - } catch (err_) { - logger.warn( - "The previous session was lost, and we couldn't log it out, " + - "either" - ); - } - } - throw err; - } - /* eslint-enable camelcase */ - } catch (err) { - clearSession(); - throw err; + loadClient() + .then((maybeClient) => { + if (!maybeClient) { + logger.error("Failed to initialize client"); + return; } - } - }; - 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, - }); + setInitClientState(maybeClient); }) + .catch((err) => logger.error(err)) .finally(() => (initializing.current = false)); }, []); const changePassword = useCallback( async (password: string) => { - const { tempPassword, ...session } = loadSession(); + const session = loadSession(); + if (!initClientState?.client || !session) { + return; + } - await client.setPassword( + await initClientState.client.setPassword( { type: "m.login.password", identifier: { @@ -232,73 +189,56 @@ export const ClientProvider: FC = ({ children }) => { user: session.user_id, }, user: session.user_id, - password: tempPassword, + password: session.tempPassword, }, password ); saveSession({ ...session, passwordlessUser: false }); - setState({ - client, - loading: false, - isAuthenticated: true, - isPasswordlessUser: false, - userName: client.getUserIdLocalpart(), - error: undefined, + setInitClientState({ + client: initClientState.client, + passwordlessUser: false, }); }, - [client] + [initClientState?.client] ); const setClient = useCallback( - (newClient: MatrixClient, session: Session) => { - if (client && client !== newClient) { - client.stopClient(); + (clientParams?: SetClientParams) => { + const oldClient = initClientState?.client; + const newClient = clientParams?.client; + if (oldClient && oldClient !== newClient) { + oldClient.stopClient(); } - if (newClient) { - saveSession(session); - - setState({ - client: newClient, - loading: false, - isAuthenticated: true, - isPasswordlessUser: session.passwordlessUser, - userName: newClient.getUserIdLocalpart(), - error: undefined, + if (clientParams) { + saveSession(clientParams.session); + setInitClientState({ + client: clientParams.client, + passwordlessUser: clientParams.session.passwordlessUser, }); } else { clearSession(); - - setState({ - client: undefined, - loading: false, - isAuthenticated: false, - isPasswordlessUser: false, - userName: null, - error: undefined, - }); + setInitClientState(undefined); } }, - [client] + [initClientState?.client] ); const logout = useCallback(async () => { + const client = initClientState?.client; + if (!client) { + return; + } + await client.logout(true); await client.clearStores(); clearSession(); - setState({ - client: undefined, - loading: false, - isAuthenticated: false, - isPasswordlessUser: true, - userName: "", - error: undefined, - }); + setInitClientState(undefined); history.push("/"); PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest); - }, [history, client]); + }, [history, initClientState?.client]); const { t } = useTranslation(); @@ -310,61 +250,140 @@ export const ClientProvider: FC = ({ children }) => { if (!widget) loadChannel?.postMessage({}); }, []); + const [alreadyOpenedErr, setAlreadyOpenedErr] = useState( + undefined + ); useEventTarget( loadChannel, "message", useCallback(() => { - client?.stopClient(); - - setState((prev) => ({ - ...prev, - error: translatedError( - "This application has been opened in another tab.", - t - ), - })); - }, [client, setState, t]) + initClientState?.client.stopClient(); + setAlreadyOpenedErr( + translatedError("This application has been opened in another tab.", t) + ); + }, [initClientState?.client, setAlreadyOpenedErr, t]) ); - const context = useMemo( - () => ({ - loading, - isAuthenticated, - isPasswordlessUser, - client, - changePassword, - logout, - userName, - setClient, - error: undefined, - }), - [ - loading, - isAuthenticated, - isPasswordlessUser, - client, - changePassword, - logout, - userName, - setClient, - ] - ); + 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(() => { - window.matrixclient = client; - window.isPasswordlessUser = isPasswordlessUser; + if (!initClientState) { + return; + } + + window.matrixclient = initClientState.client; + window.passwordlessUser = initClientState.passwordlessUser; if (PosthogAnalytics.hasInstance()) PosthogAnalytics.instance.onLoginStatusChanged(); - }, [client, isPasswordlessUser]); + }, [initClientState]); - if (error) { - return ; + if (alreadyOpenedErr) { + return ; } return ( - {children} + {children} ); }; -export const useClient = () => useContext(ClientContext); +type InitResult = { + client: MatrixClient; + passwordlessUser: boolean; +}; + +async function loadClient(): Promise { + if (widget) { + // We're inside a widget, so let's engage *matryoshka mode* + logger.log("Using a matryoshka client"); + const client = await widget.client; + return { + client, + passwordlessUser: false, + }; + } else { + // We're running as a standalone application + try { + const session = loadSession(); + if (!session) { + throw new Error("No session stored"); + } + + logger.log("Using a standalone client"); + + /* eslint-disable camelcase */ + const { user_id, device_id, access_token, passwordlessUser } = session; + const initClientParams = { + baseUrl: Config.defaultHomeserverUrl()!, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + fallbackICEServerAllowed: fallbackICEServerAllowed, + livekitServiceURL: Config.get().livekit!.livekit_service_url, + }; + + try { + const client = await initClient(initClientParams, true); + return { + client, + passwordlessUser, + }; + } catch (err) { + if (err instanceof CryptoStoreIntegrityError) { + // We can't use this session anymore, so let's log it out + try { + const client = await initClient(initClientParams, false); // Don't need the crypto store just to log out) + await client.logout(true); + } catch (err) { + logger.warn( + "The previous session was lost, and we couldn't log it out, " + + err + + "either" + ); + } + } + throw err; + } + /* eslint-enable camelcase */ + } catch (err) { + clearSession(); + 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); +}; diff --git a/src/E2EEBanner.module.css b/src/E2EEBanner.module.css new file mode 100644 index 00000000..dd777493 --- /dev/null +++ b/src/E2EEBanner.module.css @@ -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); +} diff --git a/src/E2EEBanner.tsx b/src/E2EEBanner.tsx new file mode 100644 index 00000000..957813e3 --- /dev/null +++ b/src/E2EEBanner.tsx @@ -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 ( + +
+ + + Element Call is temporarily not end-to-end encrypted while we test + scalability. + +
+
+ ); +}; diff --git a/src/E2EELock.module.css b/src/E2EELock.module.css new file mode 100644 index 00000000..504ace7b --- /dev/null +++ b/src/E2EELock.module.css @@ -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); +} diff --git a/src/E2EELock.tsx b/src/E2EELock.tsx new file mode 100644 index 00000000..d032b036 --- /dev/null +++ b/src/E2EELock.tsx @@ -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 ( + + + + ); +}; + +/** + * 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(); + const { buttonProps } = useButton({}, buttonRef); + + return ( +
+ +
+ ); +}; diff --git a/src/Facepile.tsx b/src/Facepile.tsx index f5ffff62..0c9ec239 100644 --- a/src/Facepile.tsx +++ b/src/Facepile.tsx @@ -47,8 +47,8 @@ export function Facepile({ }: Props) { const { t } = useTranslation(); - const _size = sizes.get(size); - const _overlap = overlapMap[size]; + const _size = sizes.get(size)!; + const _overlap = overlapMap[size]!; const title = useMemo(() => { return members.reduce( diff --git a/src/ListBox.tsx b/src/ListBox.tsx index 0ee2542a..b7ec7c72 100644 --- a/src/ListBox.tsx +++ b/src/ListBox.tsx @@ -36,15 +36,16 @@ export function ListBox({ listBoxRef, ...rest }: ListBoxProps) { - const ref = useRef(); - if (!listBoxRef) listBoxRef = ref; + const ref = useRef(null); - const { listBoxProps } = useListBox(rest, state, listBoxRef); + const listRef = listBoxRef ?? ref; + + const { listBoxProps } = useListBox(rest, state, listRef); return (
    {[...state.collection].map((item) => ( @@ -66,7 +67,7 @@ interface OptionProps { } function Option({ item, state, className }: OptionProps) { - const ref = useRef(); + const ref = useRef(null); const { optionProps, isSelected, isFocused, isDisabled } = useOption( { key: item.key }, state, @@ -83,7 +84,11 @@ function Option({ item, state, className }: OptionProps) { const origPointerUp = optionProps.onPointerUp; delete optionProps.onPointerUp; optionProps.onClick = useCallback( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore (e) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore origPointerUp(e as unknown as PointerEvent); }, [origPointerUp] diff --git a/src/Menu.tsx b/src/Menu.tsx index 1e158413..d7982df7 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -26,7 +26,7 @@ import styles from "./Menu.module.css"; interface MenuProps extends AriaMenuOptions { className?: String; - onClose?: () => void; + onClose: () => void; onAction: (value: Key) => void; label?: string; } @@ -39,7 +39,7 @@ export function Menu({ ...rest }: MenuProps) { const state = useTreeState({ ...rest, selectionMode: "none" }); - const menuRef = useRef(); + const menuRef = useRef(null); const { menuProps } = useMenu(rest, state, menuRef); return ( @@ -69,7 +69,7 @@ interface MenuItemProps { } function MenuItem({ item, state, onAction, onClose }: MenuItemProps) { - const ref = useRef(); + const ref = useRef(null); const { menuItemProps } = useMenuItem( { key: item.key, diff --git a/src/Modal.tsx b/src/Modal.tsx index 07217539..56db481e 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -55,7 +55,7 @@ export function Modal({ ...rest }: ModalProps) { const { t } = useTranslation(); - const modalRef = useRef(); + const modalRef = useRef(null); const { overlayProps, underlayProps } = useOverlay( { ...rest, onClose }, modalRef @@ -63,7 +63,7 @@ export function Modal({ usePreventScroll(); const { modalProps } = useModal(); const { dialogProps, titleProps } = useDialog(rest, modalRef); - const closeButtonRef = useRef(); + const closeButtonRef = useRef(null); const { buttonProps: closeButtonProps } = useButton( { onPress: () => onClose(), diff --git a/src/SequenceDiagramViewerPage.tsx b/src/SequenceDiagramViewerPage.tsx index 208352fe..9fb66f94 100644 --- a/src/SequenceDiagramViewerPage.tsx +++ b/src/SequenceDiagramViewerPage.tsx @@ -36,6 +36,9 @@ export function SequenceDiagramViewerPage() { const [debugLog, setDebugLog] = useState(); const [selectedUserId, setSelectedUserId] = useState(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const onChangeDebugLog = useCallback((e) => { if (e.target.files && e.target.files.length > 0) { e.target.files[0].text().then((text: string) => { @@ -55,7 +58,7 @@ export function SequenceDiagramViewerPage() { onChange={onChangeDebugLog} /> - {debugLog && ( + {debugLog && selectedUserId && ( ( const tooltipTriggerProps = { delay: 250, ...rest }; const tooltipState = useTooltipTriggerState(tooltipTriggerProps); const triggerRef = useObjectRef(ref); - const overlayRef = useRef(); + const overlayRef = useRef(null); const { triggerProps, tooltipProps } = useTooltipTrigger( tooltipTriggerProps, tooltipState, diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 5648edd9..9df3309d 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -36,7 +36,7 @@ interface UserMenuProps { isAuthenticated: boolean; isPasswordlessUser: boolean; displayName: string; - avatarUrl: string; + avatarUrl?: string; onAction: (value: string) => void; } @@ -119,21 +119,24 @@ export function UserMenu({ )} - {(props) => ( - - {items.map(({ key, icon: Icon, label, dataTestid }) => ( - - - {label} - - ))} - - )} + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any) => ( + + {items.map(({ key, icon: Icon, label, dataTestid }) => ( + + + {label} + + ))} + + ) + } ); } diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 4702c4fe..6a83133e 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -17,7 +17,7 @@ limitations under the License. import { useCallback, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; -import { useClient } from "./ClientContext"; +import { useClientLegacy } from "./ClientContext"; import { useProfile } from "./profile/useProfile"; import { useModalTriggerState } from "./Modal"; import { SettingsModal } from "./settings/SettingsModal"; @@ -30,8 +30,7 @@ interface Props { export function UserMenuContainer({ preventNavigation = false }: Props) { const location = useLocation(); const history = useHistory(); - const { isAuthenticated, isPasswordlessUser, logout, userName, client } = - useClient(); + const { client, logout, authenticated, passwordlessUser } = useClientLegacy(); const { displayName, avatarUrl } = useProfile(client); const { modalState, modalProps } = useModalTriggerState(); @@ -49,7 +48,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { modalState.open(); break; case "logout": - logout(); + logout?.(); break; case "login": history.push("/login", { state: { from: location } }); @@ -59,19 +58,18 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { [history, location, logout, modalState] ); + const userName = client?.getUserIdLocalpart() ?? ""; return ( <> - {modalState.isOpen && ( + {modalState.isOpen && client && ( ; + private identificationPromise?: Promise; private readonly enabled: boolean = false; private anonymity = Anonymity.Disabled; private platformSuperProperties = {}; @@ -255,7 +255,9 @@ export class PosthogAnalytics { } catch (e) { // The above could fail due to network requests, but not essential to starting the application, // 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) { this.posthog.identify(analyticsID); @@ -366,7 +368,7 @@ export class PosthogAnalytics { if (anonymity === Anonymity.Pseudonymous) { this.setRegistrationType( - window.matrixclient.isGuest() || window.isPasswordlessUser + window.matrixclient.isGuest() || window.passwordlessUser ? RegistrationType.Guest : RegistrationType.Registered ); diff --git a/src/array-utils.ts b/src/array-utils.ts index cf37e4cf..6aeb1818 100644 --- a/src/array-utils.ts +++ b/src/array-utils.ts @@ -22,10 +22,10 @@ limitations under the License. // Array.prototype.findLastIndex export function findLastIndex( array: T[], - predicate: (item: T) => boolean + predicate: (item: T, index: number) => boolean ): number | null { for (let i = array.length - 1; i >= 0; i--) { - if (predicate(array[i])) return i; + if (predicate(array[i], i)) return i; } return null; @@ -34,5 +34,11 @@ export function findLastIndex( /** * Counts the number of elements in an array that satsify the given predicate. */ -export const count = (array: T[], predicate: (item: T) => boolean): number => - array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0); +export const count = ( + array: T[], + predicate: (item: T, index: number) => boolean +): number => + array.reduce( + (acc, item, index) => (predicate(item, index) ? acc + 1 : acc), + 0 + ); diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx index c5a090f1..f17f24fa 100644 --- a/src/auth/LoginPage.tsx +++ b/src/auth/LoginPage.tsx @@ -35,8 +35,8 @@ export const LoginPage: FC = () => { const { setClient } = useClient(); const login = useInteractiveLogin(); const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable - const usernameRef = useRef(); - const passwordRef = useRef(); + const usernameRef = useRef(null); + const passwordRef = useRef(null); const history = useHistory(); const location = useLocation(); const [loading, setLoading] = useState(false); @@ -49,12 +49,27 @@ export const LoginPage: FC = () => { e.preventDefault(); 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) .then(([client, session]) => { - setClient(client, session); + if (!setClient) { + return; + } - if (location.state && location.state.from) { - history.push(location.state.from); + setClient({ client, session }); + + 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 { history.push("/"); } diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index 1fd3911c..a486aa46 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -30,7 +30,7 @@ import { Trans, useTranslation } from "react-i18next"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { Button } from "../button"; -import { useClient } from "../ClientContext"; +import { useClientLegacy } from "../ClientContext"; import { useInteractiveRegistration } from "./useInteractiveRegistration"; import styles from "./LoginPage.module.css"; import { ReactComponent as Logo } from "../icons/LogoLarge.svg"; @@ -45,17 +45,17 @@ export const RegisterPage: FC = () => { const { t } = useTranslation(); usePageTitle(t("Register")); - const { loading, isAuthenticated, isPasswordlessUser, client, setClient } = - useClient(); - const confirmPasswordRef = useRef(); + const { loading, authenticated, passwordlessUser, client, setClient } = + useClientLegacy(); + + const confirmPasswordRef = useRef(null); const history = useHistory(); const location = useLocation(); const [registering, setRegistering] = useState(false); const [error, setError] = useState(); const [password, setPassword] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); - const [privacyPolicyUrl, recaptchaKey, register] = - useInteractiveRegistration(); + const { recaptchaKey, register } = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const onSubmitRegisterForm = useCallback( @@ -76,10 +76,15 @@ export const RegisterPage: FC = () => { userName, password, userName, - recaptchaResponse + recaptchaResponse, + passwordlessUser ); - if (client && isPasswordlessUser) { + if (!client || !client.groupCallEventHandler || !setClient) { + return; + } + + if (passwordlessUser) { // Migrate the user's rooms for (const groupCall of client.groupCallEventHandler.groupCalls.values()) { const roomId = groupCall.room.roomId; @@ -87,7 +92,11 @@ export const RegisterPage: FC = () => { try { await newClient.joinRoom(roomId); } catch (error) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore 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 newClient.joinRoom(roomId); } else { @@ -98,13 +107,17 @@ export const RegisterPage: FC = () => { } } - setClient(newClient, session); + setClient({ client: newClient, session }); PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date()); }; submit() .then(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore if (location.state?.from) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore history.push(location.state?.from); } else { history.push("/"); @@ -120,7 +133,7 @@ export const RegisterPage: FC = () => { register, location, history, - isPasswordlessUser, + passwordlessUser, reset, execute, client, @@ -137,10 +150,10 @@ export const RegisterPage: FC = () => { }, [password, passwordConfirmation, t]); useEffect(() => { - if (!loading && isAuthenticated && !isPasswordlessUser && !registering) { + if (!loading && authenticated && !passwordlessUser && !registering) { history.push("/"); } - }, [loading, history, isAuthenticated, isPasswordlessUser, registering]); + }, [loading, history, authenticated, passwordlessUser, registering]); if (loading) { return ; @@ -211,7 +224,7 @@ export const RegisterPage: FC = () => { apply.
    By clicking "Register", you agree to our{" "} - + End User Licensing Agreement (EULA) diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 81b923f9..07dec12d 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -41,8 +41,10 @@ export const useInteractiveLogin = () => }, password, }), - stateUpdated: null, - requestEmailToken: null, + stateUpdated: (...args) => {}, + requestEmailToken: (...args): Promise<{ sid: string }> => { + return Promise.resolve({ sid: "" }); + }, }); // XXX: This claims to return an IAuthData which contains none of these diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 156842b0..a4863671 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -22,29 +22,33 @@ import { initClient } from "../matrix-utils"; import { Session } from "../ClientContext"; import { Config } from "../config/Config"; -export const useInteractiveRegistration = (): [ - string, - string, - ( +export const useInteractiveRegistration = (): { + privacyPolicyUrl?: string; + recaptchaKey?: string; + register: ( username: string, password: string, displayName: string, recaptchaResponse: string, - passwordlessUser?: boolean - ) => Promise<[MatrixClient, Session]> -] => { - const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState(); - const [recaptchaKey, setRecaptchaKey] = useState(); + passwordlessUser: boolean + ) => Promise<[MatrixClient, Session]>; +} => { + const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState( + undefined + ); + const [recaptchaKey, setRecaptchaKey] = useState( + undefined + ); const authClient = useRef(); if (!authClient.current) { authClient.current = createClient({ - baseUrl: Config.defaultHomeserverUrl(), + baseUrl: Config.defaultHomeserverUrl()!, }); } useEffect(() => { - authClient.current.registerRequest({}).catch((error) => { + authClient.current!.registerRequest({}).catch((error) => { setPrivacyPolicyUrl( error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url ); @@ -58,12 +62,12 @@ export const useInteractiveRegistration = (): [ password: string, displayName: string, recaptchaResponse: string, - passwordlessUser?: boolean + passwordlessUser: boolean ): Promise<[MatrixClient, Session]> => { const interactiveAuth = new InteractiveAuth({ - matrixClient: authClient.current, + matrixClient: authClient.current!, doRequest: (auth) => - authClient.current.registerRequest({ + authClient.current!.registerRequest({ username, password, 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 @@ -95,7 +101,7 @@ export const useInteractiveRegistration = (): [ const client = await initClient( { - baseUrl: Config.defaultHomeserverUrl(), + baseUrl: Config.defaultHomeserverUrl()!, accessToken: access_token, userId: user_id, deviceId: device_id, @@ -117,7 +123,7 @@ export const useInteractiveRegistration = (): [ session.tempPassword = password; } - const user = client.getUser(client.getUserId()); + const user = client.getUser(client.getUserId()!)!; user.setRawDisplayName(displayName); user.setDisplayName(displayName); @@ -126,5 +132,5 @@ export const useInteractiveRegistration = (): [ [] ); - return [privacyPolicyUrl, recaptchaKey, register]; + return { privacyPolicyUrl, recaptchaKey, register }; }; diff --git a/src/auth/useRecaptcha.ts b/src/auth/useRecaptcha.ts index ef3c7c26..647370da 100644 --- a/src/auth/useRecaptcha.ts +++ b/src/auth/useRecaptcha.ts @@ -34,7 +34,7 @@ interface RecaptchaPromiseRef { reject: (error: Error) => void; } -export const useRecaptcha = (sitekey: string) => { +export const useRecaptcha = (sitekey?: string) => { const { t } = useTranslation(); const [recaptchaId] = useState(() => randomString(16)); const promiseRef = useRef(); @@ -68,9 +68,9 @@ export const useRecaptcha = (sitekey: string) => { } }, [recaptchaId, sitekey]); - const execute = useCallback(() => { + const execute = useCallback((): Promise => { if (!sitekey) { - return Promise.resolve(null); + return Promise.resolve(""); } if (!window.grecaptcha) { diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts index d780e99b..983789ba 100644 --- a/src/auth/useRegisterPasswordlessUser.ts +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -23,19 +23,23 @@ import { generateRandomName } from "../auth/generateRandomName"; import { useRecaptcha } from "../auth/useRecaptcha"; interface UseRegisterPasswordlessUserType { - privacyPolicyUrl: string; + privacyPolicyUrl?: string; registerPasswordlessUser: (displayName: string) => Promise; - recaptchaId: string; + recaptchaId?: string; } export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { const { setClient } = useClient(); - const [privacyPolicyUrl, recaptchaKey, register] = + const { privacyPolicyUrl, recaptchaKey, register } = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const registerPasswordlessUser = useCallback( async (displayName: string) => { + if (!setClient) { + throw new Error("No client context"); + } + try { const recaptchaResponse = await execute(); const userName = generateRandomName(); @@ -46,7 +50,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { recaptchaResponse, true ); - setClient(client, session); + setClient({ client, session }); } catch (e) { reset(); throw e; diff --git a/src/button/Button.module.css b/src/button/Button.module.css index b19db46f..cf196d64 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -61,6 +61,10 @@ limitations under the License. outline: auto; } +.toolbarButton:disabled { + opacity: 0.55; +} + .toolbarButton, .toolbarButtonSecondary { width: 50px; diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 50184f09..2de62a80 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -77,6 +77,7 @@ interface Props { children: Element[]; onPress: (e: PressEvent) => void; onPressStart: (e: PressEvent) => void; + disabled: boolean; // TODO: add all props for {(props: JSX.IntrinsicAttributes) => ( - + {t("Video call")} diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index 4c5522df..018e0512 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -16,7 +16,7 @@ limitations under the License. import { useTranslation } from "react-i18next"; -import { useClient } from "../ClientContext"; +import { useClientState } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; import { UnauthenticatedView } from "./UnauthenticatedView"; import { RegisteredView } from "./RegisteredView"; @@ -26,16 +26,18 @@ export function HomePage() { const { t } = useTranslation(); usePageTitle(t("Home")); - const { isAuthenticated, isPasswordlessUser, loading, error, client } = - useClient(); + const clientState = useClientState(); - if (loading) { + if (!clientState) { return ; - } else if (error) { - return ; + } else if (clientState.state === "error") { + return ; } else { - return isAuthenticated ? ( - + return clientState.authenticated ? ( + ) : ( ); diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 3d0c6eb0..76da921d 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -39,6 +39,7 @@ import { Form } from "../form/Form"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import { useOptInAnalytics } from "../settings/useSetting"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; +import { E2EEBanner } from "../E2EEBanner"; interface Props { client: MatrixClient; @@ -146,6 +147,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { )} + {error && ( diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 88424ca2..f479901f 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -41,6 +41,8 @@ import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { useOptInAnalytics } from "../settings/useSetting"; +import { Config } from "../config/Config"; +import { E2EEBanner } from "../E2EEBanner"; export const UnauthenticatedView: FC = () => { const { setClient } = useClient(); @@ -48,8 +50,7 @@ export const UnauthenticatedView: FC = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [optInAnalytics] = useOptInAnalytics(); - const [privacyPolicyUrl, recaptchaKey, register] = - useInteractiveRegistration(); + const { recaptchaKey, register } = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { modalState, modalProps } = useModalTriggerState(); @@ -82,9 +83,15 @@ export const UnauthenticatedView: FC = () => { try { [roomAlias] = await createRoom(client, roomName, ptt); } catch (error) { + if (!setClient) { + throw error; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore if (error.errcode === "M_ROOM_IN_USE") { setOnFinished(() => { - setClient(client, session); + setClient({ client, session }); const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName); history.push(`/${aliasLocalpart}`); }); @@ -98,7 +105,11 @@ export const UnauthenticatedView: FC = () => { } // 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]}`); } @@ -164,11 +175,12 @@ export const UnauthenticatedView: FC = () => { By clicking "Go", you agree to our{" "} - + End User Licensing Agreement (EULA) + {error && ( @@ -201,7 +213,7 @@ export const UnauthenticatedView: FC = () => { - {modalState.isOpen && ( + {modalState.isOpen && onFinished && ( )} diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index b1eb2e7a..0f5d8ef9 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -42,7 +42,7 @@ function getLastTs(client: MatrixClient, r: Room) { return ts; } - const myUserId = client.getUserId(); + const myUserId = client.getUserId()!; if (r.getMyMembership() !== "join") { const membershipEvent = r.currentState.getStateEvents( @@ -83,23 +83,28 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { useEffect(() => { function updateRooms() { + if (!client.groupCallEventHandler) { + return; + } + const groupCalls = client.groupCallEventHandler.groupCalls.values(); 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 sortedRooms = sortRooms(client, filteredRooms); - const items: GroupCallRoom[] = sortedRooms.map((room) => { - const groupCall = client.getGroupCallForRoom(room.roomId); + const items = sortedRooms.map((room) => { + const groupCall = client.getGroupCallForRoom(room.roomId)!; return { roomAlias: room.getCanonicalAlias(), roomName: room.name, - avatarUrl: room.getMxcAvatarUrl(), + avatarUrl: room.getMxcAvatarUrl()!, room, groupCall, - participants: [...groupCall.participants.keys()], + participants: [...groupCall!.participants.keys()], }; }); - setRooms(items); + + setRooms(items as GroupCallRoom[]); } updateRooms(); diff --git a/src/icons/LockOff.svg b/src/icons/LockOff.svg new file mode 100644 index 00000000..d0a73909 --- /dev/null +++ b/src/icons/LockOff.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/index.css b/src/index.css index f9e00ef3..1f372a1e 100644 --- a/src/index.css +++ b/src/index.css @@ -52,6 +52,7 @@ limitations under the License. --background: #15191e; --background-85: rgba(23, 25, 28, 0.85); --bgColor3: #444; /* This isn't found anywhere in the designs or Compound */ + --subtle-primary: #26282d; } @font-face { diff --git a/src/initializer.tsx b/src/initializer.tsx index f8a6c985..850de125 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -133,6 +133,10 @@ export class Initializer { "--background-85", import.meta.env.VITE_THEME_BACKGROUND_85 as string ); + style.setProperty( + "--subtle-primary", + import.meta.env.VITE_THEME_SUBTLE_PRIMARY as string + ); } // Custom fonts @@ -225,5 +229,6 @@ export class Initializer { resolve(); } } - private initPromise: Promise | null; + + private initPromise?: Promise; } diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index 9280fad4..484f440e 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -15,10 +15,14 @@ limitations under the License. */ import { useObjectRef } from "@react-aria/utils"; -import { AllHTMLAttributes, ChangeEvent, useEffect } from "react"; -import { useCallback } from "react"; -import { useState } from "react"; -import { forwardRef } from "react"; +import { + AllHTMLAttributes, + useEffect, + useCallback, + useState, + forwardRef, + ChangeEvent, +} from "react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; @@ -43,7 +47,7 @@ export const AvatarInputField = forwardRef( const { t } = useTranslation(); const [removed, setRemoved] = useState(false); - const [objUrl, setObjUrl] = useState(null); + const [objUrl, setObjUrl] = useState(undefined); const fileInputRef = useObjectRef(ref); @@ -52,11 +56,11 @@ export const AvatarInputField = forwardRef( const onChange = (e: Event) => { const inputEvent = e as unknown as ChangeEvent; - if (inputEvent.target.files.length > 0) { + if (inputEvent.target.files && inputEvent.target.files.length > 0) { setObjUrl(URL.createObjectURL(inputEvent.target.files[0])); setRemoved(false); } else { - setObjUrl(null); + setObjUrl(undefined); } }; @@ -77,7 +81,7 @@ export const AvatarInputField = forwardRef(
    void; + onChange?: (event: ChangeEvent) => void; } export const InputField = forwardRef< @@ -119,6 +119,8 @@ export const InputField = forwardRef< > {prefix && {prefix}} {type === "textarea" ? ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore