mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-07 15:08:24 +00:00
Ensure correct focus configuration for Element Call before allowing users to call. (#31490)
* fixup type * Validate Element Call foci config * revert changes * Split out logic to CallStore so we don't repeat checks. * Refactor to use CallStore so we only fetch once. * Add test for useRoomCall * lint * Ensure we enable MatrixRTC when configuring element call. * fix test * Update @element-hq/element-web-playwright-common to 2.2.2 and enable matrix rtc * lint * Ensure call is configured for header test * type * Improve coverage * Update based on feedback * fix type
This commit is contained in:
@@ -3752,7 +3752,7 @@ foreground-child@^2.0.0:
|
|||||||
cross-spawn "^7.0.0"
|
cross-spawn "^7.0.0"
|
||||||
signal-exit "^3.0.2"
|
signal-exit "^3.0.2"
|
||||||
|
|
||||||
foreground-child@^3.1.0:
|
foreground-child@^3.1.0, foreground-child@^3.3.1:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
|
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
|
||||||
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
|
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
|
||||||
@@ -3902,6 +3902,18 @@ glob@^10.0.0, glob@^10.3.10:
|
|||||||
package-json-from-dist "^1.0.0"
|
package-json-from-dist "^1.0.0"
|
||||||
path-scurry "^1.11.1"
|
path-scurry "^1.11.1"
|
||||||
|
|
||||||
|
glob@^11.1.0:
|
||||||
|
version "11.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-11.1.0.tgz#4f826576e4eb99c7dad383793d2f9f08f67e50a6"
|
||||||
|
integrity sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==
|
||||||
|
dependencies:
|
||||||
|
foreground-child "^3.3.1"
|
||||||
|
jackspeak "^4.1.1"
|
||||||
|
minimatch "^10.1.1"
|
||||||
|
minipass "^7.1.2"
|
||||||
|
package-json-from-dist "^1.0.0"
|
||||||
|
path-scurry "^2.0.0"
|
||||||
|
|
||||||
glob@^13.0.0:
|
glob@^13.0.0:
|
||||||
version "13.0.0"
|
version "13.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3"
|
||||||
@@ -4403,6 +4415,13 @@ jackspeak@^3.1.2:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@pkgjs/parseargs" "^0.11.0"
|
"@pkgjs/parseargs" "^0.11.0"
|
||||||
|
|
||||||
|
jackspeak@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae"
|
||||||
|
integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==
|
||||||
|
dependencies:
|
||||||
|
"@isaacs/cliui" "^8.0.2"
|
||||||
|
|
||||||
jest-changed-files@30.2.0:
|
jest-changed-files@30.2.0:
|
||||||
version "30.2.0"
|
version "30.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.2.0.tgz#602266e478ed554e1e1469944faa7efd37cee61c"
|
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.2.0.tgz#602266e478ed554e1e1469944faa7efd37cee61c"
|
||||||
|
|||||||
@@ -82,6 +82,22 @@ async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "n
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
synapseConfig: {
|
||||||
|
experimental_features: {
|
||||||
|
msc4143_enabled: true,
|
||||||
|
},
|
||||||
|
matrix_rtc: {
|
||||||
|
transports: [
|
||||||
|
{
|
||||||
|
type: "livekit",
|
||||||
|
livekit_service_url: "https://example.org/can-be-anything",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("Element Call", () => {
|
test.describe("Element Call", () => {
|
||||||
test.use({
|
test.use({
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useFeatureEnabled, useSettingValue } from "../useSettings";
|
import { useFeatureEnabled, useSettingValue } from "../useSettings";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
|
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
|
||||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
||||||
import { useWidgets } from "../../utils/WidgetUtils";
|
import { useWidgets } from "../../utils/WidgetUtils";
|
||||||
import { WidgetType } from "../../widgets/WidgetType";
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
import { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
import { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
||||||
@@ -37,6 +38,9 @@ import { type InteractionName } from "../../PosthogTrackers";
|
|||||||
import { ElementCallMemberEventType } from "../../call-types";
|
import { ElementCallMemberEventType } from "../../call-types";
|
||||||
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
|
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
|
||||||
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||||
|
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||||
|
|
||||||
|
const logger = rootLogger.getChild("useRoomCall");
|
||||||
|
|
||||||
export enum PlatformCallType {
|
export enum PlatformCallType {
|
||||||
ElementCall,
|
ElementCall,
|
||||||
@@ -67,6 +71,8 @@ export const getPlatformCallTypeProps = (
|
|||||||
label: _t("voip|legacy_call"),
|
label: _t("voip|legacy_call"),
|
||||||
analyticsName: "WebVoipOptionLegacy",
|
analyticsName: "WebVoipOptionLegacy",
|
||||||
};
|
};
|
||||||
|
default:
|
||||||
|
throw Error(`Unexpected PlatformCallType ${platformCallType}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,10 +116,22 @@ export const useRoomCall = (
|
|||||||
return SdkConfig.get("element_call").use_exclusively;
|
return SdkConfig.get("element_call").use_exclusively;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const serverIsConfiguredForElementCall = CallStore.instance
|
||||||
|
.getConfiguredRTCTransports()
|
||||||
|
.some((s) => s.type === "livekit" && s.livekit_service_url);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (useElementCallExclusively && !serverIsConfiguredForElementCall) {
|
||||||
|
logger.warn(
|
||||||
|
"Element Call is configured to be used exclusively, but the server is not configured with a transport",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [useElementCallExclusively, serverIsConfiguredForElementCall]);
|
||||||
|
|
||||||
const hasLegacyCall = useEventEmitterState(
|
const hasLegacyCall = useEventEmitterState(
|
||||||
LegacyCallHandler.instance,
|
SdkContextClass.instance.legacyCallHandler,
|
||||||
LegacyCallHandlerEvent.CallsChanged,
|
LegacyCallHandlerEvent.CallsChanged,
|
||||||
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
|
() => SdkContextClass.instance.legacyCallHandler.getCallForRoom(room.roomId) !== null,
|
||||||
);
|
);
|
||||||
// settings
|
// settings
|
||||||
const widgets = useWidgets(room);
|
const widgets = useWidgets(room);
|
||||||
@@ -143,11 +161,13 @@ export const useRoomCall = (
|
|||||||
// room
|
// room
|
||||||
const memberCount = useRoomMemberCount(room);
|
const memberCount = useRoomMemberCount(room);
|
||||||
|
|
||||||
const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [
|
const [mayEditWidgets, mayCreateElementCallState] = useRoomState(room, () => [
|
||||||
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
||||||
room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client),
|
room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const mayCreateElementCalls = mayCreateElementCallState && serverIsConfiguredForElementCall;
|
||||||
|
|
||||||
// The options provided to the RoomHeader.
|
// The options provided to the RoomHeader.
|
||||||
// If there are multiple options, the user will be prompted to choose.
|
// If there are multiple options, the user will be prompted to choose.
|
||||||
const callOptions = useMemo((): PlatformCallType[] => {
|
const callOptions = useMemo((): PlatformCallType[] => {
|
||||||
@@ -221,6 +241,10 @@ export const useRoomCall = (
|
|||||||
if (!callOptions.includes(PlatformCallType.LegacyCall) && !mayCreateElementCalls && !mayEditWidgets) {
|
if (!callOptions.includes(PlatformCallType.LegacyCall) && !mayCreateElementCalls && !mayEditWidgets) {
|
||||||
return State.NoPermission;
|
return State.NoPermission;
|
||||||
}
|
}
|
||||||
|
// Catch-all for just not having any call options available.
|
||||||
|
if (!callOptions.length) {
|
||||||
|
return State.NoPermission;
|
||||||
|
}
|
||||||
return State.NoCall;
|
return State.NoCall;
|
||||||
}, [
|
}, [
|
||||||
callOptions,
|
callOptions,
|
||||||
|
|||||||
+37
-2
@@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { type MatrixRTCSession, MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc";
|
import { type MatrixRTCSession, MatrixRTCSessionManagerEvents, type Transport } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
import { MatrixError, type EmptyObject, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { UPDATE_EVENT } from "./AsyncStore";
|
import { UPDATE_EVENT } from "./AsyncStore";
|
||||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
@@ -35,6 +35,8 @@ export class CallStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly configuredMatrixRTCTransports = new Set<Transport>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super(defaultDispatcher);
|
super(defaultDispatcher);
|
||||||
this.setMaxListeners(100); // One for each RoomTile
|
this.setMaxListeners(100); // One for each RoomTile
|
||||||
@@ -44,8 +46,36 @@ export class CallStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch transports used by MatrixRTC services, such as Element Call.
|
||||||
|
* This function is called once during Store startup which means we don't refetch
|
||||||
|
* transports every time we need to check for Element Call support.
|
||||||
|
*/
|
||||||
|
protected async fetchTransports(): Promise<void> {
|
||||||
|
if (!this.matrixClient) return;
|
||||||
|
// Prefer checking the proper endpoint for transports.
|
||||||
|
try {
|
||||||
|
const transports = await this.matrixClient._unstable_getRTCTransports();
|
||||||
|
transports.forEach((t) => this.configuredMatrixRTCTransports.add(t));
|
||||||
|
} catch (ex) {
|
||||||
|
// Expected, MSC not implemented.
|
||||||
|
if (ex instanceof MatrixError === false || ex.errcode !== "M_NOT_FOUND") {
|
||||||
|
logger.warn("Unexpected error when trying to fetch RTC transports", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// See https://github.com/matrix-org/matrix-spec-proposals/blob/d61969a9a3696b6c54d7987b1643b5bc03670927/proposals/4143-matrix-rtc.md#discovery-of-foci-using-well-knownmatrixclient
|
||||||
|
// This well-known option has since been removed from the spec but is still widely deployed.
|
||||||
|
await this.matrixClient.waitForClientWellKnown();
|
||||||
|
const foci = this.matrixClient.getClientWellKnown()?.["org.matrix.msc4143.rtc_foci"];
|
||||||
|
if (Array.isArray(foci)) {
|
||||||
|
foci.forEach((foci) => this.configuredMatrixRTCTransports.add(foci));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected async onReady(): Promise<any> {
|
protected async onReady(): Promise<any> {
|
||||||
if (!this.matrixClient) return;
|
if (!this.matrixClient) return;
|
||||||
|
// Fetch transports, but don't await the result.
|
||||||
|
void this.fetchTransports();
|
||||||
// We assume that the calls present in a room are a function of room
|
// We assume that the calls present in a room are a function of room
|
||||||
// widgets and group calls, so we initialize the room map here and then
|
// widgets and group calls, so we initialize the room map here and then
|
||||||
// update it whenever those change
|
// update it whenever those change
|
||||||
@@ -81,6 +111,7 @@ export class CallStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
this.callListeners.clear();
|
this.callListeners.clear();
|
||||||
this.calls.clear();
|
this.calls.clear();
|
||||||
this._connectedCalls.clear();
|
this._connectedCalls.clear();
|
||||||
|
this.configuredMatrixRTCTransports.clear();
|
||||||
|
|
||||||
this.matrixClient?.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, this.onRTCSessionStart);
|
this.matrixClient?.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, this.onRTCSessionStart);
|
||||||
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets);
|
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets);
|
||||||
@@ -187,6 +218,10 @@ export class CallStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getConfiguredRTCTransports(): Transport[] {
|
||||||
|
return [...this.configuredMatrixRTCTransports];
|
||||||
|
}
|
||||||
|
|
||||||
private onRTCSessionStart = (roomId: string, session: MatrixRTCSession): void => {
|
private onRTCSessionStart = (roomId: string, session: MatrixRTCSession): void => {
|
||||||
this.updateRoom(session.room);
|
this.updateRoom(session.room);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export function createTestClient(): MatrixClient {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
getAccountDataFromServer: jest.fn(),
|
getAccountDataFromServer: jest.fn(),
|
||||||
|
|
||||||
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
|
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
|
||||||
setAccountData: jest.fn(),
|
setAccountData: jest.fn(),
|
||||||
deleteAccountData: jest.fn(),
|
deleteAccountData: jest.fn(),
|
||||||
@@ -310,7 +311,7 @@ export function createTestClient(): MatrixClient {
|
|||||||
_unstable_sendScheduledDelayedEvent: jest.fn(),
|
_unstable_sendScheduledDelayedEvent: jest.fn(),
|
||||||
_unstable_sendStickyEvent: jest.fn(),
|
_unstable_sendStickyEvent: jest.fn(),
|
||||||
_unstable_sendStickyDelayedEvent: jest.fn(),
|
_unstable_sendStickyDelayedEvent: jest.fn(),
|
||||||
|
_unstable_getRTCTransports: jest.fn(),
|
||||||
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
|
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
|
||||||
setDeviceVerified: jest.fn(),
|
setDeviceVerified: jest.fn(),
|
||||||
joinRoom: jest.fn(),
|
joinRoom: jest.fn(),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Room,
|
Room,
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
|
type MatrixClient,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||||
@@ -37,7 +38,7 @@ import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycl
|
|||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { filterConsole, stubClient } from "../../../../../test-utils";
|
import { filterConsole, setupAsyncStoreWithClient, stubClient } from "../../../../../test-utils";
|
||||||
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||||
@@ -85,12 +86,14 @@ describe("RoomHeader", () => {
|
|||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
let roomContext: RoomContextType;
|
let roomContext: RoomContextType;
|
||||||
|
|
||||||
function getWrapper(): RenderOptions {
|
function getWrapper(): RenderOptions {
|
||||||
return {
|
return {
|
||||||
wrapper: ({ children }) => (
|
wrapper: ({ children }) => (
|
||||||
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
|
<MatrixClientContext.Provider value={client}>
|
||||||
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
|
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
),
|
),
|
||||||
@@ -98,8 +101,8 @@ describe("RoomHeader", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
stubClient();
|
client = stubClient();
|
||||||
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org", {
|
room = new Room(ROOM_ID, client, "@alice:example.org", {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
});
|
});
|
||||||
DMRoomMap.setShared({
|
DMRoomMap.setShared({
|
||||||
@@ -405,12 +408,18 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("group call enabled", () => {
|
describe("group call enabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
SdkConfig.put({
|
SdkConfig.put({
|
||||||
features: {
|
features: {
|
||||||
feature_group_calls: true,
|
feature_group_calls: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Enable Element Call
|
||||||
|
client._unstable_getRTCTransports = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue([{ type: "livekit", livekit_service_url: "https://example.org" }]);
|
||||||
|
// And ensure the CallStore has the transports configured.
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { renderHook, waitFor } from "jest-matrix-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { PlatformCallType, useRoomCall } from "../../../src/hooks/room/useRoomCall";
|
||||||
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mkRoom,
|
||||||
|
mockClientMethodsRooms,
|
||||||
|
mockClientMethodsServer,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
MockEventEmitter,
|
||||||
|
setupAsyncStoreWithClient,
|
||||||
|
} from "../../test-utils";
|
||||||
|
import { ScopedRoomContextProvider } from "../../../src/contexts/ScopedRoomContext";
|
||||||
|
import RoomContext, { type RoomContextType } from "../../../src/contexts/RoomContext";
|
||||||
|
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
|
||||||
|
import type LegacyCallHandler from "../../../src/LegacyCallHandler";
|
||||||
|
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||||
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
|
import { CallStore } from "../../../src/stores/CallStore";
|
||||||
|
|
||||||
|
describe("useRoomCall", () => {
|
||||||
|
const client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(),
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
...mockClientMethodsRooms(),
|
||||||
|
matrixRTC: new MockEventEmitter(),
|
||||||
|
_unstable_getRTCTransports: jest.fn().mockResolvedValue([]),
|
||||||
|
getCrypto: () => null,
|
||||||
|
});
|
||||||
|
const room = mkRoom(client, "!test-room");
|
||||||
|
// Create a stable room context for this test
|
||||||
|
const mockRoomViewStore = {
|
||||||
|
isViewingCall: jest.fn().mockReturnValue(false),
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const roomContext = {
|
||||||
|
...RoomContext,
|
||||||
|
roomId: room.roomId,
|
||||||
|
roomViewStore: mockRoomViewStore,
|
||||||
|
} as unknown as RoomContextType;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const callHandler = {
|
||||||
|
getCallForRoom: jest.fn().mockReturnValue(null),
|
||||||
|
isCallSidebarShown: jest.fn().mockReturnValue(true),
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
};
|
||||||
|
jest.spyOn(SdkContextClass.instance, "legacyCallHandler", "get").mockReturnValue(
|
||||||
|
callHandler as unknown as LegacyCallHandler,
|
||||||
|
);
|
||||||
|
const origGetValue = SettingsStore.getValue;
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, ...params): any => {
|
||||||
|
if (name === "feature_group_calls") return true;
|
||||||
|
return origGetValue(name, ...params);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
return renderHook(() => useRoomCall(room), {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<MatrixClientContextProvider client={client}>
|
||||||
|
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
|
||||||
|
</MatrixClientContextProvider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Element Call focus detection", () => {
|
||||||
|
it("Blocks Element Call if required foci are not configured", async () => {
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
|
const { result } = render();
|
||||||
|
await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]));
|
||||||
|
});
|
||||||
|
it("Blocks Element Call if transport foci are the wrong type", async () => {
|
||||||
|
client._unstable_getRTCTransports.mockResolvedValue([{ type: "anything-else" }]);
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
|
const { result } = render();
|
||||||
|
await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]));
|
||||||
|
});
|
||||||
|
it("Blocks Element Call if well-known foci are the wrong type", async () => {
|
||||||
|
client.getClientWellKnown.mockReturnValue({
|
||||||
|
"org.matrix.msc4143.rtc_foci": {
|
||||||
|
type: "anything-else",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
|
const { result } = render();
|
||||||
|
await waitFor(() => expect(result.current.callOptions).toEqual([PlatformCallType.LegacyCall]));
|
||||||
|
});
|
||||||
|
it("Allows Element Call if foci is provided via getRTCTransports", async () => {
|
||||||
|
client._unstable_getRTCTransports.mockResolvedValue([
|
||||||
|
{ type: "livekit", livekit_service_url: "https://example.org" },
|
||||||
|
]);
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
|
|
||||||
|
const { result } = render();
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("Allows Element Call if foci is provided via .well-known", async () => {
|
||||||
|
client.getClientWellKnown.mockReturnValue({
|
||||||
|
"org.matrix.msc4143.rtc_foci": {
|
||||||
|
type: "livekit",
|
||||||
|
livekit_service_url: "https://example.org",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
|
const { result } = render();
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(result.current.callOptions).toEqual([PlatformCallType.ElementCall, PlatformCallType.LegacyCall]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type CallMembership, MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc";
|
import { type CallMembership, MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type MockedObject } from "jest-mock";
|
||||||
|
|
||||||
import { ElementCall } from "../../../src/models/Call";
|
import { ElementCall } from "../../../src/models/Call";
|
||||||
import { CallStore } from "../../../src/stores/CallStore";
|
import { CallStore } from "../../../src/stores/CallStore";
|
||||||
@@ -16,11 +18,22 @@ import {
|
|||||||
enableCalls,
|
enableCalls,
|
||||||
} from "../../test-utils";
|
} from "../../test-utils";
|
||||||
|
|
||||||
enableCalls();
|
describe("CallStore", () => {
|
||||||
|
let client: MockedObject<MatrixClient>;
|
||||||
|
let room: Room;
|
||||||
|
beforeEach(() => {
|
||||||
|
enableCalls();
|
||||||
|
const res = setUpClientRoomAndStores();
|
||||||
|
client = res.client;
|
||||||
|
room = res.room;
|
||||||
|
});
|
||||||
|
|
||||||
test("CallStore constructs one call for one MatrixRTC session", () => {
|
afterEach(() => {
|
||||||
const { client, room } = setUpClientRoomAndStores();
|
cleanUpClientRoomAndStores(client, room);
|
||||||
try {
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("constructs one call for one MatrixRTC session", () => {
|
||||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
const getSpy = jest.spyOn(ElementCall, "get");
|
const getSpy = jest.spyOn(ElementCall, "get");
|
||||||
|
|
||||||
@@ -32,7 +45,25 @@ test("CallStore constructs one call for one MatrixRTC session", () => {
|
|||||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(getSpy).toHaveReturnedWith(expect.any(ElementCall));
|
expect(getSpy).toHaveReturnedWith(expect.any(ElementCall));
|
||||||
expect(CallStore.instance.getCall(room.roomId)).not.toBe(null);
|
expect(CallStore.instance.getCall(room.roomId)).not.toBe(null);
|
||||||
} finally {
|
expect(CallStore.instance.getConfiguredRTCTransports()).toHaveLength(0);
|
||||||
cleanUpClientRoomAndStores(client, room);
|
});
|
||||||
}
|
it("calculates RTC transports with both modern and legacy endpoints", async () => {
|
||||||
|
client._unstable_getRTCTransports.mockResolvedValue([
|
||||||
|
{ type: "type-a", some_data: "value" },
|
||||||
|
{ type: "type-b", some_data: "foo" },
|
||||||
|
]);
|
||||||
|
client.getClientWellKnown.mockReturnValue({
|
||||||
|
"org.matrix.msc4143.rtc_foci": [
|
||||||
|
{ type: "type-c", other_data: "bar" },
|
||||||
|
{ type: "type-d", other_data: "baz" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
|
expect(CallStore.instance.getConfiguredRTCTransports()).toEqual([
|
||||||
|
{ type: "type-a", some_data: "value" },
|
||||||
|
{ type: "type-b", some_data: "foo" },
|
||||||
|
{ type: "type-c", other_data: "bar" },
|
||||||
|
{ type: "type-d", other_data: "baz" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ describe("RoomViewStore", function () {
|
|||||||
leave: jest.fn(),
|
leave: jest.fn(),
|
||||||
setRoomAccountData: jest.fn(),
|
setRoomAccountData: jest.fn(),
|
||||||
getAccountData: jest.fn(),
|
getAccountData: jest.fn(),
|
||||||
|
waitForClientWellKnown: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||||
matrixRTC: new (class extends EventEmitter {
|
matrixRTC: new (class extends EventEmitter {
|
||||||
getRoomSession() {
|
getRoomSession() {
|
||||||
return new (class extends EventEmitter {
|
return new (class extends EventEmitter {
|
||||||
|
|||||||
Reference in New Issue
Block a user