diff --git a/locales/cs/app.json b/locales/cs/app.json index b9793e1f..5641af6a 100644 --- a/locales/cs/app.json +++ b/locales/cs/app.json @@ -27,7 +27,9 @@ "settings": "Nastavení", "username": "Uživatelské jméno" }, - "full_screen_view_description": "<0>Odeslání ladících záznamů nám pomůže diagnostikovat problém.", + "error": { + "generic_description": "Odeslání ladících záznamů nám pomůže diagnostikovat problém." + }, "full_screen_view_h1": "<0>Oops, něco se pokazilo.", "header_label": "Domov Element Call", "join_existing_call_modal": { diff --git a/locales/de/app.json b/locales/de/app.json index 2c35b341..76310f2c 100644 --- a/locales/de/app.json +++ b/locales/de/app.json @@ -32,7 +32,6 @@ "browser_media_e2ee_unsupported": "Dein Webbrowser unterstützt keine Medien-Ende-zu-Ende-Verschlüsselung. Unterstützte Browser sind Chrome, Safari, Firefox >=117", "browser_media_e2ee_unsupported_heading": "Inkompatibler Browser", "call_ended_view": { - "body": "Deine Verbindung wurde getrennt", "create_account_button": "Konto erstellen", "create_account_prompt": "<0>Warum vergibst du nicht abschließend ein Passwort, um dein Konto zu erhalten?<1>Du kannst deinen Namen behalten und ein Profilbild für zukünftige Anrufe festlegen.", "feedback_done": "<0>Danke für deine Rückmeldung!", @@ -76,7 +75,10 @@ "matrix_id": "Matrix-ID: {{id}}" }, "disconnected_banner": "Die Verbindung zum Server wurde getrennt.", - "full_screen_view_description": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.", + "error": { + "connection_lost_description": "Deine Verbindung wurde getrennt", + "generic_description": "Übermittelte Problemberichte helfen uns, Fehler zu beheben." + }, "full_screen_view_h1": "<0>Hoppla, etwas ist schiefgelaufen.", "group_call_loader": { "banned_body": "Du wurdest aus dem Raum verbannt.", diff --git a/locales/el/app.json b/locales/el/app.json index 10b9396d..e6591691 100644 --- a/locales/el/app.json +++ b/locales/el/app.json @@ -35,7 +35,9 @@ "username": "Όνομα χρήστη", "video": "Βίντεο" }, - "full_screen_view_description": "<0>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.", + "error": { + "generic_description": "Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα." + }, "full_screen_view_h1": "<0>Ωχ, κάτι πήγε στραβά.", "header_label": "Element Κεντρική Οθόνη Κλήσεων", "join_existing_call_modal": { diff --git a/locales/en/app.json b/locales/en/app.json index f35c3579..b72af0d5 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -28,11 +28,7 @@ "text": "Ready to join?", "title": "Select app" }, - "application_opened_another_tab": "This application has been opened in another tab.", - "browser_media_e2ee_unsupported": "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117", - "browser_media_e2ee_unsupported_heading": "Incompatible Browser", "call_ended_view": { - "body": "You were disconnected from the call", "create_account_button": "Create account", "create_account_prompt": "<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", "feedback_done": "<0>Thanks for your feedback!", @@ -50,7 +46,6 @@ "back": "Back", "display_name": "Display name", "encrypted": "Encrypted", - "error": "Error", "home": "Home", "loading": "Loading…", "next": "Next", @@ -61,7 +56,6 @@ "reaction": "Reaction", "reactions": "Reactions", "settings": "Settings", - "something_went_wrong": "Something went wrong", "unencrypted": "Not encrypted", "username": "Username", "video": "Video" @@ -77,18 +71,26 @@ "show_non_member_tiles": "Show tiles for non-member media" }, "disconnected_banner": "Connectivity to the server has been lost.", - "full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.", - "full_screen_view_h1": "<0>Oops, something's gone wrong.", + "error": { + "call_not_found": "Call not found", + "call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one.", + "connection_lost": "Connection lost", + "connection_lost_description": "You were disconnected from the call.", + "e2ee_unsupported": "Incompatible browser", + "e2ee_unsupported_description": "Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.", + "generic": "Something went wrong", + "generic_description": "Submitting debug logs will help us track down the problem.", + "open_elsewhere": "Opened in another tab", + "open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page." + }, "group_call_loader": { "banned_body": "You have been banned from the room.", "banned_heading": "Banned", "call_ended_body": "You have been removed from the call.", "call_ended_heading": "Call ended", - "failed_heading": "Failed to join", - "failed_text": "Call not found or is not accessible.", "knock_reject_body": "Your request to join was declined.", "knock_reject_heading": "Access denied", - "reason": "Reason" + "reason": "Reason: {{reason}}" }, "hangup_button_label": "End call", "header_label": "Element Call Home", diff --git a/locales/es/app.json b/locales/es/app.json index 96f6710c..fc4441b2 100644 --- a/locales/es/app.json +++ b/locales/es/app.json @@ -32,7 +32,9 @@ "settings": "Ajustes", "username": "Nombre de usuario" }, - "full_screen_view_description": "<0>Subir los registros de depuración nos ayudará a encontrar el problema.", + "error": { + "generic_description": "Subir los registros de depuración nos ayudará a encontrar el problema." + }, "full_screen_view_h1": "<0>Ups, algo ha salido mal.", "header_label": "Inicio de Element Call", "join_existing_call_modal": { diff --git a/locales/et/app.json b/locales/et/app.json index ccd1c699..5a8cd94d 100644 --- a/locales/et/app.json +++ b/locales/et/app.json @@ -23,7 +23,6 @@ }, "browser_media_e2ee_unsupported": "Sinu veebibrauser ei toeta meedia läbivat krüptimist. Toetatud brauserid on Chrome, Chromium, Safari ja Firefox >=117", "call_ended_view": { - "body": "Sinu ühendus kõnega katkes", "create_account_button": "Loo konto", "create_account_prompt": "<0>Kas soovid salasõna seadistada ja sellega oma kasutajakonto alles jätta?<1>Nii saad säilitada oma nime ja määrata profiilipildi, mida saad kasutada tulevastes kõnedes", "feedback_done": "<0>Täname Sind tagasiside eest!", @@ -50,7 +49,10 @@ "username": "Kasutajanimi" }, "disconnected_banner": "Võrguühendus serveriga on katkenud.", - "full_screen_view_description": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.", + "error": { + "connection_lost_description": "Sinu ühendus kõnega katkes", + "generic_description": "Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida." + }, "full_screen_view_h1": "<0>Ohoo, midagi on nüüd katki.", "hangup_button_label": "Lõpeta kõne", "header_participants_label": "Osalejad", diff --git a/locales/fr/app.json b/locales/fr/app.json index f465244d..56f5f3ea 100644 --- a/locales/fr/app.json +++ b/locales/fr/app.json @@ -23,7 +23,6 @@ }, "browser_media_e2ee_unsupported": "Votre navigateur web ne prend pas en charge le chiffrement de bout-en-bout des médias. Les navigateurs pris en charge sont Chrome, Safari, Firefox >= 117", "call_ended_view": { - "body": "Vous avez été déconnecté de l’appel", "create_account_button": "Créer un compte", "create_account_prompt": "<0>Pourquoi ne pas créer un mot de passe pour conserver votre compte ?<1>Vous pourrez garder votre nom et définir un avatar pour vos futurs appels", "feedback_done": "<0>Merci pour votre commentaire !", @@ -48,7 +47,10 @@ "video": "Vidéo" }, "disconnected_banner": "La connexion avec le serveur a été perdue.", - "full_screen_view_description": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.", + "error": { + "connection_lost_description": "Vous avez été déconnecté de l’appel", + "generic_description": "Soumettre les journaux de débogage nous aidera à déterminer le problème." + }, "full_screen_view_h1": "<0>Oups, quelque chose s’est mal passé.", "hangup_button_label": "Terminer l’appel", "header_label": "Accueil Element Call", diff --git a/locales/id/app.json b/locales/id/app.json index c479d604..b1e928d3 100644 --- a/locales/id/app.json +++ b/locales/id/app.json @@ -23,7 +23,6 @@ }, "browser_media_e2ee_unsupported": "Peramban web Anda tidak mendukung enkripsi media ujung ke ujung. Peramban yang didukung adalah Chrome, Safari, dan Firefox >=117", "call_ended_view": { - "body": "Anda terputus dari panggilan", "create_account_button": "Buat akun", "create_account_prompt": "<0>Kenapa tidak selesaikan dengan mengatur sebuah kata sandi untuk menjaga akun Anda?<1>Anda akan dapat tetap menggunakan nama Anda dan atur sebuah avatar untuk digunakan dalam panggilan di masa mendatang", "feedback_done": "<0>Terima kasih atas masukan Anda!", @@ -48,7 +47,10 @@ "username": "Nama pengguna" }, "disconnected_banner": "Koneksi ke server telah hilang.", - "full_screen_view_description": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.", + "error": { + "connection_lost_description": "Anda terputus dari panggilan", + "generic_description": "Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya." + }, "full_screen_view_h1": "<0>Aduh, ada yang salah.", "hangup_button_label": "Akhiri panggilan", "header_label": "Beranda Element Call", diff --git a/locales/it/app.json b/locales/it/app.json index 6fe08427..35d61115 100644 --- a/locales/it/app.json +++ b/locales/it/app.json @@ -22,7 +22,6 @@ }, "browser_media_e2ee_unsupported": "Il tuo browser non supporta la crittografia end-to-end dei media. I browser supportati sono Chrome, Safari, Firefox >=117", "call_ended_view": { - "body": "Sei stato disconnesso dalla chiamata", "create_account_button": "Crea profilo", "create_account_prompt": "<0>Ti va di terminare impostando una password per mantenere il profilo?<1>Potrai mantenere il tuo nome e impostare un avatar da usare in chiamate future", "feedback_done": "<0>Grazie per la tua opinione!", @@ -46,7 +45,10 @@ "username": "Nome utente" }, "disconnected_banner": "La connessione al server è stata persa.", - "full_screen_view_description": "<0>L'invio di registri di debug ci aiuterà ad individuare il problema.", + "error": { + "connection_lost_description": "Sei stato disconnesso dalla chiamata", + "generic_description": "L'invio di registri di debug ci aiuterà ad individuare il problema." + }, "full_screen_view_h1": "<0>Ops, qualcosa è andato storto.", "hangup_button_label": "Termina chiamata", "header_label": "Inizio di Element Call", diff --git a/locales/ja/app.json b/locales/ja/app.json index 1641a63b..689b7899 100644 --- a/locales/ja/app.json +++ b/locales/ja/app.json @@ -28,7 +28,9 @@ "username": "ユーザー名", "video": "ビデオ" }, - "full_screen_view_h1": "<0>何かがうまく行きませんでした。", + "error": { + "generic_description": "何かがうまく行きませんでした。" + }, "header_label": "Element Call ホーム", "join_existing_call_modal": { "join_button": "はい、通話に参加", diff --git a/locales/lv/app.json b/locales/lv/app.json index ee48986f..39897954 100644 --- a/locales/lv/app.json +++ b/locales/lv/app.json @@ -14,7 +14,6 @@ }, "analytics_notice": "Piedalīšanās šajā beta apliecina piekrišanu anonīmu datu ievākšanai, ko mēs izmantojam, lai uzlabotu izstrādājumu. Vairāk informācijas par datiem, ko mēs ievācam, var atrast mūsu <2>privātuma nosacījumos un <5>sīkdatņu nosacījumos.", "call_ended_view": { - "body": "Tu tiki atvienots no zvana", "create_account_button": "Izveidot kontu", "create_account_prompt": "<0>Kādēļ nepabeigt ar paroles iestatīšanu, lai paturētu savu kontu?<1>Būs iespējams paturēt savu vārdu un iestatīt attēlu izmantošanai turpmākajos zvanos", "feedback_done": "<0>Paldies par atsauksmi!", @@ -38,7 +37,10 @@ "username": "Lietotājvārds" }, "disconnected_banner": "Ir zaudēts savienojums ar serveri.", - "full_screen_view_description": "<0>Atkļūdošanas žurnāla ierakstu iesūtīšana palīdzēs mums atklāt nepilnību.", + "error": { + "connection_lost_description": "Tu tiki atvienots no zvana", + "generic_description": "Atkļūdošanas žurnāla ierakstu iesūtīšana palīdzēs mums atklāt nepilnību." + }, "full_screen_view_h1": "<0>Ak vai, kaut kas nogāja greizi!", "header_label": "Element Call sākums", "join_existing_call_modal": { diff --git a/locales/pl/app.json b/locales/pl/app.json index db3986ef..1b7a5e02 100644 --- a/locales/pl/app.json +++ b/locales/pl/app.json @@ -23,7 +23,6 @@ }, "browser_media_e2ee_unsupported": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117", "call_ended_view": { - "body": "Rozłączono Cię z połączenia", "create_account_button": "Utwórz konto", "create_account_prompt": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?<1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości", "feedback_done": "<0>Dziękujemy za Twoją opinię!", @@ -51,7 +50,10 @@ "video": "Wideo" }, "disconnected_banner": "Utracono połączenie z serwerem.", - "full_screen_view_description": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.", + "error": { + "connection_lost_description": "Rozłączono Cię z połączenia", + "generic_description": "Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu." + }, "full_screen_view_h1": "<0>Ojej, coś poszło nie tak.", "hangup_button_label": "Zakończ połączenie", "header_label": "Strona główna Element Call", diff --git a/locales/ro/app.json b/locales/ro/app.json index 0b83b0f3..9f84200f 100644 --- a/locales/ro/app.json +++ b/locales/ro/app.json @@ -32,7 +32,6 @@ "browser_media_e2ee_unsupported": "Browserul dvs. web nu acceptă criptarea media end-to-end. Browserele acceptate sunt Chrome, Safari, Firefox > = 117", "browser_media_e2ee_unsupported_heading": "Browser incompatibil", "call_ended_view": { - "body": "Ai fost deconectat de la apel", "create_account_button": "Creează cont", "create_account_prompt": "<0>De ce să nu terminați prin configurarea unei parole pentru a vă păstra contul? <1>Veți putea să vă păstrați numele și să setați un avatar pentru a fi utilizat la apelurile viitoare ", "feedback_done": "<0>Vă mulțumim pentru feedback! ", @@ -76,7 +75,10 @@ "matrix_id": "ID-ul matricei: {{id}}" }, "disconnected_banner": "Conectivitatea la server a fost pierdută.", - "full_screen_view_description": "<0>Trimiterea jurnalelor de depanare ne va ajuta să urmărim problema. ", + "error": { + "connection_lost_description": "Ai fost deconectat de la apel", + "generic_description": "Trimiterea jurnalelor de depanare ne va ajuta să urmărim problema." + }, "full_screen_view_h1": "<0>Hopa, ceva nu a mers bine. ", "group_call_loader": { "banned_body": "Ai fost interzis să ieși din cameră.", diff --git a/locales/ru/app.json b/locales/ru/app.json index 725fffdd..9069537d 100644 --- a/locales/ru/app.json +++ b/locales/ru/app.json @@ -36,7 +36,9 @@ "username": "Имя пользователя", "video": "Видео" }, - "full_screen_view_description": "<0>Отправка журналов поможет нам найти и устранить проблему.", + "error": { + "generic_description": "Отправка журналов поможет нам найти и устранить проблему." + }, "full_screen_view_h1": "<0>Упс, что-то пошло не так.", "header_label": "Главная Element Call", "join_existing_call_modal": { diff --git a/locales/sk/app.json b/locales/sk/app.json index fbf2dc37..b9fb9a5c 100644 --- a/locales/sk/app.json +++ b/locales/sk/app.json @@ -23,7 +23,6 @@ }, "browser_media_e2ee_unsupported": "Váš webový prehliadač nepodporuje end-to-end šifrovanie médií. Podporované prehliadače sú Chrome, Safari, Firefox >=117", "call_ended_view": { - "body": "Boli ste odpojení z hovoru", "create_account_button": "Vytvoriť účet", "create_account_prompt": "<0>Prečo neskončiť nastavením hesla, aby ste si zachovali svoj účet? <1>Budete si môcť ponechať svoje meno a nastaviť obrázok, ktorý sa bude používať pri budúcich hovoroch", "feedback_done": "<0> Ďakujeme za vašu spätnú väzbu!", @@ -49,7 +48,10 @@ "username": "Meno používateľa" }, "disconnected_banner": "Spojenie so serverom sa stratilo.", - "full_screen_view_description": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.", + "error": { + "connection_lost_description": "Boli ste odpojení z hovoru", + "generic_description": "Odoslanie záznamov ladenia nám pomôže nájsť problém." + }, "full_screen_view_h1": "<0>Hups, niečo sa pokazilo.", "hangup_button_label": "Ukončiť hovor", "header_label": "Domov Element Call", diff --git a/locales/uk/app.json b/locales/uk/app.json index 4faa19df..a504362a 100644 --- a/locales/uk/app.json +++ b/locales/uk/app.json @@ -23,7 +23,6 @@ }, "browser_media_e2ee_unsupported": "Ваш браузер не підтримує наскрізне шифрування мультимедійних даних. Підтримувані браузери: Chrome, Safari, Firefox >=117", "call_ended_view": { - "body": "Вас від'єднано від виклику", "create_account_button": "Створити обліковий запис", "create_account_prompt": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?<1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів", "feedback_done": "<0>Дякуємо за ваш відгук!", @@ -51,7 +50,10 @@ "video": "Відео" }, "disconnected_banner": "Втрачено зв'язок з сервером.", - "full_screen_view_description": "<0>Надсилання журналів налагодження допоможе нам виявити проблему.", + "error": { + "connection_lost_description": "Вас від'єднано від виклику", + "generic_description": "Надсилання журналів налагодження допоможе нам виявити проблему." + }, "full_screen_view_h1": "<0>Йой, щось пішло не за планом.", "hangup_button_label": "Завершити виклик", "header_label": "Домівка Element Call", diff --git a/locales/vi/app.json b/locales/vi/app.json index 3bd622ce..023f2539 100644 --- a/locales/vi/app.json +++ b/locales/vi/app.json @@ -27,7 +27,9 @@ "username": "Tên người dùng", "video": "Truyền hình" }, - "full_screen_view_description": "<0>Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề.", + "error": { + "generic_description": "Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề." + }, "full_screen_view_h1": "<0>Ối, có cái gì đó sai.", "join_existing_call_modal": { "join_button": "Vâng, tham gia cuộc gọi", diff --git a/src/App.tsx b/src/App.tsx index 1ce4e8e6..18304ece 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,7 @@ import { LoginPage } from "./auth/LoginPage"; import { RegisterPage } from "./auth/RegisterPage"; import { RoomPage } from "./room/RoomPage"; import { ClientProvider } from "./ClientContext"; -import { CrashView, LoadingView } from "./FullScreenView"; +import { ErrorPage, LoadingPage } from "./FullScreenView"; import { DisconnectedBanner } from "./DisconnectedBanner"; import { Initializer } from "./initializer"; import { MediaDevicesProvider } from "./livekit/MediaDevicesContext"; @@ -61,8 +61,6 @@ export const App: FC = () => { .catch(logger.error); }); - const errorPage = ; - return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -74,7 +72,7 @@ export const App: FC = () => { - + } /> @@ -90,7 +88,7 @@ export const App: FC = () => { ) : ( - + )} diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index dbf8a3fd..d346e534 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -18,19 +18,18 @@ import { } from "react"; import { useNavigate } from "react-router-dom"; import { logger } from "matrix-js-sdk/src/logger"; -import { useTranslation } from "react-i18next"; import { type ISyncStateData, type SyncState } from "matrix-js-sdk/src/sync"; import { ClientEvent, type MatrixClient } from "matrix-js-sdk/src/client"; import type { WidgetApi } from "matrix-widget-api"; -import { ErrorView } from "./FullScreenView"; +import { ErrorPage } from "./FullScreenView"; import { widget } from "./widget"; import { PosthogAnalytics, RegistrationType, } from "./analytics/PosthogAnalytics"; -import { translatedError } from "./TranslatedError"; import { useEventTarget } from "./useEvents"; +import { OpenElsewhereError } from "./RichError"; declare global { interface Window { @@ -233,8 +232,6 @@ export const ClientProvider: FC = ({ children }) => { PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest); }, [navigate, initClientState?.client]); - const { t } = useTranslation(); - // To protect against multiple sessions writing to the same storage // simultaneously, we send a broadcast message that shuts down all other // running instances of the app. This isn't necessary if the app is running in @@ -251,8 +248,8 @@ export const ClientProvider: FC = ({ children }) => { "message", useCallback(() => { initClientState?.client.stopClient(); - setAlreadyOpenedErr(translatedError("application_opened_another_tab", t)); - }, [initClientState?.client, setAlreadyOpenedErr, t]), + setAlreadyOpenedErr(new OpenElsewhereError()); + }, [initClientState?.client, setAlreadyOpenedErr]), ); const [isDisconnected, setIsDisconnected] = useState(false); @@ -354,7 +351,7 @@ export const ClientProvider: FC = ({ children }) => { }, [initClientState, onSync]); if (alreadyOpenedErr) { - return ; + return ; } return ( diff --git a/src/ErrorView.module.css b/src/ErrorView.module.css new file mode 100644 index 00000000..14c5f141 --- /dev/null +++ b/src/ErrorView.module.css @@ -0,0 +1,21 @@ +.error { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--cpd-space-2x); + max-inline-size: 480px; +} + +.icon { + margin-block-end: var(--cpd-space-4x); +} + +.error > h1 { + margin: 0; +} + +.error > p { + font: var(--cpd-font-body-lg-regular); + color: var(--cpd-color-text-secondary); + text-align: center; +} diff --git a/src/ErrorView.tsx b/src/ErrorView.tsx new file mode 100644 index 00000000..a8c1ebe5 --- /dev/null +++ b/src/ErrorView.tsx @@ -0,0 +1,82 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { BigIcon, Button, Heading } from "@vector-im/compound-web"; +import { + useCallback, + type ComponentType, + type FC, + type ReactNode, + type SVGAttributes, +} from "react"; +import { useTranslation } from "react-i18next"; + +import { RageshakeButton } from "./settings/RageshakeButton"; +import styles from "./ErrorView.module.css"; +import { useUrlParams } from "./UrlParams"; +import { LinkButton } from "./button"; + +interface Props { + Icon: ComponentType>; + title: string; + /** + * Show an option to submit a rageshake. + * @default false + */ + rageshake?: boolean; + /** + * Whether the error is considered fatal, i.e. non-recoverable. Causes the app + * to fully reload when clicking 'return to home'. + * @default false + */ + fatal?: boolean; + children: ReactNode; +} + +export const ErrorView: FC = ({ + Icon, + title, + rageshake, + fatal, + children, +}) => { + const { t } = useTranslation(); + const { confineToRoom } = useUrlParams(); + + const onReload = useCallback(() => { + window.location.href = "/"; + }, []); + + return ( +
+ + + + + {title} + + {children} + {rageshake && ( + + )} + {!confineToRoom && + (fatal || location.pathname === "/" ? ( + + ) : ( + + {t("return_home_button")} + + ))} +
+ ); +}; diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index e88f45de..f848c021 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -5,21 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { type FC, type ReactNode, useCallback, useEffect } from "react"; -import { useLocation } from "react-router-dom"; +import { type FC, type ReactElement, type ReactNode, useEffect } from "react"; import classNames from "classnames"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import * as Sentry from "@sentry/react"; import { logger } from "matrix-js-sdk/src/logger"; -import { Button } from "@vector-im/compound-web"; +import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { Header, HeaderLogo, LeftNav, RightNav } from "./Header"; -import { LinkButton } from "./button"; import styles from "./FullScreenView.module.css"; -import { TranslatedError } from "./TranslatedError"; -import { Config } from "./config/Config"; -import { RageshakeButton } from "./settings/RageshakeButton"; import { useUrlParams } from "./UrlParams"; +import { RichError } from "./RichError"; +import { ErrorView } from "./ErrorView"; interface FullScreenViewProps { className?: string; @@ -44,74 +41,33 @@ export const FullScreenView: FC = ({ ); }; -interface ErrorViewProps { - error: Error; +interface ErrorPageProps { + error: Error | unknown; } -export const ErrorView: FC = ({ error }) => { - const location = useLocation(); - const { confineToRoom } = useUrlParams(); +// Due to this component being used as the crash fallback for Sentry, which has +// weird type requirements, we can't just give this a type of FC +export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => { const { t } = useTranslation(); - useEffect(() => { logger.error(error); Sentry.captureException(error); }, [error]); - const onReload = useCallback(() => { - window.location.href = "/"; - }, []); - return ( -

{t("common.error")}

-

- {error instanceof TranslatedError - ? error.translatedMessage - : error.message} -

- - {!confineToRoom && - (location.pathname === "/" ? ( - - ) : ( - - {t("return_home_button")} - - ))} -
- ); -}; - -export const CrashView: FC = () => { - const { t } = useTranslation(); - - const onReload = useCallback(() => { - window.location.href = "/"; - }, []); - - return ( - - -

Oops, something's gone wrong.

-
- {Config.get().rageshake?.submit_url && ( - -

Submitting debug logs will help us track down the problem.

-
+ {error instanceof RichError ? ( + error.richMessage + ) : ( + +

{t("error.generic_description")}

+
)} - - -
); }; -export const LoadingView: FC = () => { +export const LoadingPage: FC = () => { const { t } = useTranslation(); return ( diff --git a/src/RichError.tsx b/src/RichError.tsx new file mode 100644 index 00000000..effc7610 --- /dev/null +++ b/src/RichError.tsx @@ -0,0 +1,48 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { type FC, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { ErrorView } from "./ErrorView"; + +/** + * An error consisting of a terse message to be logged to the console and a + * richer message to be shown to the user, as a full-screen page. + */ +export class RichError extends Error { + public constructor( + message: string, + /** + * The pretty, more helpful message to be shown on the error screen. + */ + public readonly richMessage: ReactNode, + ) { + super(message); + } +} + +const OpenElsewhere: FC = () => { + const { t } = useTranslation(); + + return ( + +

+ {t("error.open_elsewhere_description", { + brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call", + })} +

+
+ ); +}; + +export class OpenElsewhereError extends RichError { + public constructor() { + super("App opened in another tab", ); + } +} diff --git a/src/TranslatedError.ts b/src/TranslatedError.ts index 420556be..40dd4ba1 100644 --- a/src/TranslatedError.ts +++ b/src/TranslatedError.ts @@ -9,6 +9,9 @@ import type { DefaultNamespace, ParseKeys, TFunction, TOptions } from "i18next"; /** * An error with messages in both English and the user's preferred language. + * Use this for errors that need to be displayed inline within another + * component. For errors that could be given their own screen, prefer + * {@link RichError}. */ // Abstract to force consumers to use the function below rather than calling the // constructor directly diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index 46d04552..edbc2ecf 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -26,7 +26,7 @@ import { useClientLegacy } from "../ClientContext"; import { useInteractiveRegistration } from "./useInteractiveRegistration"; import styles from "./LoginPage.module.css"; import Logo from "../icons/LogoLarge.svg?react"; -import { LoadingView } from "../FullScreenView"; +import { LoadingPage } from "../FullScreenView"; import { useRecaptcha } from "./useRecaptcha"; import { usePageTitle } from "../usePageTitle"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; @@ -148,7 +148,7 @@ export const RegisterPage: FC = () => { }, [loading, navigate, authenticated, passwordlessUser, registering]); if (loading) { - return ; + return ; } else { PosthogAnalytics.instance.eventSignup.cacheSignupStart(new Date()); } diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index e01d06e8..91044f74 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -87,7 +87,7 @@ export function ReactionPopupMenu({ {errorText} diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index 9340ecc0..f7d39d84 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next"; import { type FC } from "react"; import { useClientState } from "../ClientContext"; -import { ErrorView, LoadingView } from "../FullScreenView"; +import { ErrorPage, LoadingPage } from "../FullScreenView"; import { UnauthenticatedView } from "./UnauthenticatedView"; import { RegisteredView } from "./RegisteredView"; import { usePageTitle } from "../usePageTitle"; @@ -21,9 +21,9 @@ export const HomePage: FC = () => { const clientState = useClientState(); if (!clientState) { - return ; + return ; } else if (clientState.state === "error") { - return ; + return ; } else { return clientState.authenticated ? ( diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx index 99abfa42..8abd5e1e 100644 --- a/src/room/CallEndedView.tsx +++ b/src/room/CallEndedView.tsx @@ -15,6 +15,7 @@ import { import { type MatrixClient } from "matrix-js-sdk/src/client"; import { Trans, useTranslation } from "react-i18next"; import { Button, Heading, Text } from "@vector-im/compound-web"; +import { OfflineIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { useNavigate } from "react-router-dom"; import { logger } from "matrix-js-sdk/src/logger"; @@ -25,9 +26,9 @@ import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { FieldRow, InputField } from "../input/Input"; import { StarRatingInput } from "../input/StarRatingInput"; -import { RageshakeButton } from "../settings/RageshakeButton"; import { Link } from "../button/Link"; import { LinkButton } from "../button"; +import { ErrorView } from "../ErrorView"; interface Props { client: MatrixClient; @@ -147,25 +148,17 @@ export const CallEndedView: FC = ({ return ( <>
- - - You were disconnected from the call - - -
+ +

{t("error.connection_lost_description")}

-
- -
-
+
- {!confineToRoom && ( - - {t("return_home_button")} - - )} ); } else { diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index c18d91cd..ee1208c2 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -21,7 +21,7 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { JoinRule } from "matrix-js-sdk/src/matrix"; -import { Heading, Text } from "@vector-im/compound-web"; +import { WebBrowserIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -54,11 +54,11 @@ import { useJoinRule } from "./useJoinRule"; import { InviteModal } from "./InviteModal"; import { useUrlParams } from "../UrlParams"; import { E2eeType } from "../e2ee/e2eeType"; -import { Link } from "../button/Link"; import { useAudioContext } from "../useAudioContext"; import { callEventAudioSounds } from "./CallEventAudioRenderer"; import { useLatest } from "../useLatest"; import { usePageTitle } from "../usePageTitle"; +import { ErrorView } from "../ErrorView"; declare global { interface Window { @@ -331,9 +331,9 @@ export const GroupCallView: FC = ({ // If we have a encryption system but the browser does not support it. return ( - {t("browser_media_e2ee_unsupported_heading")} - {t("browser_media_e2ee_unsupported")} - {t("common.home")} + +

{t("error.e2ee_unsupported_description")}

+
); } diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index f7aad38d..dcebf44b 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -14,13 +14,15 @@ import { type JSX, } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { useTranslation } from "react-i18next"; -import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Trans, useTranslation } from "react-i18next"; +import { + CheckIcon, + UnknownSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { type MatrixError } from "matrix-js-sdk/src/http-api"; -import { Heading, Text } from "@vector-im/compound-web"; import { useClientLegacy } from "../ClientContext"; -import { ErrorView, FullScreenView, LoadingView } from "../FullScreenView"; +import { ErrorPage, FullScreenView, LoadingPage } from "../FullScreenView"; import { RoomAuthView } from "./RoomAuthView"; import { GroupCallView } from "./GroupCallView"; import { useRoomIdentifier, useUrlParams } from "../UrlParams"; @@ -37,6 +39,7 @@ import { useMuteStates } from "./MuteStates"; import { useOptInAnalytics } from "../settings/settings"; import { Config } from "../config/Config"; import { Link } from "../button/Link"; +import { ErrorView } from "../ErrorView"; export const RoomPage: FC = () => { const { @@ -171,29 +174,40 @@ export const RoomPage: FC = () => { if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") { return ( - {t("group_call_loader.failed_heading")} - {t("group_call_loader.failed_text")} - {/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have - dupes of this flow, let's make a common component and put it here. */} - {t("common.home")} + + +

+ That link doesn't appear to belong to any existing call. + Check that you have the right link, or{" "} + create a new one. +

+
+
); } else if (groupCallState.error instanceof CallTerminatedMessage) { return ( - {groupCallState.error.message} - {groupCallState.error.messageBody} - {groupCallState.error.reason && ( - <> - {t("group_call_loader.reason")}: - "{groupCallState.error.reason}" - - )} - {t("common.home")} + +

{groupCallState.error.messageBody}

+ {groupCallState.error.reason && ( +

+ {t("group_call_loader.reason", { + reason: groupCallState.error.reason, + })} +

+ )} +
); } else { - return ; + return ; } default: return <> ; @@ -202,9 +216,9 @@ export const RoomPage: FC = () => { let content: ReactNode; if (loading || isRegistering) { - content = ; + content = ; } else if (error) { - content = ; + content = ; } else if (!client) { content = ; } else if (!roomIdOrAlias) { diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index bd36f4e2..1336a343 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { useState, useEffect, useRef, useCallback } from "react"; +import { + useState, + useEffect, + useRef, + useCallback, + type ComponentType, + type SVGAttributes, +} from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { @@ -19,6 +26,11 @@ import { RoomEvent, type Room } from "matrix-js-sdk/src/models/room"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { JoinRule, MatrixError } from "matrix-js-sdk/src/matrix"; import { useTranslation } from "react-i18next"; +import { + AdminIcon, + CloseIcon, + EndCallIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { widget } from "../widget"; @@ -92,27 +104,25 @@ async function joinRoomAfterInvite( export class CallTerminatedMessage extends Error { /** - * @param messageBody The message explaining the kind of termination (kick, ban, knock reject, etc.) (translated) - */ - public messageBody: string; - /** - * @param reason The user provided reason for the termination (kick/ban) - */ - public reason?: string; - /** - * * @param messageTitle The title of the call ended screen message (translated) - * @param messageBody The message explaining the kind of termination (kick, ban, knock reject, etc.) (translated) - * @param reason The user provided reason for the termination (kick/ban) */ public constructor( + /** + * The icon to display with the message. + */ + public readonly icon: ComponentType>, messageTitle: string, - messageBody: string, - reason?: string, + /** + * The message explaining the kind of termination (kick, ban, knock reject, + * etc.) (translated) + */ + public readonly messageBody: string, + /** + * The user-provided reason for the termination (kick/ban) + */ + public readonly reason?: string, ) { super(messageTitle); - this.messageBody = messageBody; - this.reason = reason; } } @@ -128,6 +138,7 @@ export const useLoadGroupCall = ( const bannedError = useCallback( (): CallTerminatedMessage => new CallTerminatedMessage( + AdminIcon, t("group_call_loader.banned_heading"), t("group_call_loader.banned_body"), leaveReason(), @@ -137,6 +148,7 @@ export const useLoadGroupCall = ( const knockRejectError = useCallback( (): CallTerminatedMessage => new CallTerminatedMessage( + CloseIcon, t("group_call_loader.knock_reject_heading"), t("group_call_loader.knock_reject_body"), leaveReason(), @@ -146,6 +158,7 @@ export const useLoadGroupCall = ( const removeNoticeError = useCallback( (): CallTerminatedMessage => new CallTerminatedMessage( + EndCallIcon, t("group_call_loader.call_ended_heading"), t("group_call_loader.call_ended_body"), leaveReason(),