From 5444b32ace1c0422256d95e0bb01c5c25777e1d8 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 12 Feb 2026 15:17:03 +0100 Subject: [PATCH 1/2] Avoid showing two chat timelines side by side after a call In certain situations you could still end up with the chat timeline visible in the right panel in addition to the main split. For instance if you are in a call, open the chat panel, then leave the call while looking at another room, the chat panel would remain open upon navigating back to the original room. --- src/components/structures/RoomView.tsx | 3 +- test/test-utils/test-utils.ts | 1 + .../components/structures/RoomView-test.tsx | 95 ++++++++++++++++++- .../__snapshots__/RoomView-test.tsx.snap | 6 +- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 7f86f5d28c4..76177bc5c82 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -647,13 +647,12 @@ export class RoomView extends React.Component { }; if ( - this.state.mainSplitContentType !== MainSplitContentType.Timeline && newState.mainSplitContentType === MainSplitContentType.Timeline && this.context.rightPanelStore.isOpen && this.context.rightPanelStore.currentCard.phase === RightPanelPhases.Timeline && this.context.rightPanelStore.roomPhaseHistory.some((card) => card.phase === RightPanelPhases.Timeline) ) { - // We're returning to the main timeline, so hide the right panel timeline + // The main split shows the main timeline, so hide the right panel timeline this.context.rightPanelStore.setCard({ phase: RightPanelPhases.RoomSummary }); this.context.rightPanelStore.togglePanel(this.state.roomId ?? null); newState.showRightPanel = false; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index a894608b242..f15c21130b7 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -186,6 +186,7 @@ export function createTestClient(): MatrixClient { peekInRoom: jest.fn().mockResolvedValue(mkStubRoom(undefined, undefined, undefined)), stopPeeking: jest.fn(), + getEventTimeline: jest.fn().mockResolvedValue([]), paginateEventTimeline: jest.fn().mockResolvedValue(undefined), sendReadReceipt: jest.fn().mockResolvedValue(undefined), getRoomIdForAlias: jest.fn().mockResolvedValue(undefined), diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index ca1020a9b98..ded0c46627e 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -24,7 +24,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { type CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { act, cleanup, fireEvent, render, type RenderResult, screen, waitFor } from "jest-matrix-react"; +import { act, cleanup, fireEvent, render, type RenderResult, screen, waitFor, getByRole } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { @@ -643,6 +643,99 @@ describe("RoomView", () => { }); }); + describe("group calls", () => { + beforeEach(async () => { + await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet()); + jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join); + }); + + it("hides the right panel chat when closing a call", async () => { + await mountRoomView(); + + // Open the call + await act(() => + stores.roomViewStore.viewRoom({ + action: Action.ViewRoom, + room_id: stores.roomViewStore.getRoomId()!, + event_id: "$eventId", + metricsTrigger: undefined, + view_call: true, + }), + ); + // Open the chat in the right panel + await act(async () => { + stores.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }); + await flushPromises(); + }); + // Chat should be visible in the right panel + getByRole(screen.getByRole("complementary"), "heading", { name: "Chat" }); + + // Close the call + await act(() => + stores.roomViewStore.viewRoom({ + action: Action.ViewRoom, + room_id: stores.roomViewStore.getRoomId()!, + event_id: "$eventId", + metricsTrigger: undefined, + view_call: false, + }), + ); + // Right panel should be gone + expect(screen.queryByRole("complementary")).toBe(null); + // Opening the right panel again should just show the room summary + await act(async () => { + stores.rightPanelStore.show(room.roomId); + await flushPromises(); + }); + getByRole(screen.getByRole("complementary"), "heading", { name: room.roomId }); + }); + + it("hides the right panel chat when returning to a room that previously showed a call", async () => { + const room2 = new Room(`!roomswitchtest:example.org`, cli, "@alice:example.org"); + rooms.set(room2.roomId, room2); + await mountRoomView(); + + // Open the call + await act(() => + stores.roomViewStore.viewRoom({ + action: Action.ViewRoom, + room_id: room.roomId, + event_id: "$eventId", + metricsTrigger: undefined, + view_call: true, + }), + ); + // Open the chat in the right panel + await act(async () => { + stores.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }); + await flushPromises(); + }); + // Chat should be visible in the right panel + getByRole(screen.getByRole("complementary"), "heading", { name: "Chat" }); + + // Navigate away to another room + await act(() => + stores.roomViewStore.viewRoom({ + action: Action.ViewRoom, + room_id: room2.roomId, + event_id: "$eventId", + metricsTrigger: undefined, + }), + ); + // Navigate back to the original room + await act(() => + stores.roomViewStore.viewRoom({ + action: Action.ViewRoom, + room_id: room.roomId, + event_id: "$eventId", + metricsTrigger: undefined, + }), + ); + // Right panel should be gone + expect(screen.queryByRole("complementary")).toBe(null); + }); + }); + describe("for a local room", () => { let localRoom: LocalRoom; diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index e15ac173d29..537aa6b422c 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -212,7 +212,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]

Could not start a chat with this user

@@ -417,7 +417,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = > Date: Thu, 12 Feb 2026 16:41:30 +0100 Subject: [PATCH 2/2] Avoid using flushPromises in tests --- .../components/structures/RoomView-test.tsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index ded0c46627e..a1b43be2328 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -24,7 +24,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { type CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { act, cleanup, fireEvent, render, type RenderResult, screen, waitFor, getByRole } from "jest-matrix-react"; +import { act, cleanup, fireEvent, render, type RenderResult, screen, waitFor, findByRole } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { @@ -663,12 +663,9 @@ describe("RoomView", () => { }), ); // Open the chat in the right panel - await act(async () => { - stores.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }); - await flushPromises(); - }); + act(() => stores.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline })); // Chat should be visible in the right panel - getByRole(screen.getByRole("complementary"), "heading", { name: "Chat" }); + await findByRole(await screen.findByRole("complementary"), "heading", { name: "Chat" }); // Close the call await act(() => @@ -683,11 +680,8 @@ describe("RoomView", () => { // Right panel should be gone expect(screen.queryByRole("complementary")).toBe(null); // Opening the right panel again should just show the room summary - await act(async () => { - stores.rightPanelStore.show(room.roomId); - await flushPromises(); - }); - getByRole(screen.getByRole("complementary"), "heading", { name: room.roomId }); + act(() => stores.rightPanelStore.show(room.roomId)); + await findByRole(await screen.findByRole("complementary"), "heading", { name: room.roomId }); }); it("hides the right panel chat when returning to a room that previously showed a call", async () => { @@ -706,12 +700,9 @@ describe("RoomView", () => { }), ); // Open the chat in the right panel - await act(async () => { - stores.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }); - await flushPromises(); - }); + act(() => stores.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline })); // Chat should be visible in the right panel - getByRole(screen.getByRole("complementary"), "heading", { name: "Chat" }); + await findByRole(await screen.findByRole("complementary"), "heading", { name: "Chat" }); // Navigate away to another room await act(() =>