From b83d34fad0ae5ec9c1b4c6cbdb1e7c1f53a7e1e5 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 15 Jan 2025 20:24:38 -0500 Subject: [PATCH] Make error screens more visually consistent This change gives our error screens a more consistent visual appearance and lets us easily present any new errors that we come up with in the future with custom React components. This will make it easy to, for example, throw an error if the JWT service is unreachable, present that error in non-technical language, while also letting admins dig deeper into the details on the error screen. --- locales/en/app.json | 24 +++++---- src/App.tsx | 8 ++- src/ClientContext.tsx | 13 ++--- src/ErrorView.module.css | 21 ++++++++ src/ErrorView.tsx | 82 +++++++++++++++++++++++++++++ src/FullScreenView.tsx | 78 ++++++--------------------- src/RichError.tsx | 48 +++++++++++++++++ src/TranslatedError.ts | 3 ++ src/auth/RegisterPage.tsx | 4 +- src/button/ReactionToggleButton.tsx | 2 +- src/home/HomePage.tsx | 6 +-- src/room/CallEndedView.tsx | 25 ++++----- src/room/GroupCallView.tsx | 10 ++-- src/room/RoomPage.tsx | 56 ++++++++++++-------- src/room/useLoadGroupCall.ts | 45 ++++++++++------ 15 files changed, 276 insertions(+), 149 deletions(-) create mode 100644 src/ErrorView.module.css create mode 100644 src/ErrorView.tsx create mode 100644 src/RichError.tsx diff --git a/locales/en/app.json b/locales/en/app.json index f35c35793..b72af0d54 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/src/App.tsx b/src/App.tsx index 1ce4e8e68..18304ecee 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 dbf8a3fdc..d346e5341 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 000000000..14c5f1417 --- /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 000000000..a8c1ebe5b --- /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 e88f45de4..f848c0218 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 000000000..effc76107 --- /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 420556be3..40dd4ba19 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 46d04552b..edbc2ecf4 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 e01d06e89..91044f746 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 9340ecc0e..f7d39d84f 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 99abfa42c..8abd5e1e8 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 c18d91cd2..ee1208c2e 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 f7aad38d0..dcebf44b4 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 bd36f4e21..1336a3431 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(),