Compare commits

...

6 Commits

Author SHA1 Message Date
Timo
471712cbf0 fix destroy condition 2025-02-11 14:22:26 +01:00
Timo
b7b8719171 Update src/stores/RoomViewStore.tsx
Co-authored-by: Florian D <florianduros@element.io>
2025-02-11 11:28:35 +01:00
Timo
2135918d02 test fixing 2025-02-10 18:15:19 +01:00
Timo
99a0057d7d test fixes 2025-02-10 18:15:19 +01:00
Timo
dc7cefdffe lint and typo 2025-02-10 18:15:19 +01:00
Timo
1f3203f3a2 ElementCall fix strict mode call creation loop
In strict mode there is a call create -> destroy -> create infinite loop wen pressing the call button.

This loop was a consequence of relying on component creation/destruction to handle creating and removing the call.
This logic:
 - destroying a call if it was in the lobby but leaving it if it is connected when the user stops viewing the room the call belongs to.
  - Creating an ElementCall if there is not yet once when the user starts viewing a call.

Belongs into the roomViewStore and not the components that are just a sideffect in the call livecycle. (view model separation)
2025-02-10 18:15:19 +01:00
6 changed files with 51 additions and 14 deletions

View File

@@ -1697,7 +1697,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
.sendStickerContentToRoom(url, roomId, threadId, info, text, this.context.client)
.then(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
// Let the status bar handle this
return;
}
});

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type FC, useContext, useEffect, type AriaRole, useCallback } from "react";
import type { Room } from "matrix-js-sdk/src/matrix";
import { type Call, ConnectionState, ElementCall } from "../../../models/Call";
import { type Call, ConnectionState } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AppTile from "../elements/AppTile";
@@ -29,6 +29,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
useEffect(() => {
// We'll take this opportunity to tidy up our room state
// Only needed for jitsi.
call.clean();
}, [call]);
@@ -44,10 +45,6 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
// (this will start the lobby view in the widget and connect to all required widget events)
call.start();
}
return (): void => {
// If we are connected the widget is sticky and we do not want to destroy the call.
if (!call.connected) call.destroy();
};
}, [call]);
const disconnectAllOtherCalls: () => Promise<void> = useCallback(async () => {
// The stickyPromise has to resolve before the widget actually becomes sticky.
@@ -88,11 +85,6 @@ interface CallViewProps {
export const CallView: FC<CallViewProps> = ({ room, resizing, waitForCall, skipLobby, role }) => {
const call = useCall(room.roomId);
useEffect(() => {
if (call === null && !waitForCall) {
ElementCall.create(room, skipLobby);
}
}, [call, room, skipLobby, waitForCall]);
if (call === null) {
return null;
} else {

View File

@@ -740,7 +740,7 @@ export class ElementCall extends Call {
// To use Element Call without touching room state, we create a virtual
// widget (one that doesn't have a corresponding state event)
const url = ElementCall.generateWidgetUrl(client, roomId);
return WidgetStore.instance.addVirtualWidget(
const createdWidget = WidgetStore.instance.addVirtualWidget(
{
id: secureRandomString(24), // So that it's globally unique
creatorUserId: client.getUserId()!,
@@ -761,6 +761,8 @@ export class ElementCall extends Call {
},
roomId,
);
WidgetStore.instance.emit(UPDATE_EVENT, null);
return createdWidget;
}
private static getWidgetData(
@@ -829,7 +831,6 @@ export class ElementCall extends Call {
public static async create(room: Room, skipLobby = false): Promise<void> {
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room));
WidgetStore.instance.emit(UPDATE_EVENT, null);
}
protected async sendCallNotify(): Promise<void> {

View File

@@ -50,6 +50,7 @@ import { type CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJ
import { type SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
import { ModuleRunner } from "../modules/ModuleRunner";
import { setMarkedUnreadState } from "../utils/notifications";
import { ElementCall } from "../models/Call";
const NUM_JOIN_RETRY = 5;
@@ -353,6 +354,23 @@ export class RoomViewStore extends EventEmitter {
});
}
// Start a call if requested and not already there
const currentRoomCall = payload.room_id ? CallStore.instance.getCall(payload.room_id) : null;
if (payload.view_call && room && !currentRoomCall) {
ElementCall.create(room, false);
}
// Destroy all calls that are not connected when dispatching to a new room id.
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;
if (
// not looking at prevRoomCall anymore
(!payload.view_call || payload.room_id !== this.state.roomId) &&
// not connected to the previous call anymore
prevRoomCall &&
!prevRoomCall.connected
) {
prevRoomCall?.destroy();
}
if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) {
// unsubscribe from this room, but don't await it as we don't care when this gets done.

View File

@@ -34,6 +34,12 @@ import { CallView as _CallView } from "../../../../../src/components/views/voip/
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../../src/stores/CallStore";
import { Call, ConnectionState } from "../../../../../src/models/Call";
import { RoomViewStore } from "../../../../../src/stores/RoomViewStore";
import { type ViewRoomPayload } from "../../../../../src/dispatcher/payloads/ViewRoomPayload";
import { MatrixDispatcher } from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { TestSdkContext } from "../../../TestSdkContext";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
const CallView = wrapInMatrixClientContext(_CallView);
@@ -156,7 +162,18 @@ describe("CallView", () => {
describe("without an existing call", () => {
it("creates and connects to a new call when the join button is pressed", async () => {
expect(Call.get(room)).toBeNull();
const disp = new MatrixDispatcher();
const stores = new TestSdkContext();
stores.client = room.client;
DMRoomMap.makeShared(room.client);
new RoomViewStore(disp, stores);
await renderView(true);
disp.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
metricsTrigger: "Timeline",
});
await waitFor(() => expect(CallStore.instance.getCall(room.roomId)).not.toBeNull());
const call = CallStore.instance.getCall(room.roomId)!;

View File

@@ -72,7 +72,15 @@ jest.mock("../../../src/utils/DMRoomMap", () => {
};
});
jest.mock("../../../src/stores/WidgetStore");
jest.mock("../../../src/stores/WidgetStore", () => {
return {
instance: {
getApps: () => [],
addVirtualWidget: jest.fn(),
emit: jest.fn(),
},
};
});
jest.mock("../../../src/stores/widgets/WidgetLayoutStore");
describe("RoomViewStore", function () {
@@ -97,6 +105,7 @@ describe("RoomViewStore", function () {
knockRoom: jest.fn(),
leave: jest.fn(),
setRoomAccountData: jest.fn(),
getAccountData: jest.fn(),
});
const room = new Room(roomId, mockClient, userId);
const room2 = new Room(roomId2, mockClient, userId);