Show error screens in group calls (#29254)
* Avoid destroying calls until they are hidden from the UI We often want calls to exist even when no more participants are left in the MatrixRTC session. So, we should avoid destroying calls as long as they're being presented in the UI; this means that the user has an intent to either join the call or continue looking at an error screen, and we shouldn't interrupt that interaction. The RoomViewStore is now what takes care of creating and destroying calls, rather than the CallView. In general it seems kinda impossible to safely create and destroy model objects from React lifecycle hooks, so moving this responsibility to a store seemed appropriate and resolves existing issues with calls in React strict mode. * Wait for a close action before closing a call This creates a distinction between the user hanging up and the widget being ready to close, which is useful for allowing Element Call to show error screens when disconnected from the call, for example. * Don't expect a 'close' action in video rooms These use the returnToLobby option and are expected to remain visible when the user leaves the call.
This commit is contained in:
@@ -13,10 +13,16 @@ import {
|
||||
RoomViewLifecycle,
|
||||
type ViewRoomOpts,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { getMockClientWithEventEmitter, untilDispatch, untilEmission } from "../../test-utils";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
setupAsyncStoreWithClient,
|
||||
untilDispatch,
|
||||
untilEmission,
|
||||
} from "../../test-utils";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { SlidingSyncManager } from "../../../src/SlidingSyncManager";
|
||||
import { PosthogAnalytics } from "../../../src/PosthogAnalytics";
|
||||
@@ -33,6 +39,10 @@ import { type CancelAskToJoinPayload } from "../../../src/dispatcher/payloads/Ca
|
||||
import { type JoinRoomErrorPayload } from "../../../src/dispatcher/payloads/JoinRoomErrorPayload";
|
||||
import { type SubmitAskToJoinPayload } from "../../../src/dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
import { ModuleRunner } from "../../../src/modules/ModuleRunner";
|
||||
import { type IApp } from "../../../src/utils/WidgetUtils-types";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
|
||||
|
||||
jest.mock("../../../src/Modal");
|
||||
|
||||
@@ -60,6 +70,12 @@ jest.mock("../../../src/audio/VoiceRecording", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||
[MediaDeviceKindEnum.AudioInput]: [],
|
||||
[MediaDeviceKindEnum.VideoInput]: [],
|
||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||
});
|
||||
|
||||
jest.mock("../../../src/utils/DMRoomMap", () => {
|
||||
const mock = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
@@ -72,7 +88,21 @@ jest.mock("../../../src/utils/DMRoomMap", () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../../src/stores/WidgetStore");
|
||||
jest.mock("../../../src/stores/WidgetStore", () => {
|
||||
// This mock needs to use a real EventEmitter; require is the only way to import that in a hoisted block
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const EventEmitter = require("events");
|
||||
const apps: IApp[] = [];
|
||||
const instance = new (class extends EventEmitter {
|
||||
getApps() {
|
||||
return apps;
|
||||
}
|
||||
addVirtualWidget(app: IApp) {
|
||||
apps.push(app);
|
||||
}
|
||||
})();
|
||||
return { instance };
|
||||
});
|
||||
jest.mock("../../../src/stores/widgets/WidgetLayoutStore");
|
||||
|
||||
describe("RoomViewStore", function () {
|
||||
@@ -82,10 +112,12 @@ describe("RoomViewStore", function () {
|
||||
// we need to change the alias to ensure cache misses as the cache exists
|
||||
// through all tests.
|
||||
let alias = "#somealias2:aser.ver";
|
||||
const getRooms = jest.fn();
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
joinRoom: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
getRoomIdForAlias: jest.fn(),
|
||||
getRooms,
|
||||
isGuest: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
@@ -97,9 +129,18 @@ describe("RoomViewStore", function () {
|
||||
knockRoom: jest.fn(),
|
||||
leave: jest.fn(),
|
||||
setRoomAccountData: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
matrixRTC: new (class extends EventEmitter {
|
||||
getRoomSession() {
|
||||
return new (class extends EventEmitter {
|
||||
memberships = [];
|
||||
})();
|
||||
}
|
||||
})(),
|
||||
});
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
const room2 = new Room(roomId2, mockClient, userId);
|
||||
getRooms.mockReturnValue([room, room2]);
|
||||
|
||||
const viewCall = async (): Promise<void> => {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
@@ -301,6 +342,7 @@ describe("RoomViewStore", function () {
|
||||
});
|
||||
|
||||
it("when viewing a call without a broadcast, it should not raise an error", async () => {
|
||||
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
|
||||
await viewCall();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user