From 6f99cca23884ffbc0d7e9b6da0c41a0f4003cc05 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 24 Nov 2025 16:06:34 -0500 Subject: [PATCH] Fix calls sometimes not knowing that they're presented Because RoomViewStore used two slightly different conditions, the Call.presented flag could get out of sync with the viewingCall flag. But these should effectively be the same thing. This was causing some subtle bugs if you would join a call, switch to another room, and then click back into the call room via the room list. The call would be visible but not know that it's presented, causing: 1. The hangup sound to get cut off at the end of the call 2. The widget to disappear immediately without offering a 'reconnect' button if you lose connectivity --- src/stores/RoomViewStore.tsx | 18 ++++++--- .../structures/PipContainer-test.tsx | 1 + test/unit-tests/stores/RoomViewStore-test.ts | 37 +++++++++++++------ 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index da3ea49bc11..489dd775f82 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -361,7 +361,17 @@ export class RoomViewStore extends EventEmitter { }); } - if (room && (payload.view_call || isVideoRoom(room))) { + let viewingCall = payload.view_call; + if (viewingCall === undefined) { + // Default behavior: keep the same call state as before if viewing the same room + if (payload.room_id === this.state.roomId) viewingCall = this.state.viewingCall; + // Always view the call in video rooms + else if (room && isVideoRoom(room)) viewingCall = true; + // Otherwise, only view if actively connected + else viewingCall = CallStore.instance.getActiveCall(payload.room_id) !== null; + } + + if (room && viewingCall) { let call = CallStore.instance.getCall(payload.room_id); // Start a call if not already there if (call === null) { @@ -421,11 +431,7 @@ export class RoomViewStore extends EventEmitter { replyingToEvent: null, viaServers: payload.via_servers ?? [], wasContextSwitch: payload.context_switch ?? false, - viewingCall: - payload.view_call ?? - (payload.room_id === this.state.roomId - ? this.state.viewingCall - : CallStore.instance.getActiveCall(payload.room_id) !== null), + viewingCall, }; // Allow being given an event to be replied to when switching rooms but sanity check its for this room diff --git a/test/unit-tests/components/structures/PipContainer-test.tsx b/test/unit-tests/components/structures/PipContainer-test.tsx index e7b7e958627..bce01af2025 100644 --- a/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/test/unit-tests/components/structures/PipContainer-test.tsx @@ -133,6 +133,7 @@ describe("PipContainer", () => { { action: Action.ViewRoom, room_id: roomId, + view_call: false, // We're testing PiP functionality, so view the timeline metricsTrigger: undefined, }, true, diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 17ba9670c36..2f7c6668952 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -146,16 +146,6 @@ describe("RoomViewStore", function () { const room2 = new Room(roomId2, mockClient, userId); getRooms.mockReturnValue([room, room2]); - const viewCall = async (): Promise => { - dis.dispatch({ - action: Action.ViewRoom, - room_id: roomId, - view_call: true, - metricsTrigger: undefined, - }); - await untilDispatch(Action.ViewRoom, dis); - }; - const dispatchPromptAskToJoin = async () => { dis.dispatch({ action: Action.PromptAskToJoin }); await untilDispatch(Action.PromptAskToJoin, dis); @@ -404,11 +394,36 @@ describe("RoomViewStore", function () { const call = { presented: false } as Call; const getCallSpy = jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call); await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet()); - await viewCall(); + + dis.dispatch({ + action: Action.ViewRoom, + room_id: roomId, + view_call: true, + metricsTrigger: undefined, + }); + await untilDispatch(Action.ViewRoom, dis); + expect(getCallSpy).toHaveBeenCalledWith(roomId); expect(call.presented).toEqual(true); }); + it("implicitly views an active call", async () => { + const call = { presented: false } as Call; + jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call); + jest.spyOn(CallStore.instance, "getActiveCall").mockImplementation((rId) => (rId === roomId ? call : null)); + await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet()); + + // View the room without explicitly setting view_call to true + dis.dispatch({ + action: Action.ViewRoom, + room_id: roomId, + metricsTrigger: undefined, + }); + await untilDispatch(Action.ViewRoom, dis); + + expect(call.presented).toEqual(true); + }); + it("should display an error message when the room is unreachable via the roomId", async () => { // When // View and wait for the room