From 29a54405d87509db6f23811fc66d10adede31890 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 25 Nov 2025 09:49:11 -0500 Subject: [PATCH] Fix calls sometimes not knowing that they're presented (#31313) 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 da3ea49bc1..489dd775f8 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 e7b7e95862..bce01af202 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 17ba9670c3..2f7c666895 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