diff --git a/src/App.tsx b/src/App.tsx index 62b3cccb..5b74b4dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,7 +72,11 @@ export const App: FC = () => { - + ( + + )} + > } /> diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 7d7542a0..0fbff564 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -351,7 +351,7 @@ export const ClientProvider: FC = ({ children }) => { }, [initClientState, onSync]); if (alreadyOpenedErr) { - return ; + return ; } return ( diff --git a/src/ErrorView.tsx b/src/ErrorView.tsx index da6165ca..8da84ba9 100644 --- a/src/ErrorView.tsx +++ b/src/ErrorView.tsx @@ -12,13 +12,16 @@ import { type FC, type ReactNode, type SVGAttributes, + type ReactElement, } from "react"; import { useTranslation } from "react-i18next"; +import { logger } from "matrix-js-sdk/src/logger"; import { RageshakeButton } from "./settings/RageshakeButton"; import styles from "./ErrorView.module.css"; import { useUrlParams } from "./UrlParams"; import { LinkButton } from "./button"; +import { ElementWidgetActions, type WidgetHelpers } from "./widget.ts"; interface Props { Icon: ComponentType>; @@ -35,6 +38,7 @@ interface Props { */ fatal?: boolean; children: ReactNode; + widget: WidgetHelpers | null; } export const ErrorView: FC = ({ @@ -43,6 +47,7 @@ export const ErrorView: FC = ({ rageshake, fatal, children, + widget, }) => { const { t } = useTranslation(); const { confineToRoom } = useUrlParams(); @@ -51,6 +56,46 @@ export const ErrorView: FC = ({ window.location.href = "/"; }, []); + const CloseWidgetButton: FC<{ widget: WidgetHelpers }> = ({ + widget, + }): ReactElement => { + // in widget mode we don't want to show the return home button but a close button + const closeWidget = (): void => { + widget.api.transport + .send(ElementWidgetActions.Close, {}) + .catch((e) => { + // What to do here? + logger.error("Failed to send close action", e); + }) + .finally(() => { + widget.api.transport.stop(); + }); + }; + return ( + + ); + }; + + // Whether the error is considered fatal or pathname is `/` then reload the all app. + // If not then navigate to home page. + const ReturnToHomeButton = (): ReactElement => { + if (fatal || location.pathname === "/") { + return ( + + ); + } else { + return ( + + {t("return_home_button")} + + ); + } + }; + return (
@@ -63,20 +108,11 @@ export const ErrorView: FC = ({ {rageshake && ( )} - {!confineToRoom && - (fatal || location.pathname === "/" ? ( - - ) : ( - - {t("return_home_button")} - - ))} + {widget ? ( + + ) : ( + !confineToRoom && + )}
); }; diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index fb2d4609..c8655229 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -17,6 +17,7 @@ import styles from "./FullScreenView.module.css"; import { useUrlParams } from "./UrlParams"; import { RichError } from "./RichError"; import { ErrorView } from "./ErrorView"; +import { type WidgetHelpers } from "./widget.ts"; interface FullScreenViewProps { className?: string; @@ -47,11 +48,12 @@ export const FullScreenView: FC = ({ interface ErrorPageProps { error: Error | unknown; + widget: WidgetHelpers | null; } // 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 => { +export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => { const { t } = useTranslation(); useEffect(() => { logger.error(error); @@ -63,7 +65,13 @@ export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => { {error instanceof RichError ? ( error.richMessage ) : ( - +

{t("error.generic_description")}

)} diff --git a/src/RichError.tsx b/src/RichError.tsx index abacf0b3..699486e2 100644 --- a/src/RichError.tsx +++ b/src/RichError.tsx @@ -10,6 +10,7 @@ import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import type { FC, ReactNode } from "react"; import { ErrorView } from "./ErrorView"; +import { widget } from "./widget.ts"; /** * An error consisting of a terse message to be logged to the console and a @@ -31,7 +32,11 @@ const OpenElsewhere: FC = () => { const { t } = useTranslation(); return ( - +

{t("error.open_elsewhere_description", { brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call", diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index e6136855..ca1f0ea8 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -13,6 +13,7 @@ import { ErrorPage, LoadingPage } from "../FullScreenView"; import { UnauthenticatedView } from "./UnauthenticatedView"; import { RegisteredView } from "./RegisteredView"; import { usePageTitle } from "../usePageTitle"; +import { widget } from "../widget.ts"; export const HomePage: FC = () => { const { t } = useTranslation(); @@ -23,7 +24,7 @@ export const HomePage: FC = () => { if (!clientState) { return ; } else if (clientState.state === "error") { - return ; + return ; } else { return clientState.authenticated ? ( diff --git a/src/livekit/useECConnectionState.test.tsx b/src/livekit/useECConnectionState.test.tsx index 0b13e6ce..287ee4dc 100644 --- a/src/livekit/useECConnectionState.test.tsx +++ b/src/livekit/useECConnectionState.test.tsx @@ -60,7 +60,7 @@ test.each<[string, ConnectionError]>([ const user = userEvent.setup(); render( - + , diff --git a/src/room/GroupCallErrorBoundary.test.tsx b/src/room/GroupCallErrorBoundary.test.tsx index 145b606e..f2a10bc2 100644 --- a/src/room/GroupCallErrorBoundary.test.tsx +++ b/src/room/GroupCallErrorBoundary.test.tsx @@ -30,6 +30,7 @@ import { UnknownCallError, } from "../utils/errors.ts"; import { mockConfig } from "../utils/test.ts"; +import { ElementWidgetActions, type WidgetHelpers } from "../widget.ts"; test.each([ { @@ -66,6 +67,7 @@ test.each([ @@ -94,6 +96,7 @@ test("should render the error page with link back to home", async () => { @@ -138,7 +141,10 @@ test("ConnectionLostError: Action handling should reset error state", async () = return ( - + @@ -180,6 +186,7 @@ describe("Rageshake button", () => { @@ -203,3 +210,44 @@ describe("Rageshake button", () => { ).not.toBeInTheDocument(); }); }); + +test("should have a close button in widget mode", async () => { + const error = new MatrixRTCFocusMissingError("example.com"); + const TestComponent = (): ReactNode => { + throw error; + }; + + const mockWidget = { + api: { + transport: { send: vi.fn().mockResolvedValue(undefined), stop: vi.fn() }, + }, + } as unknown as WidgetHelpers; + + const user = userEvent.setup(); + const onErrorMock = vi.fn(); + const { asFragment } = render( + + + + + , + ); + + await screen.findByText("Call is not supported"); + + await screen.findByRole("button", { name: "Close" }); + + expect(asFragment()).toMatchSnapshot(); + + await user.click(screen.getByRole("button", { name: "Close" })); + + expect(mockWidget.api.transport.send).toHaveBeenCalledWith( + ElementWidgetActions.Close, + expect.anything(), + ); + expect(mockWidget.api.transport.stop).toHaveBeenCalled(); +}); diff --git a/src/room/GroupCallErrorBoundary.tsx b/src/room/GroupCallErrorBoundary.tsx index 71a676ed..170718d6 100644 --- a/src/room/GroupCallErrorBoundary.tsx +++ b/src/room/GroupCallErrorBoundary.tsx @@ -31,6 +31,7 @@ import { } from "../utils/errors.ts"; import { FullScreenView } from "../FullScreenView.tsx"; import { ErrorView } from "../ErrorView.tsx"; +import { type WidgetHelpers } from "../widget.ts"; export type CallErrorRecoveryAction = "reconnect"; // | "retry" ; @@ -40,11 +41,13 @@ interface ErrorPageProps { error: ElementCallError; recoveryActionHandler: RecoveryActionHandler; resetError: () => void; + widget: WidgetHelpers | null; } const ErrorPage: FC = ({ error, recoveryActionHandler, + widget, }: ErrorPageProps): ReactElement => { const { t } = useTranslation(); @@ -77,6 +80,7 @@ const ErrorPage: FC = ({ Icon={icon} title={error.localisedTitle} rageshake={error.code == ErrorCode.UNKNOWN_ERROR} + widget={widget} >

{error.localisedMessage ?? ( @@ -102,12 +106,14 @@ interface BoundaryProps { children: ReactNode | (() => ReactNode); recoveryActionHandler: RecoveryActionHandler; onError?: (error: unknown) => void; + widget: WidgetHelpers | null; } export const GroupCallErrorBoundary = ({ recoveryActionHandler, onError, children, + widget, }: BoundaryProps): ReactElement => { const fallbackRenderer: FallbackRender = useCallback( ({ error, resetError }): ReactElement => { @@ -117,6 +123,7 @@ export const GroupCallErrorBoundary = ({ : new UnknownCallError(error instanceof Error ? error : new Error()); return ( { @@ -126,7 +133,7 @@ export const GroupCallErrorBoundary = ({ /> ); }, - [recoveryActionHandler], + [recoveryActionHandler, widget], ); return ( diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 47cbbc87..9ebd7e3b 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -479,6 +479,7 @@ export const GroupCallView: FC = ({ return ( { if (action == "reconnect") { setLeft(false); diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 59e56aac..e96790c5 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -182,6 +182,7 @@ export const RoomPage: FC = () => {

@@ -199,6 +200,7 @@ export const RoomPage: FC = () => {

{groupCallState.error.messageBody}

{groupCallState.error.reason && ( @@ -212,7 +214,7 @@ export const RoomPage: FC = () => { ); } else { - return ; + return ; } default: return <> ; @@ -223,7 +225,7 @@ export const RoomPage: FC = () => { if (loading || isRegistering) { content = ; } else if (error) { - content = ; + content = ; } else if (!client) { content = ; } else if (!roomIdOrAlias) { diff --git a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap index c261b635..5aab22a2 100644 --- a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap +++ b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap @@ -150,6 +150,158 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = ` `; +exports[`should have a close button in widget mode 1`] = ` + +
+
+ +
+
+
+
+
+ + + + +
+

+ Call is not supported +

+

+ The server is not configured to work with Element Call. Please contact your server admin (Domain: example.com, Error Code: MISSING_MATRIX_RTC_FOCUS). +

+ +
+
+
+
+
+`; + exports[`should render the error page with link back to home 1`] = `