diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 10c55b5b..ad884865 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -81,6 +81,7 @@ vi.mock("../rtcSessionHelpers", async (importOriginal) => { // TODO: perhaps there is a more elegant way to manage the type import here? // eslint-disable-next-line @typescript-eslint/consistent-type-imports const orig = await importOriginal(); + // TODO: leaveRTCSession no longer exists! Tests need adapting. return { ...orig, enterRTCSession, leaveRTCSession }; }); diff --git a/src/room/checkForParallelCalls.test.ts b/src/room/checkForParallelCalls.test.ts deleted file mode 100644 index 2d14c79a..00000000 --- a/src/room/checkForParallelCalls.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2023, 2024 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 { vi, type Mocked, test, expect } from "vitest"; -import { type RoomState } from "matrix-js-sdk"; - -import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics"; -import { checkForParallelCalls } from "../../src/room/checkForParallelCalls"; -import { withFakeTimers } from "../utils/test"; - -const withMockedPosthog = ( - continuation: (posthog: Mocked) => void, -): void => { - const posthog = vi.mocked({ - trackEvent: vi.fn(), - } as unknown as PosthogAnalytics); - const instanceSpy = vi - .spyOn(PosthogAnalytics, "instance", "get") - .mockReturnValue(posthog); - try { - continuation(posthog); - } finally { - instanceSpy.mockRestore(); - } -}; - -const mockRoomState = ( - groupCallMemberContents: Record[], -): RoomState => { - const stateEvents = groupCallMemberContents.map((content) => ({ - getContent: (): Record => content, - })); - return { getStateEvents: () => stateEvents } as unknown as RoomState; -}; - -test("checkForParallelCalls does nothing if all participants are in the same call", () => { - withFakeTimers(() => { - withMockedPosthog((posthog) => { - const roomState = mockRoomState([ - { - "m.calls": [ - { - "m.call_id": "1", - "m.devices": [ - { - device_id: "Element Call", - session_id: "a", - expires_ts: Date.now() + 1000, - }, - ], - }, - { - "m.call_id": null, // invalid - "m.devices": [ - { - device_id: "Element Android", - session_id: "a", - expires_ts: Date.now() + 1000, - }, - ], - }, - null, // invalid - ], - }, - { - "m.calls": [ - { - "m.call_id": "1", - "m.devices": [ - { - device_id: "Element Desktop", - session_id: "a", - expires_ts: Date.now() + 1000, - }, - ], - }, - ], - }, - ]); - - checkForParallelCalls(roomState); - expect(posthog.trackEvent).not.toHaveBeenCalled(); - }); - }); -}); - -test("checkForParallelCalls sends diagnostics to PostHog if there is a split-brain", () => { - withFakeTimers(() => { - withMockedPosthog((posthog) => { - const roomState = mockRoomState([ - { - "m.calls": [ - { - "m.call_id": "1", - "m.devices": [ - { - device_id: "Element Call", - session_id: "a", - expires_ts: Date.now() + 1000, - }, - ], - }, - { - "m.call_id": "2", - "m.devices": [ - { - device_id: "Element Android", - session_id: "a", - expires_ts: Date.now() + 1000, - }, - ], - }, - ], - }, - { - "m.calls": [ - { - "m.call_id": "1", - "m.devices": [ - { - device_id: "Element Desktop", - session_id: "a", - expires_ts: Date.now() + 1000, - }, - ], - }, - { - "m.call_id": "2", - "m.devices": [ - { - device_id: "Element Call", - session_id: "a", - expires_ts: Date.now() - 1000, - }, - ], - }, - ], - }, - ]); - - checkForParallelCalls(roomState); - expect(posthog.trackEvent).toHaveBeenCalledWith({ - eventName: "ParallelCalls", - participantsPerCall: { - "1": 2, - "2": 1, - }, - }); - }); - }); -}); diff --git a/src/room/checkForParallelCalls.ts b/src/room/checkForParallelCalls.ts deleted file mode 100644 index ab947176..00000000 --- a/src/room/checkForParallelCalls.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2023, 2024 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 { EventType, type RoomState } from "matrix-js-sdk"; - -import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; - -function isObject(x: unknown): x is Record { - return typeof x === "object" && x !== null; -} - -/** - * Checks the state of a room for multiple calls happening in parallel, sending - * the details to PostHog if that is indeed what's happening. (This is unwanted - * as it indicates a split-brain scenario.) - */ -export function checkForParallelCalls(state: RoomState): void { - const now = Date.now(); - const participantsPerCall = new Map(); - - // For each participant in each call, increment the participant count - for (const e of state.getStateEvents(EventType.GroupCallMemberPrefix)) { - const content = e.getContent>(); - const calls: unknown[] = Array.isArray(content["m.calls"]) - ? content["m.calls"] - : []; - - for (const call of calls) { - if (isObject(call) && typeof call["m.call_id"] === "string") { - const devices: unknown[] = Array.isArray(call["m.devices"]) - ? call["m.devices"] - : []; - - for (const device of devices) { - if (isObject(device) && (device["expires_ts"] as number) > now) { - const participantCount = - participantsPerCall.get(call["m.call_id"]) ?? 0; - participantsPerCall.set(call["m.call_id"], participantCount + 1); - } - } - } - } - } - - if (participantsPerCall.size > 1) { - PosthogAnalytics.instance.trackEvent({ - eventName: "ParallelCalls", - participantsPerCall: Object.fromEntries(participantsPerCall), - }); - } -} diff --git a/src/rtcSessionHelpers.test.ts b/src/rtcSessionHelpers.test.ts index e6b76784..8aca40f5 100644 --- a/src/rtcSessionHelpers.test.ts +++ b/src/rtcSessionHelpers.test.ts @@ -6,13 +6,12 @@ Please see LICENSE in the repository root for full details. */ import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; -import { expect, onTestFinished, test, vi } from "vitest"; +import { expect, test, vi } from "vitest"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import EventEmitter from "events"; -import { enterRTCSession, leaveRTCSession } from "../src/rtcSessionHelpers"; +import { enterRTCSession } from "../src/rtcSessionHelpers"; import { mockConfig } from "./utils/test"; -import { ElementWidgetActions, widget } from "./widget"; const USE_MUTI_SFU = false; const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); @@ -116,47 +115,6 @@ test("It joins the correct Session", async () => { ); }); -async function testLeaveRTCSession( - cause: "user" | "error", - expectClose: boolean, -): Promise { - vi.clearAllMocks(); - const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession; - await leaveRTCSession(session, cause); - expect(session.leaveRoomSession).toHaveBeenCalled(); - expect(widget!.api.transport.send).toHaveBeenCalledWith( - ElementWidgetActions.HangupCall, - expect.anything(), - ); - if (expectClose) { - expect(widget!.api.transport.send).toHaveBeenCalledWith( - ElementWidgetActions.Close, - expect.anything(), - ); - expect(widget!.api.transport.stop).toHaveBeenCalled(); - } else { - expect(widget!.api.transport.send).not.toHaveBeenCalledWith( - ElementWidgetActions.Close, - expect.anything(), - ); - expect(widget!.api.transport.stop).not.toHaveBeenCalled(); - } -} - -test("leaveRTCSession closes the widget on a normal hangup", async () => { - await testLeaveRTCSession("user", true); -}); - -test("leaveRTCSession doesn't close the widget on a fatal error", async () => { - await testLeaveRTCSession("error", false); -}); - -test("leaveRTCSession doesn't close the widget when returning to lobby", async () => { - getUrlParams.mockReturnValue({ returnToLobby: true }); - onTestFinished(() => void getUrlParams.mockReset()); - await testLeaveRTCSession("user", false); -}); - test("It should not fail with configuration error if homeserver config has livekit url but not fallback", async () => { mockConfig({}); vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({ diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 90e1fca1..fadc7b37 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -16,7 +16,7 @@ import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { PosthogAnalytics } from "./analytics/PosthogAnalytics"; import { Config } from "./config/Config"; -import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget"; +import { ElementWidgetActions, widget } from "./widget"; import { MatrixRTCTransportMissingError } from "./utils/errors"; import { getUrlParams } from "./UrlParams"; import { getSFUConfigWithOpenID } from "./livekit/openIDSFU.ts"; @@ -159,49 +159,3 @@ export async function enterRTCSession( } } } - -const widgetPostHangupProcedure = async ( - widget: WidgetHelpers, - cause: "user" | "error", - promiseBeforeHangup?: Promise, -): Promise => { - try { - await widget.api.setAlwaysOnScreen(false); - } catch (e) { - logger.error("Failed to set call widget `alwaysOnScreen` to false", e); - } - - // Wait for any last bits before hanging up. - await promiseBeforeHangup; - // We send the hangup event after the memberships have been updated - // calling leaveRTCSession. - // We need to wait because this makes the client hosting this widget killing the IFrame. - try { - await widget.api.transport.send(ElementWidgetActions.HangupCall, {}); - } catch (e) { - logger.error("Failed to send hangup action", e); - } - // On a normal user hangup we can shut down and close the widget. But if an - // error occurs we should keep the widget open until the user reads it. - if (cause === "user" && !getUrlParams().returnToLobby) { - try { - await widget.api.transport.send(ElementWidgetActions.Close, {}); - } catch (e) { - logger.error("Failed to send close action", e); - } - widget.api.transport.stop(); - } -}; - -export async function leaveRTCSession( - rtcSession: MatrixRTCSession, - cause: "user" | "error", - promiseBeforeHangup?: Promise, -): Promise { - await rtcSession.leaveRoomSession(); - if (widget) { - await widgetPostHangupProcedure(widget, cause, promiseBeforeHangup); - } else { - await promiseBeforeHangup; - } -} diff --git a/src/useErrorBoundary.test.tsx b/src/useErrorBoundary.test.tsx deleted file mode 100644 index 13fa43bb..00000000 --- a/src/useErrorBoundary.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* -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 { it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { type ReactElement, useCallback } from "react"; -import userEvent from "@testing-library/user-event"; -import { BrowserRouter } from "react-router-dom"; - -import { GroupCallErrorBoundary } from "./room/GroupCallErrorBoundary"; -import { useErrorBoundary } from "./useErrorBoundary"; -import { ConnectionLostError } from "./utils/errors"; - -it("should show async error", async () => { - const user = userEvent.setup(); - - const TestComponent = (): ReactElement => { - const { showErrorBoundary } = useErrorBoundary(); - - const onClick = useCallback((): void => { - showErrorBoundary(new ConnectionLostError()); - }, [showErrorBoundary]); - - return ( -
-

HELLO

- -
- ); - }; - - render( - - - - - , - ); - - await user.click(screen.getByRole("button", { name: "Click me" })); - - await screen.findByText("Connection lost"); - - await user.click(screen.getByRole("button", { name: "Reconnect" })); - - await screen.findByText("HELLO"); -}); diff --git a/src/useErrorBoundary.ts b/src/useErrorBoundary.ts deleted file mode 100644 index 4430394e..00000000 --- a/src/useErrorBoundary.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2023, 2024 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 { useMemo, useState } from "react"; - -export type UseErrorBoundaryApi = { - showErrorBoundary: (error: Error) => void; -}; - -export function useErrorBoundary(): UseErrorBoundaryApi { - const [error, setError] = useState(null); - - const memoized: UseErrorBoundaryApi = useMemo( - () => ({ - showErrorBoundary: (error: Error) => setError(error), - }), - [], - ); - - if (error) { - throw error; - } - - return memoized; -}