diff --git a/locales/en/app.json b/locales/en/app.json index ca971fbc..8656502c 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -120,6 +120,8 @@ "peer_connection_timeout_description": "Connection to the media server timed out. Try switching to a different network or disabling your VPN. If the problem persists, see our <0>troubleshooting guide or contact your server administrator.", "room_creation_restricted": "Failed to create call", "room_creation_restricted_description": "Call creation might be restricted to authorized users only. Try again later, or contact your server admin if the problem persists.", + "sticky_events_required": "Homeserver does not support Matrix 2.0 calls", + "sticky_events_required_description": "This deployment is configured to use Matrix 2.0 call mode, but the homeserver does not advertise support for sticky events (MSC4354). Ask your server admin to upgrade, or switch the deployment to a compatible mode.", "unexpected_ec_error": "An unexpected error occurred (<0>Error Code: <1>{{ errorCode }}). Please contact your server admin." }, "group_call_loader": { diff --git a/src/room/GroupCallErrorBoundary.test.tsx b/src/room/GroupCallErrorBoundary.test.tsx index 891a9724..1687554c 100644 --- a/src/room/GroupCallErrorBoundary.test.tsx +++ b/src/room/GroupCallErrorBoundary.test.tsx @@ -59,6 +59,12 @@ test.each([ 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.", }, + { + error: new StickyEventsRequiredError(), + expectedTitle: "Homeserver does not support Matrix 2.0 calls", + expectedDescription: + "This deployment is configured to use Matrix 2.0 call mode, but the homeserver does not advertise support for sticky events (MSC4354). Ask your server admin to upgrade, or switch the deployment to a compatible mode.", + }, ])( "should report correct error for $expectedTitle", async ({ error, expectedTitle, expectedDescription }) => { diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 02fcd64b..1c4f2462 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -38,6 +38,7 @@ import { useAudioContext } from "../useAudioContext"; import { ActiveCall } from "./InCallView"; import { flushPromises, + mockConfig, mockEmitter, mockMatrixRoom, mockMatrixRoomMember, @@ -46,9 +47,11 @@ import { MockRTCSession, } from "../utils/test"; import { GroupCallView } from "./GroupCallView"; +import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary"; import { ElementWidgetActions, type WidgetHelpers } from "../widget"; import { LazyEventEmitter } from "../LazyEventEmitter"; import { MatrixRTCTransportMissingError } from "../utils/errors"; +import { MatrixRTCMode } from "../config/ConfigOptions"; import { ProcessorProvider } from "../livekit/TrackProcessorContext"; import { MediaDevicesContext } from "../MediaDevicesContext"; import { constant } from "../state/Behavior"; @@ -130,6 +133,10 @@ beforeEach(() => { function createGroupCallView( widget: WidgetHelpers | null, joined = true, + options: { + doesServerSupportUnstableFeature?: (feature: string) => Promise; + withErrorBoundary?: boolean; + } = {}, ): { rtcSession: MatrixRTCSession; getByText: ReturnType["getByText"]; @@ -139,6 +146,9 @@ function createGroupCallView( getUserId: () => localRtcMember.userId, getDeviceId: () => localRtcMember.deviceId, getRoom: (rId) => (rId === roomId ? room : null), + doesServerSupportUnstableFeature: + options.doesServerSupportUnstableFeature ?? + vi.fn().mockResolvedValue(true), } as Partial as MatrixClient; const room = mockMatrixRoom({ relations: { @@ -166,24 +176,36 @@ function createGroupCallView( video: { enabled: false }, // TODO-MULTI-SFU: This cast isn't valid, it's likely the cause of some current test failures } as unknown as MuteStates; + const groupCallView = ( + + ); const { getByText } = render( - + {options.withErrorBoundary ? ( + + {groupCallView} + + ) : ( + groupCallView + )} @@ -394,6 +416,30 @@ test.skip("GroupCallView shows errors that occur during joining", async () => { screen.getByText("Call is not supported"); }); +test("shows StickyEventsRequiredError when matrix_2_0 is forced but homeserver lacks MSC4354", async () => { + mockConfig({ matrix_rtc_mode: MatrixRTCMode.Matrix_2_0 }); + createGroupCallView(null, true, { + doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false), + withErrorBoundary: true, + }); + await screen.findByText("Homeserver does not support Matrix 2.0 calls"); +}); + +test("does not show StickyEventsRequiredError when homeserver supports MSC4354", async () => { + mockConfig({ matrix_rtc_mode: MatrixRTCMode.Matrix_2_0 }); + const { getByText } = createGroupCallView(null, true, { + doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(true), + withErrorBoundary: true, + }); + // Give the async support check a chance to resolve. + await flushPromises(); + expect( + screen.queryByText("Homeserver does not support Matrix 2.0 calls"), + ).toBeNull(); + // The normal call UI (mocked ActiveCall) renders instead. + expect(getByText("Leave")).toBeInTheDocument(); +}); + test("user can reconnect after a membership manager error", async () => { const user = userEvent.setup(); const { rtcSession } = createGroupCallView(null, true); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 7c9009fe..7fa47551 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -13,7 +13,12 @@ import { useMemo, useState, } from "react"; -import { type MatrixClient, JoinRule, type Room } from "matrix-js-sdk"; +import { + type MatrixClient, + JoinRule, + type Room, + UNSTABLE_MSC4354_STICKY_EVENTS, +} from "matrix-js-sdk"; import { Room as LivekitRoom, isE2EESupported as isE2EESupportedBrowser, @@ -67,8 +72,11 @@ import { ConnectionLostError, E2EENotSupportedError, ElementCallError, + StickyEventsRequiredError, UnknownCallError, } from "../utils/errors.ts"; +import { Config } from "../config/Config.ts"; +import { MatrixRTCMode } from "../config/ConfigOptions.ts"; import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx"; import { useTypedEventEmitter } from "../useEvents"; import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts"; @@ -164,6 +172,28 @@ export const GroupCallView: FC = ({ MatrixRTCSessionEvent.MembershipManagerError, (error) => setExternalError(new ConnectionLostError()), ); + + // If the deployment pins matrix_rtc_mode=matrix_2_0 but this homeserver + // doesn't advertise MSC4354 (sticky events), the call will fail to connect + // with a generic "Connection lost" message. Surface the real cause up front. + useEffect(() => { + if (Config.get().matrix_rtc_mode !== MatrixRTCMode.Matrix_2_0) return; + let cancelled = false; + client + .doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS) + .then((supported) => { + if (!cancelled && !supported) { + setExternalError(new StickyEventsRequiredError()); + } + }) + .catch((e) => { + logger.warn("Failed to check sticky-events homeserver support", e); + }); + return (): void => { + cancelled = true; + }; + }, [client]); + useEffect(() => { // Sanity check the room object if (client.getRoom(rtcSession.room.roomId) !== rtcSession.room) diff --git a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap index 5a687eb2..81041942 100644 --- a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap +++ b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap @@ -1720,6 +1720,160 @@ exports[`should report correct error for 'Connection lost' 1`] = ` `; +exports[`should report correct error for 'Homeserver does not support Matrix 2.…' 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Homeserver does not support Matrix 2.0 calls +

+

+ This deployment is configured to use Matrix 2.0 call mode, but the homeserver does not advertise support for sticky events (MSC4354). Ask your server admin to upgrade, or switch the deployment to a compatible mode. +

+ +
+
+
+
+
+`; + exports[`should report correct error for 'Incompatible browser' 1`] = `