Skip to content

Commit

Permalink
Make error screens more visually consistent
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
robintown committed Jan 16, 2025
1 parent 0f2e67d commit b83d34f
Show file tree
Hide file tree
Showing 15 changed files with 276 additions and 149 deletions.
24 changes: 13 additions & 11 deletions locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>",
"feedback_done": "<0>Thanks for your feedback!</0>",
Expand All @@ -50,7 +46,6 @@
"back": "Back",
"display_name": "Display name",
"encrypted": "Encrypted",
"error": "Error",
"home": "Home",
"loading": "Loading…",
"next": "Next",
Expand All @@ -61,7 +56,6 @@
"reaction": "Reaction",
"reactions": "Reactions",
"settings": "Settings",
"something_went_wrong": "Something went wrong",
"unencrypted": "Not encrypted",
"username": "Username",
"video": "Video"
Expand All @@ -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.</0>",
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
"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</1>.</0>",
"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",
Expand Down
8 changes: 3 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -61,8 +61,6 @@ export const App: FC = () => {
.catch(logger.error);
});

const errorPage = <CrashView />;

return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand All @@ -74,7 +72,7 @@ export const App: FC = () => {
<Suspense fallback={null}>
<ClientProvider>
<MediaDevicesProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<Sentry.ErrorBoundary fallback={ErrorPage}>
<DisconnectedBanner />
<Routes>
<SentryRoute path="/" element={<HomePage />} />
Expand All @@ -90,7 +88,7 @@ export const App: FC = () => {
</ClientProvider>
</Suspense>
) : (
<LoadingView />
<LoadingPage />
)}
</TooltipProvider>
</ThemeProvider>
Expand Down
13 changes: 5 additions & 8 deletions src/ClientContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -233,8 +232,6 @@ export const ClientProvider: FC<Props> = ({ 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
Expand All @@ -251,8 +248,8 @@ export const ClientProvider: FC<Props> = ({ 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);
Expand Down Expand Up @@ -354,7 +351,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}, [initClientState, onSync]);

if (alreadyOpenedErr) {
return <ErrorView error={alreadyOpenedErr} />;
return <ErrorPage error={alreadyOpenedErr} />;
}

return (
Expand Down
21 changes: 21 additions & 0 deletions src/ErrorView.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
82 changes: 82 additions & 0 deletions src/ErrorView.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGAttributes<SVGElement>>;
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<Props> = ({
Icon,
title,
rageshake,
fatal,
children,
}) => {
const { t } = useTranslation();
const { confineToRoom } = useUrlParams();

const onReload = useCallback(() => {
window.location.href = "/";
}, []);

return (
<div className={styles.error}>
<BigIcon className={styles.icon}>
<Icon />
</BigIcon>
<Heading as="h1" weight="semibold" size="md">
{title}
</Heading>
{children}
{rageshake && (
<RageshakeButton description={`***Error View***: ${title}`} />
)}
{!confineToRoom &&
(fatal || location.pathname === "/" ? (
<Button
kind="tertiary"
className={styles.homeLink}
onClick={onReload}
>
{t("return_home_button")}
</Button>
) : (
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
{t("return_home_button")}
</LinkButton>
))}
</div>
);
};
78 changes: 17 additions & 61 deletions src/FullScreenView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,74 +41,33 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
);
};

interface ErrorViewProps {
error: Error;
interface ErrorPageProps {
error: Error | unknown;
}

export const ErrorView: FC<ErrorViewProps> = ({ 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<ErrorPageProps>
export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => {
const { t } = useTranslation();

useEffect(() => {
logger.error(error);
Sentry.captureException(error);
}, [error]);

const onReload = useCallback(() => {
window.location.href = "/";
}, []);

return (
<FullScreenView>
<h1>{t("common.error")}</h1>
<p>
{error instanceof TranslatedError
? error.translatedMessage
: error.message}
</p>
<RageshakeButton description={`***Error View***: ${error.message}`} />
{!confineToRoom &&
(location.pathname === "/" ? (
<Button className={styles.homeLink} onClick={onReload}>
{t("return_home_button")}
</Button>
) : (
<LinkButton className={styles.homeLink} to="/">
{t("return_home_button")}
</LinkButton>
))}
</FullScreenView>
);
};

export const CrashView: FC = () => {
const { t } = useTranslation();

const onReload = useCallback(() => {
window.location.href = "/";
}, []);

return (
<FullScreenView>
<Trans i18nKey="full_screen_view_h1">
<h1>Oops, something's gone wrong.</h1>
</Trans>
{Config.get().rageshake?.submit_url && (
<Trans i18nKey="full_screen_view_description">
<p>Submitting debug logs will help us track down the problem.</p>
</Trans>
{error instanceof RichError ? (
error.richMessage
) : (
<ErrorView Icon={ErrorIcon} title={t("error.generic")} rageshake fatal>
<p>{t("error.generic_description")}</p>
</ErrorView>
)}

<RageshakeButton description="***Soft Crash***" />
<Button className={styles.wideButton} onClick={onReload}>
{t("return_home_button")}
</Button>
</FullScreenView>
);
};

export const LoadingView: FC = () => {
export const LoadingPage: FC = () => {
const { t } = useTranslation();

return (
Expand Down
Loading

0 comments on commit b83d34f

Please sign in to comment.