mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
12 Commits
robin/butt
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2df8fae95 | ||
|
|
8c3e4509d7 | ||
|
|
b8c77b1528 | ||
|
|
b24378d652 | ||
|
|
b788b0e9ad | ||
|
|
f555e96b4b | ||
|
|
67f9af2f76 | ||
|
|
7176718ade | ||
|
|
d532f478ef | ||
|
|
ca3f1d994d | ||
|
|
1a17879562 | ||
|
|
67b2015fa5 |
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ inputs.artifact_run_id }}
|
||||
name: build-output
|
||||
name: build-output-full
|
||||
path: dist
|
||||
|
||||
- name: Log in to container registry
|
||||
|
||||
2
.github/workflows/build-element-call.yaml
vendored
2
.github/workflows/build-element-call.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
with:
|
||||
name: build-output
|
||||
name: build-output-full
|
||||
path: dist
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
2
.github/workflows/deploy-to-netlify.yaml
vendored
2
.github/workflows/deploy-to-netlify.yaml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ inputs.artifact_run_id }}
|
||||
name: build-output
|
||||
name: build-output-full
|
||||
path: webapp
|
||||
|
||||
- name: Add redirects file
|
||||
|
||||
2
.github/workflows/publish.yaml
vendored
2
.github/workflows/publish.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
name: build-output
|
||||
name: build-output-full
|
||||
path: dist
|
||||
- name: Create Tarball
|
||||
env:
|
||||
|
||||
@@ -88,10 +88,10 @@
|
||||
"i18next-parser": "^9.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"knip": "^5.27.2",
|
||||
"livekit-client": "^2.5.7",
|
||||
"livekit-client": "2.9.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#0cdb07544d9926cf0855a76ca5cc7dab253bdb24",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8395919f0fd1af7cab1e793d736f2cdf18ef7686",
|
||||
"matrix-widget-api": "1.11.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
|
||||
@@ -72,7 +72,11 @@ export const App: FC = () => {
|
||||
<Suspense fallback={null}>
|
||||
<ClientProvider>
|
||||
<MediaDevicesProvider>
|
||||
<Sentry.ErrorBoundary fallback={ErrorPage}>
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={(error) => (
|
||||
<ErrorPage error={error} widget={widget} />
|
||||
)}
|
||||
>
|
||||
<DisconnectedBanner />
|
||||
<Routes>
|
||||
<SentryRoute path="/" element={<HomePage />} />
|
||||
|
||||
@@ -351,7 +351,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
}, [initClientState, onSync]);
|
||||
|
||||
if (alreadyOpenedErr) {
|
||||
return <ErrorPage error={alreadyOpenedErr} />;
|
||||
return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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<SVGAttributes<SVGElement>>;
|
||||
@@ -35,6 +38,7 @@ interface Props {
|
||||
*/
|
||||
fatal?: boolean;
|
||||
children: ReactNode;
|
||||
widget: WidgetHelpers | null;
|
||||
}
|
||||
|
||||
export const ErrorView: FC<Props> = ({
|
||||
@@ -43,6 +47,7 @@ export const ErrorView: FC<Props> = ({
|
||||
rageshake,
|
||||
fatal,
|
||||
children,
|
||||
widget,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { confineToRoom } = useUrlParams();
|
||||
@@ -51,6 +56,46 @@ export const ErrorView: FC<Props> = ({
|
||||
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 (
|
||||
<Button kind="primary" onClick={closeWidget}>
|
||||
{t("action.close")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<Button kind="tertiary" className={styles.homeLink} onClick={onReload}>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<BigIcon className={styles.icon}>
|
||||
@@ -63,20 +108,11 @@ export const ErrorView: FC<Props> = ({
|
||||
{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>
|
||||
))}
|
||||
{widget ? (
|
||||
<CloseWidgetButton widget={widget} />
|
||||
) : (
|
||||
!confineToRoom && <ReturnToHomeButton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<FullScreenViewProps> = ({
|
||||
|
||||
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<ErrorPageProps>
|
||||
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
|
||||
) : (
|
||||
<ErrorView Icon={ErrorIcon} title={t("error.generic")} rageshake fatal>
|
||||
<ErrorView
|
||||
widget={widget}
|
||||
Icon={ErrorIcon}
|
||||
title={t("error.generic")}
|
||||
rageshake
|
||||
fatal
|
||||
>
|
||||
<p>{t("error.generic_description")}</p>
|
||||
</ErrorView>
|
||||
)}
|
||||
|
||||
@@ -5,16 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
ErrorIcon,
|
||||
HostIcon,
|
||||
PopOutIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import type { ComponentType, FC, ReactNode, SVGAttributes } from "react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { ErrorView } from "./ErrorView";
|
||||
import { type ElementCallError, ErrorCategory } from "./utils/errors.ts";
|
||||
import { widget } from "./widget.ts";
|
||||
|
||||
/**
|
||||
* An error consisting of a terse message to be logged to the console and a
|
||||
@@ -36,7 +32,11 @@ const OpenElsewhere: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ErrorView Icon={PopOutIcon} title={t("error.open_elsewhere")}>
|
||||
<ErrorView
|
||||
widget={widget}
|
||||
Icon={PopOutIcon}
|
||||
title={t("error.open_elsewhere")}
|
||||
>
|
||||
<p>
|
||||
{t("error.open_elsewhere_description", {
|
||||
brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call",
|
||||
@@ -51,62 +51,3 @@ export class OpenElsewhereError extends RichError {
|
||||
super("App opened in another tab", <OpenElsewhere />);
|
||||
}
|
||||
}
|
||||
|
||||
const InsufficientCapacity: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ErrorView Icon={HostIcon} title={t("error.insufficient_capacity")}>
|
||||
<p>{t("error.insufficient_capacity_description")}</p>
|
||||
</ErrorView>
|
||||
);
|
||||
};
|
||||
|
||||
export class InsufficientCapacityError extends RichError {
|
||||
public constructor() {
|
||||
super("Insufficient server capacity", <InsufficientCapacity />);
|
||||
}
|
||||
}
|
||||
|
||||
type ECErrorProps = {
|
||||
error: ElementCallError;
|
||||
};
|
||||
|
||||
const GenericECError: FC<{ error: ElementCallError }> = ({
|
||||
error,
|
||||
}: ECErrorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let title: string;
|
||||
let icon: ComponentType<SVGAttributes<SVGElement>>;
|
||||
switch (error.category) {
|
||||
case ErrorCategory.CONFIGURATION_ISSUE:
|
||||
title = t("error.call_is_not_supported");
|
||||
icon = HostIcon;
|
||||
break;
|
||||
default:
|
||||
title = t("error.generic");
|
||||
icon = ErrorIcon;
|
||||
}
|
||||
return (
|
||||
<ErrorView Icon={icon} title={title}>
|
||||
<p>
|
||||
{error.localisedMessage ?? (
|
||||
<Trans
|
||||
i18nKey="error.unexpected_ec_error"
|
||||
components={[<b />, <code />]}
|
||||
values={{ errorCode: error.code }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</ErrorView>
|
||||
);
|
||||
};
|
||||
|
||||
export class ElementCallRichError extends RichError {
|
||||
public ecError: ElementCallError;
|
||||
public constructor(ecError: ElementCallError) {
|
||||
super(ecError.message, <GenericECError error={ecError} />);
|
||||
this.ecError = ecError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <LoadingPage />;
|
||||
} else if (clientState.state === "error") {
|
||||
return <ErrorPage error={clientState.error} />;
|
||||
return <ErrorPage widget={widget} error={clientState.error} />;
|
||||
} else {
|
||||
return clientState.authenticated ? (
|
||||
<RegisteredView client={clientState.authenticated.client} />
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type FC, useCallback, useState } from "react";
|
||||
import { test } from "vitest";
|
||||
import { test, vi } from "vitest";
|
||||
import {
|
||||
ConnectionError,
|
||||
ConnectionErrorReason,
|
||||
@@ -14,12 +14,11 @@ import {
|
||||
} from "livekit-client";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
import { ErrorPage } from "../FullScreenView";
|
||||
import { useECConnectionState } from "./useECConnectionState";
|
||||
import { type SFUConfig } from "./openIDSFU";
|
||||
import { GroupCallErrorBoundary } from "../room/GroupCallErrorBoundary.tsx";
|
||||
|
||||
test.each<[string, ConnectionError]>([
|
||||
[
|
||||
@@ -61,9 +60,9 @@ test.each<[string, ConnectionError]>([
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ErrorBoundary fallback={ErrorPage}>
|
||||
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()} widget={null}>
|
||||
<TestComponent />
|
||||
</ErrorBoundary>
|
||||
</GroupCallErrorBoundary>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Connect" }));
|
||||
|
||||
@@ -20,7 +20,11 @@ import * as Sentry from "@sentry/react";
|
||||
|
||||
import { type SFUConfig, sfuConfigEquals } from "./openIDSFU";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { InsufficientCapacityError, RichError } from "../RichError";
|
||||
import {
|
||||
ElementCallError,
|
||||
InsufficientCapacityError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -188,7 +192,7 @@ export function useECConnectionState(
|
||||
|
||||
const [isSwitchingFocus, setSwitchingFocus] = useState(false);
|
||||
const [isInDoConnect, setIsInDoConnect] = useState(false);
|
||||
const [error, setError] = useState<RichError | null>(null);
|
||||
const [error, setError] = useState<ElementCallError | null>(null);
|
||||
if (error !== null) throw error;
|
||||
|
||||
const onConnStateChanged = useCallback((state: ConnectionState) => {
|
||||
@@ -271,9 +275,11 @@ export function useECConnectionState(
|
||||
initialAudioOptions,
|
||||
)
|
||||
.catch((e) => {
|
||||
if (e instanceof RichError)
|
||||
if (e instanceof ElementCallError) {
|
||||
setError(e); // Bubble up any error screens to React
|
||||
else logger.error("Failed to connect to SFU", e);
|
||||
} else if (e instanceof Error) {
|
||||
setError(new UnknownCallError(e));
|
||||
} else logger.error("Failed to connect to SFU", e);
|
||||
})
|
||||
.finally(() => setIsInDoConnect(false));
|
||||
}
|
||||
|
||||
253
src/room/GroupCallErrorBoundary.test.tsx
Normal file
253
src/room/GroupCallErrorBoundary.test.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
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 { describe, expect, test, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import {
|
||||
type FC,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
type CallErrorRecoveryAction,
|
||||
GroupCallErrorBoundary,
|
||||
} from "./GroupCallErrorBoundary.tsx";
|
||||
import {
|
||||
ConnectionLostError,
|
||||
E2EENotSupportedError,
|
||||
type ElementCallError,
|
||||
InsufficientCapacityError,
|
||||
MatrixRTCFocusMissingError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { mockConfig } from "../utils/test.ts";
|
||||
import { ElementWidgetActions, type WidgetHelpers } from "../widget.ts";
|
||||
|
||||
test.each([
|
||||
{
|
||||
error: new MatrixRTCFocusMissingError("example.com"),
|
||||
expectedTitle: "Call is not supported",
|
||||
},
|
||||
{
|
||||
error: new ConnectionLostError(),
|
||||
expectedTitle: "Connection lost",
|
||||
expectedDescription: "You were disconnected from the call.",
|
||||
},
|
||||
{
|
||||
error: new E2EENotSupportedError(),
|
||||
expectedTitle: "Incompatible browser",
|
||||
expectedDescription:
|
||||
"Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.",
|
||||
},
|
||||
{
|
||||
error: new InsufficientCapacityError(),
|
||||
expectedTitle: "Insufficient capacity",
|
||||
expectedDescription:
|
||||
"The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.",
|
||||
},
|
||||
])(
|
||||
"should report correct error for $expectedTitle",
|
||||
async ({ error, expectedTitle, expectedDescription }) => {
|
||||
const TestComponent = (): ReactNode => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
const onErrorMock = vi.fn();
|
||||
const { asFragment } = render(
|
||||
<BrowserRouter>
|
||||
<GroupCallErrorBoundary
|
||||
onError={onErrorMock}
|
||||
recoveryActionHandler={vi.fn()}
|
||||
widget={null}
|
||||
>
|
||||
<TestComponent />
|
||||
</GroupCallErrorBoundary>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
await screen.findByText(expectedTitle);
|
||||
if (expectedDescription) {
|
||||
expect(screen.queryByText(expectedDescription)).toBeInTheDocument();
|
||||
}
|
||||
expect(onErrorMock).toHaveBeenCalledWith(error);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
test("should render the error page with link back to home", async () => {
|
||||
const error = new MatrixRTCFocusMissingError("example.com");
|
||||
const TestComponent = (): ReactNode => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
const onErrorMock = vi.fn();
|
||||
const { asFragment } = render(
|
||||
<BrowserRouter>
|
||||
<GroupCallErrorBoundary
|
||||
onError={onErrorMock}
|
||||
recoveryActionHandler={vi.fn()}
|
||||
widget={null}
|
||||
>
|
||||
<TestComponent />
|
||||
</GroupCallErrorBoundary>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
await screen.findByText("Call is not supported");
|
||||
expect(screen.getByText(/Domain: example\.com/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Error Code: MISSING_MATRIX_RTC_FOCUS/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await screen.findByRole("button", { name: "Return to home screen" });
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledOnce();
|
||||
expect(onErrorMock).toHaveBeenCalledWith(error);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("ConnectionLostError: Action handling should reset error state", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const TestComponent: FC<{ fail: boolean }> = ({ fail }): ReactNode => {
|
||||
if (fail) {
|
||||
throw new ConnectionLostError();
|
||||
}
|
||||
return <div>HELLO</div>;
|
||||
};
|
||||
|
||||
const reconnectCallbackSpy = vi.fn();
|
||||
|
||||
const WrapComponent = (): ReactNode => {
|
||||
const [failState, setFailState] = useState(true);
|
||||
const reconnectCallback = useCallback(
|
||||
(action: CallErrorRecoveryAction) => {
|
||||
reconnectCallbackSpy(action);
|
||||
setFailState(false);
|
||||
},
|
||||
[setFailState],
|
||||
);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GroupCallErrorBoundary
|
||||
recoveryActionHandler={reconnectCallback}
|
||||
widget={null}
|
||||
>
|
||||
<TestComponent fail={failState} />
|
||||
</GroupCallErrorBoundary>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const { asFragment } = render(<WrapComponent />);
|
||||
|
||||
// Should fail first
|
||||
await screen.findByText("Connection lost");
|
||||
await screen.findByRole("button", { name: "Reconnect" });
|
||||
await screen.findByRole("button", { name: "Return to home screen" });
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Reconnect" }));
|
||||
|
||||
// reconnect should have reset the error, thus rendering should be ok
|
||||
await screen.findByText("HELLO");
|
||||
|
||||
expect(reconnectCallbackSpy).toHaveBeenCalledOnce();
|
||||
expect(reconnectCallbackSpy).toHaveBeenCalledWith("reconnect");
|
||||
});
|
||||
|
||||
describe("Rageshake button", () => {
|
||||
function setupTest(testError: ElementCallError): void {
|
||||
mockConfig({
|
||||
rageshake: {
|
||||
submit_url: "https://rageshake.example.com.localhost",
|
||||
},
|
||||
});
|
||||
|
||||
const TestComponent = (): ReactElement => {
|
||||
throw testError;
|
||||
};
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<GroupCallErrorBoundary
|
||||
onError={vi.fn()}
|
||||
recoveryActionHandler={vi.fn()}
|
||||
widget={null}
|
||||
>
|
||||
<TestComponent />
|
||||
</GroupCallErrorBoundary>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
test("should show send rageshake button for unknown errors", () => {
|
||||
setupTest(new UnknownCallError(new Error("FOO")));
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Send debug logs" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should not show send rageshake button for call errors", () => {
|
||||
setupTest(new E2EENotSupportedError());
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Send debug logs" }),
|
||||
).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(
|
||||
<BrowserRouter>
|
||||
<GroupCallErrorBoundary
|
||||
widget={mockWidget}
|
||||
onError={onErrorMock}
|
||||
recoveryActionHandler={vi.fn()}
|
||||
>
|
||||
<TestComponent />
|
||||
</GroupCallErrorBoundary>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
146
src/room/GroupCallErrorBoundary.tsx
Normal file
146
src/room/GroupCallErrorBoundary.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
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 {
|
||||
ErrorIcon,
|
||||
HostIcon,
|
||||
OfflineIcon,
|
||||
WebBrowserIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import {
|
||||
ConnectionLostError,
|
||||
ElementCallError,
|
||||
ErrorCategory,
|
||||
ErrorCode,
|
||||
UnknownCallError,
|
||||
} 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" ;
|
||||
|
||||
export type RecoveryActionHandler = (action: CallErrorRecoveryAction) => void;
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: ElementCallError;
|
||||
recoveryActionHandler: RecoveryActionHandler;
|
||||
resetError: () => void;
|
||||
widget: WidgetHelpers | null;
|
||||
}
|
||||
|
||||
const ErrorPage: FC<ErrorPageProps> = ({
|
||||
error,
|
||||
recoveryActionHandler,
|
||||
widget,
|
||||
}: ErrorPageProps): ReactElement => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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 = ErrorIcon;
|
||||
}
|
||||
|
||||
const actions: { label: string; onClick: () => void }[] = [];
|
||||
if (error instanceof ConnectionLostError) {
|
||||
actions.push({
|
||||
label: t("call_ended_view.reconnect_button"),
|
||||
onClick: () => recoveryActionHandler("reconnect"),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<FullScreenView>
|
||||
<ErrorView
|
||||
Icon={icon}
|
||||
title={error.localisedTitle}
|
||||
rageshake={error.code == ErrorCode.UNKNOWN_ERROR}
|
||||
widget={widget}
|
||||
>
|
||||
<p>
|
||||
{error.localisedMessage ?? (
|
||||
<Trans
|
||||
i18nKey="error.unexpected_ec_error"
|
||||
components={[<b />, <code />]}
|
||||
values={{ errorCode: error.code }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
{actions &&
|
||||
actions.map((action, index) => (
|
||||
<button onClick={action.onClick} key={`action${index}`}>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</ErrorView>
|
||||
</FullScreenView>
|
||||
);
|
||||
};
|
||||
|
||||
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 => {
|
||||
const callError =
|
||||
error instanceof ElementCallError
|
||||
? error
|
||||
: new UnknownCallError(error instanceof Error ? error : new Error());
|
||||
return (
|
||||
<ErrorPage
|
||||
widget={widget ?? null}
|
||||
error={callError}
|
||||
resetError={resetError}
|
||||
recoveryActionHandler={(action: CallErrorRecoveryAction) => {
|
||||
resetError();
|
||||
recoveryActionHandler(action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[recoveryActionHandler, widget],
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={fallbackRenderer}
|
||||
onError={(error) => onError?.(error)}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import {
|
||||
type FC,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -25,14 +24,7 @@ import {
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
OfflineIcon,
|
||||
WebBrowserIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import {
|
||||
@@ -40,7 +32,6 @@ import {
|
||||
type JoinCallData,
|
||||
type WidgetHelpers,
|
||||
} from "../widget";
|
||||
import { ErrorPage, FullScreenView } from "../FullScreenView";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { type MatrixInfo } from "./VideoPreview";
|
||||
import { CallEndedView } from "./CallEndedView";
|
||||
@@ -63,15 +54,14 @@ import { useAudioContext } from "../useAudioContext";
|
||||
import { callEventAudioSounds } from "./CallEventAudioRenderer";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { ErrorView } from "../ErrorView";
|
||||
import {
|
||||
ConnectionLostError,
|
||||
E2EENotSupportedError,
|
||||
ElementCallError,
|
||||
ErrorCategory,
|
||||
ErrorCode,
|
||||
RTCSessionError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { ElementCallRichError } from "../RichError.tsx";
|
||||
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
|
||||
import {
|
||||
useNewMembershipManagerSetting as useNewMembershipManagerSetting,
|
||||
useSetting,
|
||||
@@ -84,11 +74,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface GroupCallErrorPageProps {
|
||||
error: Error | unknown;
|
||||
resetError: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
@@ -205,10 +190,8 @@ export const GroupCallView: FC<Props> = ({
|
||||
setError(e);
|
||||
} else {
|
||||
logger.error(`Unknown Error while entering RTC session`, e);
|
||||
const error = new ElementCallError(
|
||||
e instanceof Error ? e.message : "Unknown error",
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
ErrorCategory.UNKNOWN,
|
||||
const error = new UnknownCallError(
|
||||
e instanceof Error ? e : new Error("Unknown error", { cause: e }),
|
||||
);
|
||||
setError(error);
|
||||
}
|
||||
@@ -398,58 +381,9 @@ export const GroupCallView: FC<Props> = ({
|
||||
);
|
||||
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const errorPage = useMemo(() => {
|
||||
function GroupCallErrorPage({
|
||||
error,
|
||||
resetError,
|
||||
}: GroupCallErrorPageProps): ReactElement {
|
||||
useEffect(() => {
|
||||
if (rtcSession.isJoined()) onLeave("error");
|
||||
}, [error]);
|
||||
|
||||
const onReconnect = useCallback(() => {
|
||||
setLeft(false);
|
||||
resetError();
|
||||
enterRTCSessionOrError(
|
||||
rtcSession,
|
||||
perParticipantE2EE,
|
||||
useNewMembershipManager,
|
||||
).catch((e) => {
|
||||
logger.error("Error re-entering RTC session on reconnect", e);
|
||||
});
|
||||
}, [resetError]);
|
||||
|
||||
return error instanceof ConnectionLostError ? (
|
||||
<FullScreenView>
|
||||
<ErrorView
|
||||
Icon={OfflineIcon}
|
||||
title={t("error.connection_lost")}
|
||||
rageshake
|
||||
>
|
||||
<p>{t("error.connection_lost_description")}</p>
|
||||
<Button onClick={onReconnect}>
|
||||
{t("call_ended_view.reconnect_button")}
|
||||
</Button>
|
||||
</ErrorView>
|
||||
</FullScreenView>
|
||||
) : (
|
||||
<ErrorPage error={error} />
|
||||
);
|
||||
}
|
||||
return GroupCallErrorPage;
|
||||
}, [t, rtcSession, onLeave, perParticipantE2EE, useNewMembershipManager]);
|
||||
|
||||
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
|
||||
// If we have a encryption system but the browser does not support it.
|
||||
return (
|
||||
<FullScreenView>
|
||||
<ErrorView Icon={WebBrowserIcon} title={t("error.e2ee_unsupported")}>
|
||||
<p>{t("error.e2ee_unsupported_description")}</p>
|
||||
</ErrorView>
|
||||
</FullScreenView>
|
||||
);
|
||||
throw new E2EENotSupportedError();
|
||||
}
|
||||
|
||||
const shareModal = (
|
||||
@@ -484,9 +418,9 @@ export const GroupCallView: FC<Props> = ({
|
||||
let body: ReactNode;
|
||||
if (error) {
|
||||
// If an ElementCallError was recorded, then create a component that will fail to render and throw
|
||||
// an ElementCallRichError error. This will then be handled by the ErrorBoundary component.
|
||||
// the error. This will then be handled by the ErrorBoundary component.
|
||||
const ErrorComponent = (): ReactNode => {
|
||||
throw new ElementCallRichError(error);
|
||||
throw error;
|
||||
};
|
||||
body = <ErrorComponent />;
|
||||
} else if (isJoined) {
|
||||
@@ -543,5 +477,28 @@ export const GroupCallView: FC<Props> = ({
|
||||
body = lobbyView;
|
||||
}
|
||||
|
||||
return <ErrorBoundary fallback={errorPage}>{body}</ErrorBoundary>;
|
||||
return (
|
||||
<GroupCallErrorBoundary
|
||||
widget={widget}
|
||||
recoveryActionHandler={(action) => {
|
||||
if (action == "reconnect") {
|
||||
setLeft(false);
|
||||
enterRTCSessionOrError(
|
||||
rtcSession,
|
||||
perParticipantE2EE,
|
||||
useNewMembershipManager,
|
||||
).catch((e) => {
|
||||
logger.error("Error re-entering RTC session", e);
|
||||
});
|
||||
}
|
||||
}}
|
||||
onError={
|
||||
(/**error*/) => {
|
||||
if (rtcSession.isJoined()) onLeave("error");
|
||||
}
|
||||
}
|
||||
>
|
||||
{body}
|
||||
</GroupCallErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -182,6 +182,7 @@ export const RoomPage: FC = () => {
|
||||
<ErrorView
|
||||
Icon={UnknownSolidIcon}
|
||||
title={t("error.call_not_found")}
|
||||
widget={widget}
|
||||
>
|
||||
<Trans i18nKey="error.call_not_found_description">
|
||||
<p>
|
||||
@@ -199,6 +200,7 @@ export const RoomPage: FC = () => {
|
||||
<ErrorView
|
||||
Icon={groupCallState.error.icon}
|
||||
title={groupCallState.error.message}
|
||||
widget={widget}
|
||||
>
|
||||
<p>{groupCallState.error.messageBody}</p>
|
||||
{groupCallState.error.reason && (
|
||||
@@ -212,7 +214,7 @@ export const RoomPage: FC = () => {
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return <ErrorPage error={groupCallState.error} />;
|
||||
return <ErrorPage widget={widget} error={groupCallState.error} />;
|
||||
}
|
||||
default:
|
||||
return <> </>;
|
||||
@@ -223,7 +225,7 @@ export const RoomPage: FC = () => {
|
||||
if (loading || isRegistering) {
|
||||
content = <LoadingPage />;
|
||||
} else if (error) {
|
||||
content = <ErrorPage error={error} />;
|
||||
content = <ErrorPage widget={widget} error={error} />;
|
||||
} else if (!client) {
|
||||
content = <RoomAuthView />;
|
||||
} else if (!roomIdOrAlias) {
|
||||
|
||||
1051
src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap
Normal file
1051
src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load Diff
40
src/utils/displayname-integration.test.ts
Normal file
40
src/utils/displayname-integration.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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 { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { shouldDisambiguate } from "./displayname";
|
||||
import { alice } from "./test-fixtures";
|
||||
import { mockMatrixRoom } from "./test";
|
||||
|
||||
// Ideally these tests would be in ./displayname.test.ts but I can't figure out how to
|
||||
// just spy on the removeHiddenChars() function without impacting the other tests.
|
||||
// So, these tests are in this separate test file.
|
||||
vi.mock("matrix-js-sdk/src/utils");
|
||||
|
||||
describe("shouldDisambiguate", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
let jsUtils: typeof import("matrix-js-sdk/src/utils");
|
||||
|
||||
beforeAll(async () => {
|
||||
jsUtils = await import("matrix-js-sdk/src/utils");
|
||||
vi.spyOn(jsUtils, "removeHiddenChars").mockImplementation((str) => str);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should only call removeHiddenChars once for a single displayname", () => {
|
||||
const room = mockMatrixRoom({});
|
||||
shouldDisambiguate(alice, [], room);
|
||||
expect(jsUtils.removeHiddenChars).toHaveBeenCalledTimes(1);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
shouldDisambiguate(alice, [], room);
|
||||
}
|
||||
expect(jsUtils.removeHiddenChars).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -7,12 +7,36 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import {
|
||||
removeDirectionOverrideChars,
|
||||
removeHiddenChars,
|
||||
removeHiddenChars as removeHiddenCharsUncached,
|
||||
} from "matrix-js-sdk/src/utils";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { CallMembership } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
// Calling removeHiddenChars() can be slow on Safari, so we cache the results.
|
||||
// To illustrate a simple benchmark:
|
||||
// Chrome: 10,000 calls took 2.599ms
|
||||
// Safari: 10,000 calls took 242ms
|
||||
// See: https://github.com/element-hq/element-call/issues/3065
|
||||
|
||||
const removeHiddenCharsCache = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Calls removeHiddenCharsUncached and caches the result
|
||||
*/
|
||||
function removeHiddenChars(str: string): string {
|
||||
if (removeHiddenCharsCache.has(str)) {
|
||||
return removeHiddenCharsCache.get(str)!;
|
||||
}
|
||||
const result = removeHiddenCharsUncached(str);
|
||||
// this is naive but should be good enough for our purposes
|
||||
if (removeHiddenCharsCache.size > 500) {
|
||||
removeHiddenCharsCache.clear();
|
||||
}
|
||||
removeHiddenCharsCache.set(str, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/matrix-org/matrix-js-sdk/blob/f10deb5ef2e8f061ff005af0476034382ea128ca/src/models/room-member.ts#L409
|
||||
export function shouldDisambiguate(
|
||||
member: { rawDisplayName?: string; userId: string },
|
||||
|
||||
@@ -14,6 +14,9 @@ export enum ErrorCode {
|
||||
MISSING_MATRIX_RTC_FOCUS = "MISSING_MATRIX_RTC_FOCUS",
|
||||
CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR",
|
||||
MEMBERSHIP_MANAGER_UNRECOVERABLE = "MEMBERSHIP_MANAGER_UNRECOVERABLE",
|
||||
/** LiveKit indicates that the server has hit its track limits */
|
||||
INSUFFICIENT_CAPACITY_ERROR = "INSUFFICIENT_CAPACITY_ERROR",
|
||||
E2EE_NOT_SUPPORTED = "E2EE_NOT_SUPPORTED",
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
||||
}
|
||||
|
||||
@@ -22,6 +25,7 @@ export enum ErrorCategory {
|
||||
CONFIGURATION_ISSUE = "CONFIGURATION_ISSUE",
|
||||
NETWORK_CONNECTIVITY = "NETWORK_CONNECTIVITY",
|
||||
RTC_SESSION_FAILURE = "RTC_SESSION_FAILURE",
|
||||
CLIENT_CONFIGURATION = "CLIENT_CONFIGURATION",
|
||||
UNKNOWN = "UNKNOWN",
|
||||
// SYSTEM_FAILURE / FEDERATION_FAILURE ..
|
||||
}
|
||||
@@ -33,14 +37,17 @@ export class ElementCallError extends Error {
|
||||
public code: ErrorCode;
|
||||
public category: ErrorCategory;
|
||||
public localisedMessage?: string;
|
||||
public localisedTitle: string;
|
||||
|
||||
public constructor(
|
||||
name: string,
|
||||
protected constructor(
|
||||
localisedTitle: string,
|
||||
code: ErrorCode,
|
||||
category: ErrorCategory,
|
||||
localisedMessage?: string,
|
||||
localisedMessage: string,
|
||||
cause?: Error,
|
||||
) {
|
||||
super(name);
|
||||
super(localisedTitle, { cause });
|
||||
this.localisedTitle = localisedTitle;
|
||||
this.localisedMessage = localisedMessage;
|
||||
this.category = category;
|
||||
this.code = code;
|
||||
@@ -52,7 +59,7 @@ export class MatrixRTCFocusMissingError extends ElementCallError {
|
||||
|
||||
public constructor(domain: string) {
|
||||
super(
|
||||
"MatrixRTCFocusMissingError",
|
||||
t("error.call_is_not_supported"),
|
||||
ErrorCode.MISSING_MATRIX_RTC_FOCUS,
|
||||
ErrorCategory.CONFIGURATION_ISSUE,
|
||||
t("error.matrix_rtc_focus_missing", {
|
||||
@@ -68,9 +75,10 @@ export class MatrixRTCFocusMissingError extends ElementCallError {
|
||||
export class ConnectionLostError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
"Connection lost",
|
||||
t("error.connection_lost"),
|
||||
ErrorCode.CONNECTION_LOST_ERROR,
|
||||
ErrorCategory.NETWORK_CONNECTIVITY,
|
||||
t("error.connection_lost_description"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -80,3 +88,38 @@ export class RTCSessionError extends ElementCallError {
|
||||
super("RTCSession Error", code, ErrorCategory.RTC_SESSION_FAILURE, message);
|
||||
}
|
||||
}
|
||||
|
||||
export class E2EENotSupportedError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
t("error.e2ee_unsupported"),
|
||||
ErrorCode.E2EE_NOT_SUPPORTED,
|
||||
ErrorCategory.CLIENT_CONFIGURATION,
|
||||
t("error.e2ee_unsupported_description"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnknownCallError extends ElementCallError {
|
||||
public constructor(error: Error) {
|
||||
super(
|
||||
t("error.generic"),
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
ErrorCategory.UNKNOWN,
|
||||
error.message,
|
||||
// Properly set it as a cause for a better reporting on sentry
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class InsufficientCapacityError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
t("error.insufficient_capacity"),
|
||||
ErrorCode.INSUFFICIENT_CAPACITY_ERROR,
|
||||
ErrorCategory.UNKNOWN,
|
||||
t("error.insufficient_capacity_description"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"noEmitOnError": true,
|
||||
"experimentalDecorators": true,
|
||||
"experimentalDecorators": false,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
26
yarn.lock
26
yarn.lock
@@ -6181,20 +6181,20 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
livekit-client@^2.5.7:
|
||||
version "2.9.5"
|
||||
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.9.5.tgz#b9bcce5972ccf194ee36eabbf8d93b20dd672b19"
|
||||
integrity sha512-2EJmiB4XItaRjTEmL4XxGzsahLYTer9T5N6lKyhBHQxwH4GrjBWewPySvJEO8zCpD2nvWZCmCQjIJx0+w+y6DA==
|
||||
livekit-client@2.9.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.9.1.tgz#7286d845dc3a063354a92caf5821d4e9d7584de0"
|
||||
integrity sha512-MqBxnOmkOB88+n2Whq2pgXVwevfJ+yhERNf0tZf/7RG1ZpCWF+pk7/muRVzZeCxal4fwGOB3Z3MIT0KBJoT0RQ==
|
||||
dependencies:
|
||||
"@livekit/mutex" "1.1.1"
|
||||
"@livekit/protocol" "1.33.0"
|
||||
events "^3.3.0"
|
||||
loglevel "^1.9.2"
|
||||
sdp-transform "^2.15.0"
|
||||
loglevel "^1.8.0"
|
||||
sdp-transform "^2.14.1"
|
||||
ts-debounce "^4.0.0"
|
||||
tslib "2.8.1"
|
||||
typed-emitter "^2.1.0"
|
||||
webrtc-adapter "^9.0.1"
|
||||
webrtc-adapter "^9.0.0"
|
||||
|
||||
locate-path@^5.0.0:
|
||||
version "5.0.0"
|
||||
@@ -6235,7 +6235,7 @@ loglevel@1.9.1:
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7"
|
||||
integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==
|
||||
|
||||
loglevel@^1.7.1, loglevel@^1.9.1, loglevel@^1.9.2:
|
||||
loglevel@^1.7.1, loglevel@^1.8.0, loglevel@^1.9.1:
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08"
|
||||
integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==
|
||||
@@ -6334,9 +6334,9 @@ matrix-events-sdk@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#0cdb07544d9926cf0855a76ca5cc7dab253bdb24":
|
||||
version "37.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0cdb07544d9926cf0855a76ca5cc7dab253bdb24"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8395919f0fd1af7cab1e793d736f2cdf18ef7686":
|
||||
version "37.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8395919f0fd1af7cab1e793d736f2cdf18ef7686"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1"
|
||||
@@ -7710,7 +7710,7 @@ scheduler@^0.23.2:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
sdp-transform@^2.14.1, sdp-transform@^2.15.0:
|
||||
sdp-transform@^2.14.1:
|
||||
version "2.15.0"
|
||||
resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.15.0.tgz#79d37a2481916f36a0534e07b32ceaa87f71df42"
|
||||
integrity sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==
|
||||
@@ -8758,7 +8758,7 @@ webpack-virtual-modules@^0.6.2:
|
||||
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
||||
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
|
||||
|
||||
webrtc-adapter@^9.0.1:
|
||||
webrtc-adapter@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz#d4efa22ca9604cb2c8cdb9e492815ba37acfa0b2"
|
||||
integrity sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==
|
||||
|
||||
Reference in New Issue
Block a user