Files
element-call-Github/src/room/GroupCallErrorBoundary.tsx
T
Valere 8e78ac76bd Drop ErrorView's widget prop in favour of the host bridge
ErrorView needed a widget only to decide whether to offer a close button
or a link home. That prop was threaded down through ErrorPage, RichError
and GroupCallErrorBoundary from seven call sites, to answer the single
question of whether the host can dismiss Element Call — which the host
bridge now answers directly through the presence of close().

Read the bridge from context in ErrorView and remove the prop, along with
the plumbing that carried it.

No functional change: the rendered output is unchanged, as the existing
snapshot confirms.

Move GroupCallView onto the host bridge

GroupCallView asked the widget API to keep it on screen, to close it, and
to tell it when a preloaded call should join. Route all three through the
host bridge and drop the widget prop.
2026-09-02 13:03:23 +02:00

182 lines
4.9 KiB
TypeScript

/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { ErrorBoundary, type FallbackRender } from "@sentry/react";
import {
type ComponentType,
type FC,
type ReactElement,
type ReactNode,
type SVGAttributes,
useCallback,
} from "react";
import { Trans, useTranslation } from "react-i18next";
import {
ErrorSolidIcon,
HostIcon,
OfflineIcon,
WebBrowserIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { Button } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/lib/logger";
import { MatrixError } from "matrix-js-sdk";
import {
ConnectionLostError,
ElementCallError,
ErrorCategory,
ErrorCode,
UnknownCallError,
} from "../utils/errors.ts";
import { FullScreenView } from "../FullScreenView.tsx";
import { ErrorView } from "../ErrorView.tsx";
import styles from "../ErrorView.module.css";
export type CallErrorRecoveryAction = "reconnect"; // | "retry" ;
export type RecoveryActionHandler = (
action: CallErrorRecoveryAction,
) => Promise<void>;
interface ErrorPageProps {
error: ElementCallError;
recoveryActionHandler: RecoveryActionHandler;
resetError: () => void;
}
const ErrorPage: FC<ErrorPageProps> = ({
error,
recoveryActionHandler,
}: ErrorPageProps): ReactElement => {
const { t } = useTranslation();
logger.error("Error boundary caught:", error);
let icon: ComponentType<SVGAttributes<SVGElement>>;
switch (error.category) {
case ErrorCategory.CONFIGURATION_ISSUE:
icon = HostIcon;
break;
case ErrorCategory.NETWORK_CONNECTIVITY:
icon = OfflineIcon;
break;
case ErrorCategory.CLIENT_CONFIGURATION:
icon = WebBrowserIcon;
break;
default:
icon = ErrorSolidIcon;
}
const actions: { label: string; onClick: () => void }[] = [];
if (error instanceof ConnectionLostError) {
actions.push({
label: t("call_ended_view.reconnect_button"),
onClick: () => void recoveryActionHandler("reconnect"),
});
}
const technicalError =
error.cause instanceof MatrixError ? error.cause : null;
return (
<FullScreenView>
<ErrorView
Icon={icon}
title={error.localisedTitle}
rageshake={error.code == ErrorCode.UNKNOWN_ERROR}
>
<p>
{error.localisedMessageKey ? (
<Trans
// @ts-expect-error - Dynamic i18nKey from error object
i18nKey={error.localisedMessageKey}
values={error.localisedMessageValues}
components={[
<a
href={String(error.localisedMessageValues?.linkUrl || "#")}
target="_blank"
rel="noopener noreferrer"
>
{/* Content injected by Trans component */}
</a>,
<b />,
<code />,
]}
/>
) : error.localisedMessage ? (
error.localisedMessage
) : (
<Trans
i18nKey="error.unexpected_ec_error"
components={[<b />, <code />]}
values={{ errorCode: error.code }}
/>
)}
</p>
{technicalError ? (
<details className={styles.technicalDetails}>
<summary className={styles.technicalDetailsSummary}>
{t("technical_details")}
</summary>
<pre className={styles.technicalDetailsPre}>
{technicalError.message}
</pre>
</details>
) : null}
{actions &&
actions.map((action, index) => (
<Button
kind="secondary"
onClick={action.onClick}
key={`action${index}`}
>
{action.label}
</Button>
))}
</ErrorView>
</FullScreenView>
);
};
interface BoundaryProps {
children: ReactNode | (() => ReactNode);
recoveryActionHandler: RecoveryActionHandler;
onError?: (error: unknown) => void;
}
export const GroupCallErrorBoundary = ({
recoveryActionHandler,
onError,
children,
}: BoundaryProps): ReactElement => {
const fallbackRenderer: FallbackRender = useCallback(
({ error, resetError }): ReactElement => {
const callError =
error instanceof ElementCallError
? error
: new UnknownCallError(error instanceof Error ? error : new Error());
return (
<ErrorPage
error={callError}
resetError={resetError}
recoveryActionHandler={async (action: CallErrorRecoveryAction) => {
await recoveryActionHandler(action);
resetError();
}}
/>
);
},
[recoveryActionHandler],
);
return (
<ErrorBoundary
fallback={fallbackRenderer}
onError={(error) => onError?.(error)}
children={children}
/>
);
};