From 81aa79ad240314b6a06e02737fffba16149ebc80 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 5 Feb 2025 12:36:34 +0100 Subject: [PATCH 1/2] 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) --- src/components/views/voip/CallView.tsx | 10 +--------- src/models/Call.ts | 5 +++-- src/stores/RoomViewStore.tsx | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index b7b15c2f715..0b275917db0 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -29,6 +29,7 @@ const JoinCallView: FC = ({ 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 = ({ 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 = useCallback(async () => { // The stickyPromise has to resolve before the widget actually becomes sticky. @@ -88,11 +85,6 @@ interface CallViewProps { export const CallView: FC = ({ 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 { diff --git a/src/models/Call.ts b/src/models/Call.ts index b5b0d5a6ec2..4b1e30a82b0 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -741,7 +741,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()!, @@ -762,6 +762,8 @@ export class ElementCall extends Call { }, roomId, ); + WidgetStore.instance.emit(UPDATE_EVENT, null); + return createdWidget; } private static getWidgetData( @@ -830,7 +832,6 @@ export class ElementCall extends Call { public static async create(room: Room, skipLobby = false): Promise { ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room)); - WidgetStore.instance.emit(UPDATE_EVENT, null); } protected async sendCallNotify(): Promise { diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 822a6a9dd1f..a788d5ad1f7 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -47,6 +47,7 @@ import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPa import { 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; @@ -350,6 +351,19 @@ export class RoomViewStore extends EventEmitter { }); } + // Start call when requested + const currentRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null; + if (payload.view_call && room) { + if (!currentRoomCall) { + ElementCall.create(room, false); + } + } + // Destroy call when requested leaving call view + const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null; + if (prevRoomCall && !prevRoomCall.connected) { + currentRoomCall?.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. From b62cfca11d8d50aba93506330aed6b6bec7242ca Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 5 Feb 2025 12:42:42 +0100 Subject: [PATCH 2/2] lint and typo --- src/components/structures/RoomView.tsx | 2 +- src/components/views/voip/CallView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5695c7a4048..d642c36497d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1689,7 +1689,7 @@ export class RoomView extends React.Component { .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; } }); diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 0b275917db0..5c0fe391852 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { FC, useContext, useEffect, AriaRole, useCallback } from "react"; import type { Room } from "matrix-js-sdk/src/matrix"; -import { Call, ConnectionState, ElementCall } from "../../../models/Call"; +import { Call, ConnectionState } from "../../../models/Call"; import { useCall } from "../../../hooks/useCall"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import AppTile from "../elements/AppTile";