From 07577a6cdefdfe95ef6deb5b8c38e1fb2496f8e1 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Fri, 2 Dec 2022 10:35:04 +0000 Subject: [PATCH] [Backport staging] Fix call splitbrains when switching between rooms (#9701) * Fix call splitbrains when switching between rooms Mounting CallView causes the user's call membership room state to be cleaned up. However, because the GroupCall object always thought the local device was disconnected from the call, it would remove the local device from room state when the clean method is called, causing a splitbrain. This uses GroupCall's new enteredViaAnotherSession field to fix that, and also simplify participant tracking. * Remove clean tests that have been moved to matrix-js-sdk (cherry picked from commit 62a740d318980c98475aa564ef54bfc27e6d3880) Co-authored-by: Robin --- src/models/Call.ts | 27 +---------- test/models/Call-test.ts | 90 ----------------------------------- test/test-utils/test-utils.ts | 1 + 3 files changed, 3 insertions(+), 115 deletions(-) diff --git a/src/models/Call.ts b/src/models/Call.ts index 0e20c331fb..383f3557ac 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -647,7 +647,6 @@ export class ElementCall extends Call { client, ); - this.on(CallEvent.ConnectionState, this.onConnectionState); this.on(CallEvent.Participants, this.onParticipants); groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants); groupCall.on(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState); @@ -704,6 +703,7 @@ export class ElementCall extends Call { throw new Error(`Failed to join call in room ${this.roomId}: ${e}`); } + this.groupCall.enteredViaAnotherSession = true; this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); @@ -724,11 +724,11 @@ export class ElementCall extends Call { this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest); super.setDisconnected(); + this.groupCall.enteredViaAnotherSession = false; } public destroy() { WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId); - this.off(CallEvent.ConnectionState, this.onConnectionState); this.off(CallEvent.Participants, this.onParticipants); this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants); this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState); @@ -760,20 +760,6 @@ export class ElementCall extends Call { participants.set(member, new Set(deviceMap.keys())); } - // We never enter group calls natively, so the GroupCall will think it's - // disconnected regardless of what our call member state says. Thus we - // have to insert our own device manually when connected via the widget. - if (this.connected) { - const localMember = this.room.getMember(this.client.getUserId()!)!; - let devices = participants.get(localMember); - if (devices === undefined) { - devices = new Set(); - participants.set(localMember, devices); - } - - devices.add(this.client.getDeviceId()!); - } - this.participants = participants; } @@ -782,15 +768,6 @@ export class ElementCall extends Call { && this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client); } - private onConnectionState = (state: ConnectionState, prevState: ConnectionState) => { - if ( - (state === ConnectionState.Connected && !isConnected(prevState)) - || (state === ConnectionState.Disconnected && isConnected(prevState)) - ) { - this.updateParticipants(); // Local echo - } - }; - private onParticipants = async ( participants: Map>, prevParticipants: Map>, diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts index aa22db2718..785b9eea58 100644 --- a/test/models/Call-test.ts +++ b/test/models/Call-test.ts @@ -939,96 +939,6 @@ describe("ElementCall", () => { call.off(CallEvent.Destroy, onDestroy); }); - - describe("clean", () => { - const aliceWeb: IMyDevice = { - device_id: "aliceweb", - last_seen_ts: 0, - }; - const aliceDesktop: IMyDevice = { - device_id: "alicedesktop", - last_seen_ts: 0, - }; - const aliceDesktopOffline: IMyDevice = { - device_id: "alicedesktopoffline", - last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago - }; - const aliceDesktopNeverOnline: IMyDevice = { - device_id: "alicedesktopneveronline", - }; - - const mkContent = (devices: IMyDevice[]) => ({ - "m.calls": [{ - "m.call_id": call.groupCall.groupCallId, - "m.devices": devices.map(d => ({ - device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10, - })), - }], - }); - const expectDevices = (devices: IMyDevice[]) => expect( - room.currentState.getStateEvents(ElementCall.MEMBER_EVENT_TYPE.name, alice.userId)?.getContent(), - ).toEqual({ - "m.calls": [{ - "m.call_id": call.groupCall.groupCallId, - "m.devices": devices.map(d => ({ - device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number), - })), - }], - }); - - beforeEach(() => { - client.getDeviceId.mockReturnValue(aliceWeb.device_id); - client.getDevices.mockResolvedValue({ - devices: [ - aliceWeb, - aliceDesktop, - aliceDesktopOffline, - aliceDesktopNeverOnline, - ], - }); - }); - - it("doesn't clean up valid devices", async () => { - await client.sendStateEvent( - room.roomId, - ElementCall.MEMBER_EVENT_TYPE.name, - mkContent([aliceDesktop]), - alice.userId, - ); - - await call.clean(); - expectDevices([aliceDesktop]); - }); - - it("cleans up our own device if we're disconnected", async () => { - await client.sendStateEvent( - room.roomId, - ElementCall.MEMBER_EVENT_TYPE.name, - mkContent([aliceWeb, aliceDesktop]), - alice.userId, - ); - - await call.clean(); - expectDevices([aliceDesktop]); - }); - - it("cleans up devices that have never been online", async () => { - await client.sendStateEvent( - room.roomId, - ElementCall.MEMBER_EVENT_TYPE.name, - mkContent([aliceDesktop, aliceDesktopNeverOnline]), - alice.userId, - ); - - await call.clean(); - expectDevices([aliceDesktop]); - }); - - it("no-ops if there are no state events", async () => { - await call.clean(); - expect(room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId)).toBe(null); - }); - }); }); describe("instance in a video room", () => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index e218629547..69b626dfd5 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -91,6 +91,7 @@ export function createTestClient(): MatrixClient { getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), deviceId: "ABCDEFGHI", getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }), + getSessionId: jest.fn().mockReturnValue("iaszphgvfku"), credentials: { userId: "@userId:matrix.org" }, store: {