diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a64b118c2c3..755589d33ea 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1697,7 +1697,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 d45554d08f6..7a295c919d6 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, { 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 = ({ 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 8ce5a1d2ca4..6cc913d93b0 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -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 { 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 c691f0af797..207c966376d 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -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,19 @@ export class RoomViewStore extends EventEmitter { }); } + // Start a call if 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 the call when 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. diff --git a/test/unit-tests/components/views/voip/CallView-test.tsx b/test/unit-tests/components/views/voip/CallView-test.tsx index 52998074847..93e84c343d3 100644 --- a/test/unit-tests/components/views/voip/CallView-test.tsx +++ b/test/unit-tests/components/views/voip/CallView-test.tsx @@ -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({ + 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)!; diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 74aa952036b..5116f8452d5 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -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); @@ -307,6 +316,7 @@ describe("RoomViewStore", function () { it("should display an error message when the room is unreachable via the roomId", async () => { // When // View and wait for the room + // jest.spyOn(WidgetStore).mockReturnValue({ getApps: () => [] } as unknown as void & WidgetStore); dis.dispatch({ action: Action.ViewRoom, room_id: roomId }); await untilDispatch(Action.ActiveRoomChanged, dis); // Generate error to display the expected error message