Skip to content

Commit 90a282b

Browse files
authored
Merge pull request #3063 from element-hq/valere/fix_add_close_button_in_widget_mode
Error: use a close button instead of go to home when in widget mode
2 parents 79e2947 + b02ad88 commit 90a282b

12 files changed

+289
-25
lines changed

src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ export const App: FC = () => {
7272
<Suspense fallback={null}>
7373
<ClientProvider>
7474
<MediaDevicesProvider>
75-
<Sentry.ErrorBoundary fallback={ErrorPage}>
75+
<Sentry.ErrorBoundary
76+
fallback={(error) => (
77+
<ErrorPage error={error} widget={widget} />
78+
)}
79+
>
7680
<DisconnectedBanner />
7781
<Routes>
7882
<SentryRoute path="/" element={<HomePage />} />

src/ClientContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
351351
}, [initClientState, onSync]);
352352

353353
if (alreadyOpenedErr) {
354-
return <ErrorPage error={alreadyOpenedErr} />;
354+
return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
355355
}
356356

357357
return (

src/ErrorView.tsx

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import {
1212
type FC,
1313
type ReactNode,
1414
type SVGAttributes,
15+
type ReactElement,
1516
} from "react";
1617
import { useTranslation } from "react-i18next";
18+
import { logger } from "matrix-js-sdk/src/logger";
1719

1820
import { RageshakeButton } from "./settings/RageshakeButton";
1921
import styles from "./ErrorView.module.css";
2022
import { useUrlParams } from "./UrlParams";
2123
import { LinkButton } from "./button";
24+
import { ElementWidgetActions, type WidgetHelpers } from "./widget.ts";
2225

2326
interface Props {
2427
Icon: ComponentType<SVGAttributes<SVGElement>>;
@@ -35,6 +38,7 @@ interface Props {
3538
*/
3639
fatal?: boolean;
3740
children: ReactNode;
41+
widget: WidgetHelpers | null;
3842
}
3943

4044
export const ErrorView: FC<Props> = ({
@@ -43,6 +47,7 @@ export const ErrorView: FC<Props> = ({
4347
rageshake,
4448
fatal,
4549
children,
50+
widget,
4651
}) => {
4752
const { t } = useTranslation();
4853
const { confineToRoom } = useUrlParams();
@@ -51,6 +56,46 @@ export const ErrorView: FC<Props> = ({
5156
window.location.href = "/";
5257
}, []);
5358

59+
const CloseWidgetButton: FC<{ widget: WidgetHelpers }> = ({
60+
widget,
61+
}): ReactElement => {
62+
// in widget mode we don't want to show the return home button but a close button
63+
const closeWidget = (): void => {
64+
widget.api.transport
65+
.send(ElementWidgetActions.Close, {})
66+
.catch((e) => {
67+
// What to do here?
68+
logger.error("Failed to send close action", e);
69+
})
70+
.finally(() => {
71+
widget.api.transport.stop();
72+
});
73+
};
74+
return (
75+
<Button kind="primary" onClick={closeWidget}>
76+
{t("action.close")}
77+
</Button>
78+
);
79+
};
80+
81+
// Whether the error is considered fatal or pathname is `/` then reload the all app.
82+
// If not then navigate to home page.
83+
const ReturnToHomeButton = (): ReactElement => {
84+
if (fatal || location.pathname === "/") {
85+
return (
86+
<Button kind="tertiary" className={styles.homeLink} onClick={onReload}>
87+
{t("return_home_button")}
88+
</Button>
89+
);
90+
} else {
91+
return (
92+
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
93+
{t("return_home_button")}
94+
</LinkButton>
95+
);
96+
}
97+
};
98+
5499
return (
55100
<div className={styles.error}>
56101
<BigIcon className={styles.icon}>
@@ -63,20 +108,11 @@ export const ErrorView: FC<Props> = ({
63108
{rageshake && (
64109
<RageshakeButton description={`***Error View***: ${title}`} />
65110
)}
66-
{!confineToRoom &&
67-
(fatal || location.pathname === "/" ? (
68-
<Button
69-
kind="tertiary"
70-
className={styles.homeLink}
71-
onClick={onReload}
72-
>
73-
{t("return_home_button")}
74-
</Button>
75-
) : (
76-
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
77-
{t("return_home_button")}
78-
</LinkButton>
79-
))}
111+
{widget ? (
112+
<CloseWidgetButton widget={widget} />
113+
) : (
114+
!confineToRoom && <ReturnToHomeButton />
115+
)}
80116
</div>
81117
);
82118
};

src/FullScreenView.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import styles from "./FullScreenView.module.css";
1717
import { useUrlParams } from "./UrlParams";
1818
import { RichError } from "./RichError";
1919
import { ErrorView } from "./ErrorView";
20+
import { type WidgetHelpers } from "./widget.ts";
2021

2122
interface FullScreenViewProps {
2223
className?: string;
@@ -47,11 +48,12 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
4748

4849
interface ErrorPageProps {
4950
error: Error | unknown;
51+
widget: WidgetHelpers | null;
5052
}
5153

5254
// Due to this component being used as the crash fallback for Sentry, which has
5355
// weird type requirements, we can't just give this a type of FC<ErrorPageProps>
54-
export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => {
56+
export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => {
5557
const { t } = useTranslation();
5658
useEffect(() => {
5759
logger.error(error);
@@ -63,7 +65,13 @@ export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => {
6365
{error instanceof RichError ? (
6466
error.richMessage
6567
) : (
66-
<ErrorView Icon={ErrorIcon} title={t("error.generic")} rageshake fatal>
68+
<ErrorView
69+
widget={widget}
70+
Icon={ErrorIcon}
71+
title={t("error.generic")}
72+
rageshake
73+
fatal
74+
>
6775
<p>{t("error.generic_description")}</p>
6876
</ErrorView>
6977
)}

src/RichError.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
1010

1111
import type { FC, ReactNode } from "react";
1212
import { ErrorView } from "./ErrorView";
13+
import { widget } from "./widget.ts";
1314

1415
/**
1516
* An error consisting of a terse message to be logged to the console and a
@@ -31,7 +32,11 @@ const OpenElsewhere: FC = () => {
3132
const { t } = useTranslation();
3233

3334
return (
34-
<ErrorView Icon={PopOutIcon} title={t("error.open_elsewhere")}>
35+
<ErrorView
36+
widget={widget}
37+
Icon={PopOutIcon}
38+
title={t("error.open_elsewhere")}
39+
>
3540
<p>
3641
{t("error.open_elsewhere_description", {
3742
brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call",

src/home/HomePage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ErrorPage, LoadingPage } from "../FullScreenView";
1313
import { UnauthenticatedView } from "./UnauthenticatedView";
1414
import { RegisteredView } from "./RegisteredView";
1515
import { usePageTitle } from "../usePageTitle";
16+
import { widget } from "../widget.ts";
1617

1718
export const HomePage: FC = () => {
1819
const { t } = useTranslation();
@@ -23,7 +24,7 @@ export const HomePage: FC = () => {
2324
if (!clientState) {
2425
return <LoadingPage />;
2526
} else if (clientState.state === "error") {
26-
return <ErrorPage error={clientState.error} />;
27+
return <ErrorPage widget={widget} error={clientState.error} />;
2728
} else {
2829
return clientState.authenticated ? (
2930
<RegisteredView client={clientState.authenticated.client} />

src/livekit/useECConnectionState.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ test.each<[string, ConnectionError]>([
6060
const user = userEvent.setup();
6161
render(
6262
<MemoryRouter>
63-
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()}>
63+
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()} widget={null}>
6464
<TestComponent />
6565
</GroupCallErrorBoundary>
6666
</MemoryRouter>,

src/room/GroupCallErrorBoundary.test.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
UnknownCallError,
3131
} from "../utils/errors.ts";
3232
import { mockConfig } from "../utils/test.ts";
33+
import { ElementWidgetActions, type WidgetHelpers } from "../widget.ts";
3334

3435
test.each([
3536
{
@@ -66,6 +67,7 @@ test.each([
6667
<GroupCallErrorBoundary
6768
onError={onErrorMock}
6869
recoveryActionHandler={vi.fn()}
70+
widget={null}
6971
>
7072
<TestComponent />
7173
</GroupCallErrorBoundary>
@@ -94,6 +96,7 @@ test("should render the error page with link back to home", async () => {
9496
<GroupCallErrorBoundary
9597
onError={onErrorMock}
9698
recoveryActionHandler={vi.fn()}
99+
widget={null}
97100
>
98101
<TestComponent />
99102
</GroupCallErrorBoundary>
@@ -138,7 +141,10 @@ test("ConnectionLostError: Action handling should reset error state", async () =
138141

139142
return (
140143
<BrowserRouter>
141-
<GroupCallErrorBoundary recoveryActionHandler={reconnectCallback}>
144+
<GroupCallErrorBoundary
145+
recoveryActionHandler={reconnectCallback}
146+
widget={null}
147+
>
142148
<TestComponent fail={failState} />
143149
</GroupCallErrorBoundary>
144150
</BrowserRouter>
@@ -180,6 +186,7 @@ describe("Rageshake button", () => {
180186
<GroupCallErrorBoundary
181187
onError={vi.fn()}
182188
recoveryActionHandler={vi.fn()}
189+
widget={null}
183190
>
184191
<TestComponent />
185192
</GroupCallErrorBoundary>
@@ -203,3 +210,44 @@ describe("Rageshake button", () => {
203210
).not.toBeInTheDocument();
204211
});
205212
});
213+
214+
test("should have a close button in widget mode", async () => {
215+
const error = new MatrixRTCFocusMissingError("example.com");
216+
const TestComponent = (): ReactNode => {
217+
throw error;
218+
};
219+
220+
const mockWidget = {
221+
api: {
222+
transport: { send: vi.fn().mockResolvedValue(undefined), stop: vi.fn() },
223+
},
224+
} as unknown as WidgetHelpers;
225+
226+
const user = userEvent.setup();
227+
const onErrorMock = vi.fn();
228+
const { asFragment } = render(
229+
<BrowserRouter>
230+
<GroupCallErrorBoundary
231+
widget={mockWidget}
232+
onError={onErrorMock}
233+
recoveryActionHandler={vi.fn()}
234+
>
235+
<TestComponent />
236+
</GroupCallErrorBoundary>
237+
</BrowserRouter>,
238+
);
239+
240+
await screen.findByText("Call is not supported");
241+
242+
await screen.findByRole("button", { name: "Close" });
243+
244+
expect(asFragment()).toMatchSnapshot();
245+
246+
await user.click(screen.getByRole("button", { name: "Close" }));
247+
248+
expect(mockWidget.api.transport.send).toHaveBeenCalledWith(
249+
ElementWidgetActions.Close,
250+
expect.anything(),
251+
);
252+
expect(mockWidget.api.transport.stop).toHaveBeenCalled();
253+
});

src/room/GroupCallErrorBoundary.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from "../utils/errors.ts";
3232
import { FullScreenView } from "../FullScreenView.tsx";
3333
import { ErrorView } from "../ErrorView.tsx";
34+
import { type WidgetHelpers } from "../widget.ts";
3435

3536
export type CallErrorRecoveryAction = "reconnect"; // | "retry" ;
3637

@@ -40,11 +41,13 @@ interface ErrorPageProps {
4041
error: ElementCallError;
4142
recoveryActionHandler: RecoveryActionHandler;
4243
resetError: () => void;
44+
widget: WidgetHelpers | null;
4345
}
4446

4547
const ErrorPage: FC<ErrorPageProps> = ({
4648
error,
4749
recoveryActionHandler,
50+
widget,
4851
}: ErrorPageProps): ReactElement => {
4952
const { t } = useTranslation();
5053

@@ -77,6 +80,7 @@ const ErrorPage: FC<ErrorPageProps> = ({
7780
Icon={icon}
7881
title={error.localisedTitle}
7982
rageshake={error.code == ErrorCode.UNKNOWN_ERROR}
83+
widget={widget}
8084
>
8185
<p>
8286
{error.localisedMessage ?? (
@@ -102,12 +106,14 @@ interface BoundaryProps {
102106
children: ReactNode | (() => ReactNode);
103107
recoveryActionHandler: RecoveryActionHandler;
104108
onError?: (error: unknown) => void;
109+
widget: WidgetHelpers | null;
105110
}
106111

107112
export const GroupCallErrorBoundary = ({
108113
recoveryActionHandler,
109114
onError,
110115
children,
116+
widget,
111117
}: BoundaryProps): ReactElement => {
112118
const fallbackRenderer: FallbackRender = useCallback(
113119
({ error, resetError }): ReactElement => {
@@ -117,6 +123,7 @@ export const GroupCallErrorBoundary = ({
117123
: new UnknownCallError(error instanceof Error ? error : new Error());
118124
return (
119125
<ErrorPage
126+
widget={widget ?? null}
120127
error={callError}
121128
resetError={resetError}
122129
recoveryActionHandler={(action: CallErrorRecoveryAction) => {
@@ -126,7 +133,7 @@ export const GroupCallErrorBoundary = ({
126133
/>
127134
);
128135
},
129-
[recoveryActionHandler],
136+
[recoveryActionHandler, widget],
130137
);
131138

132139
return (

src/room/GroupCallView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ export const GroupCallView: FC<Props> = ({
479479

480480
return (
481481
<GroupCallErrorBoundary
482+
widget={widget}
482483
recoveryActionHandler={(action) => {
483484
if (action == "reconnect") {
484485
setLeft(false);

0 commit comments

Comments
 (0)