From 8bb4d44532dc837c459c0a5a2de0c502c3fddce5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 Mar 2025 20:34:32 +0000 Subject: [PATCH] Bundle Element Call with Element Web packages (#29309) * Embed Element Call into Element Web packages Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Pass rageshakeSubmitUrl & posthogApiHost to EC widget Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use @vector-im/element-call-embedded * Only pass posthog params to EC if Analytics is enabled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test mock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update EC params to match https://github.com/element-hq/element-call/pull/3089 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update to latest element-call package Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * yarn.lock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update to element-call-embedded@ v0.9.0-rc.1 * Gate Sentry params behind analytics consent Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update to element-call-embedded v0.9.0-rc.4 * Update Element Call embedded to 0.9.0 release Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Hugh Nimmo-Smith --- .github/workflows/static_analysis.yaml | 1 + docs/config.md | 2 - knip.ts | 2 + package.json | 1 + src/IConfigOptions.ts | 1 - src/SdkConfig.ts | 1 - .../views/dialogs/DevtoolsDialog.tsx | 2 + .../views/elements/SettingsField.tsx | 59 +++++++++++++++++++ src/i18n/strings/en_EN.json | 3 + src/models/Call.ts | 58 +++++++++++++----- src/settings/Settings.tsx | 8 +++ src/stores/widgets/StopGapWidgetDriver.ts | 6 +- .../DevtoolsDialog-test.tsx.snap | 25 ++++++++ .../views/elements/SettingsField-test.tsx | 36 +++++++++++ .../__snapshots__/SettingsField-test.tsx.snap | 31 ++++++++++ test/unit-tests/models/Call-test.ts | 37 ++++++++++-- test/unit-tests/stores/RoomViewStore-test.ts | 2 + .../widgets/StopGapWidgetDriver-test.ts | 4 +- .../widgets/WidgetPermissionStore-test.ts | 5 +- .../utils/device/clientInformation-test.ts | 2 +- webpack.config.js | 6 ++ yarn.lock | 5 ++ 22 files changed, 264 insertions(+), 33 deletions(-) create mode 100644 src/components/views/elements/SettingsField.tsx create mode 100644 test/unit-tests/components/views/elements/SettingsField-test.tsx create mode 100644 test/unit-tests/components/views/elements/__snapshots__/SettingsField-test.tsx.snap diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index ee731b3ac3..8b15fdfe76 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -51,6 +51,7 @@ jobs: error|invalid_json error|misconfigured welcome_to_element + devtools|settings|elementCallUrl rethemendex_lint: name: "Rethemendex Check" diff --git a/docs/config.md b/docs/config.md index 9ae9f21254..f1ced14fd2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -384,8 +384,6 @@ The VoIP and Jitsi options are: 5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed at any time without notice. 6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys: - - `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental - and may be removed at any time without notice. Defaults to `https://call.element.io`. - `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`. - `participant_limit`: The maximum number of users who can join a call; if diff --git a/knip.ts b/knip.ts index 0188e096e5..e129c11481 100644 --- a/knip.ts +++ b/knip.ts @@ -40,6 +40,8 @@ export default { // Used by webpack "process", "util", + // Embedded into webapp + "@element-hq/element-call-embedded", ], ignoreBinaries: [ // Used in scripts & workflows diff --git a/package.json b/package.json index 1adc4eee41..8ccf6d4e77 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", + "@element-hq/element-call-embedded": "0.9.0", "@element-hq/element-web-playwright-common": "^1.1.5", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 67b7515eac..8b88a18075 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -117,7 +117,6 @@ export interface IConfigOptions { obey_asserted_identity?: boolean; // MSC3086 }; element_call: { - url?: string; guest_spa_url?: string; use_exclusively?: boolean; participant_limit?: number; diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index a41b890b19..9576aa209e 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly = { preferred_domain: "meet.element.io", }, element_call: { - url: "https://call.element.io", use_exclusively: false, participant_limit: 8, brand: "Element Call", diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index a95f9da0ab..9b8b28ea7f 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -25,6 +25,7 @@ import ServerInfo from "./devtools/ServerInfo"; import CopyableText from "../elements/CopyableText"; import RoomNotifications from "./devtools/RoomNotifications"; import { Crypto } from "./devtools/Crypto"; +import SettingsField from "../elements/SettingsField.tsx"; enum Category { Room, @@ -101,6 +102,7 @@ const DevtoolsDialog: React.FC = ({ roomId, threadRootId, onFinished }) + ); diff --git a/src/components/views/elements/SettingsField.tsx b/src/components/views/elements/SettingsField.tsx new file mode 100644 index 0000000000..d46b5b0f32 --- /dev/null +++ b/src/components/views/elements/SettingsField.tsx @@ -0,0 +1,59 @@ +/* +Copyright 2025 New Vector 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 React, { type ChangeEvent, type JSX, useCallback, useState } from "react"; +import { EditInPlace } from "@vector-im/compound-web"; + +import SettingsStore from "../../../settings/SettingsStore"; +import { _t } from "../../../languageHandler"; +import { type SettingLevel } from "../../../settings/SettingLevel"; +import { type StringSettingKey } from "../../../settings/Settings"; + +interface Props { + settingKey: StringSettingKey; + level: SettingLevel; + roomId?: string; // for per-room settings + label?: string; + isExplicit?: boolean; + onChange?(value: string): void; +} + +const SettingsField = ({ settingKey, level, roomId, isExplicit, label, onChange: _onSave }: Props): JSX.Element => { + const settingsValue = SettingsStore.getValueAt(level, settingKey, roomId, isExplicit); + const [value, setValue] = useState(settingsValue); + const [busy, setBusy] = useState(false); + + const onChange = useCallback((e: ChangeEvent) => { + setValue(e.target.value); + }, []); + const onCancel = useCallback(() => { + setValue(settingsValue); + }, [settingsValue]); + const onSave = useCallback(async () => { + setBusy(true); + await SettingsStore.setValue(settingKey, roomId ?? null, level, value); + setBusy(false); + _onSave?.(value); + }, [level, roomId, settingKey, value, _onSave]); + + return ( + + ); +}; + +export default SettingsField; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b7339fd9f4..ce1c2e503b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -846,6 +846,9 @@ "setting_colon": "Setting:", "setting_definition": "Setting definition:", "setting_id": "Setting ID", + "settings": { + "elementCallUrl": "Element Call URL" + }, "settings_explorer": "Settings explorer", "show_hidden_events": "Show hidden events in timeline", "spaces": { diff --git a/src/models/Call.ts b/src/models/Call.ts index 3497dc01c6..60ff2b2ffa 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -32,7 +32,6 @@ import { import type EventEmitter from "events"; import type { IApp } from "../stores/WidgetStore"; -import SdkConfig, { DEFAULTS } from "../SdkConfig"; import SettingsStore from "../settings/SettingsStore"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler"; import { timeout } from "../utils/promise"; @@ -43,12 +42,13 @@ import WidgetStore from "../stores/WidgetStore"; import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widgets/WidgetMessagingStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore"; import { getCurrentLanguage } from "../languageHandler"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { Anonymity, PosthogAnalytics } from "../PosthogAnalytics"; import { UPDATE_EVENT } from "../stores/AsyncStore"; import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers"; import { isVideoRoom } from "../utils/video-rooms"; import { FontWatcher } from "../settings/watchers/FontWatcher"; import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types"; +import SdkConfig from "../SdkConfig.ts"; const TIMEOUT_MS = 16000; @@ -676,14 +676,12 @@ export class ElementCall extends Call { } private static generateWidgetUrl(client: MatrixClient, roomId: string): URL { - const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE); - // The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget. - // We really don't want the same analyticID's for the EC and EW posthog instances (Data on posthog should be limited/anonymized as much as possible). - // This is prohibited in EC where a hashed version of the analyticsID is used for the actual posthog identification. - // We can pass the raw EW analyticsID here since we need to trust EC with not sending sensitive data to posthog (EC has access to more sensible data than the analyticsID e.g. the username) - const analyticsID: string = accountAnalyticsData?.getContent().pseudonymousAnalyticsOptIn - ? accountAnalyticsData?.getContent().id - : ""; + const baseUrl = window.location.href; + let url = new URL("./widgets/element-call/index.html#", baseUrl); // this strips hash fragment from baseUrl + + const elementCallUrl = SettingsStore.getValue("Developer.elementCallUrl"); + if (elementCallUrl) url = new URL(elementCallUrl); + // Splice together the Element Call URL for this call const params = new URLSearchParams({ embed: "true", // We're embedding EC within another application @@ -700,12 +698,44 @@ export class ElementCall extends Call { lang: getCurrentLanguage().replace("_", "-"), fontScale: (FontWatcher.getRootFontSize() / FontWatcher.getBrowserDefaultFontSize()).toString(), theme: "$org.matrix.msc2873.client_theme", - analyticsID, }); - if (SettingsStore.getValue("fallbackICEServerAllowed")) params.append("allowIceFallback", "true"); - if (SettingsStore.getValue("feature_allow_screen_share_only_mode")) + const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url"); + if (rageshakeSubmitUrl) { + params.append("rageshakeSubmitUrl", rageshakeSubmitUrl); + } + + const posthogConfig = SdkConfig.get("posthog"); + if (posthogConfig && PosthogAnalytics.instance.getAnonymity() !== Anonymity.Disabled) { + const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE)?.getContent(); + // The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget. + // We really don't want the same analyticID's for the EC and EW posthog instances (Data on posthog should be limited/anonymized as much as possible). + // This is prohibited in EC where a hashed version of the analyticsID is used for the actual posthog identification. + // We can pass the raw EW analyticsID here since we need to trust EC with not sending sensitive data to posthog (EC has access to more sensible data than the analyticsID e.g. the username) + const analyticsID: string = accountAnalyticsData?.pseudonymousAnalyticsOptIn + ? accountAnalyticsData?.id + : ""; + + params.append("analyticsID", analyticsID); // Legacy, deprecated in favour of posthogUserId + params.append("posthogUserId", analyticsID); + params.append("posthogApiHost", posthogConfig.api_host); + params.append("posthogApiKey", posthogConfig.project_api_key); + + // We gate passing sentry behind analytics consent as EC shares data automatically without user-consent, + // unlike EW where data is shared upon an intentional user action (rageshake). + const sentryConfig = SdkConfig.get("sentry"); + if (sentryConfig) { + params.append("sentryDsn", sentryConfig.dsn); + params.append("sentryEnvironment", sentryConfig.environment ?? ""); + } + } + + if (SettingsStore.getValue("fallbackICEServerAllowed")) { + params.append("allowIceFallback", "true"); + } + if (SettingsStore.getValue("feature_allow_screen_share_only_mode")) { params.append("allowVoipWithNoMedia", "true"); + } // Set custom fonts if (SettingsStore.getValue("useSystemFont")) { @@ -720,8 +750,6 @@ export class ElementCall extends Call { .forEach((font) => params.append("font", font)); } - const url = new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url!); - url.pathname = "/room"; const replacedUrl = params.toString().replace(/%24/g, "$"); url.hash = `#?${replacedUrl}`; return url; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 6984932fd9..aee9141c29 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -349,11 +349,13 @@ export interface Settings { "Electron.alwaysShowMenuBar": IBaseSetting; "Electron.showTrayIcon": IBaseSetting; "Electron.enableHardwareAcceleration": IBaseSetting; + "Developer.elementCallUrl": IBaseSetting; } export type SettingKey = keyof Settings; export type FeatureSettingKey = Assignable; export type BooleanSettingKey = Assignable> | FeatureSettingKey; +export type StringSettingKey = Assignable>; export const SETTINGS: Settings = { "feature_video_rooms": { @@ -1384,4 +1386,10 @@ export const SETTINGS: Settings = { displayName: _td("settings|preferences|enable_hardware_acceleration"), default: true, }, + + "Developer.elementCallUrl": { + supportedLevels: [SettingLevel.DEVICE], + displayName: _td("devtools|settings|elementCallUrl"), + default: "", + }, }; diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 988cbae8f8..31d1d6edac 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -48,7 +48,6 @@ import { WidgetLifecycle, } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; import { iterableDiff, iterableIntersection } from "../../utils/iterables"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import Modal from "../../Modal"; @@ -116,10 +115,7 @@ export class StopGapWidgetDriver extends WidgetDriver { // Auto-approve the legacy visibility capability. We send it regardless of capability. // Widgets don't technically need to request this capability, but Scalar still does. this.allowedCapabilities.add("visibility"); - } else if ( - virtual && - new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url!).origin === this.forWidget.origin - ) { + } else if (virtual && WidgetType.CALL.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) { // This is a trusted Element Call widget that we control this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen); this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers); diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap index dd1deb8696..b468763bbe 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap @@ -190,6 +190,31 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = ` /> +
+
+ +
+ +
+
+
", () => { + it("should render with the default label", () => { + const component = render(); + + expect(screen.getByText("Element Call URL")).toBeTruthy(); + expect(component.asFragment()).toMatchSnapshot(); + }); + + it("should call onChange when saving a change", async () => { + const fn = jest.fn(); + render(); + + const input = screen.getByRole("textbox"); + await userEvent.type(input, "https://call.element.dev"); + expect(input).toHaveValue("https://call.element.dev"); + + screen.getByLabelText("Save").click(); + await waitFor(() => { + expect(fn).toHaveBeenCalledWith("https://call.element.dev"); + }); + }); +}); diff --git a/test/unit-tests/components/views/elements/__snapshots__/SettingsField-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/SettingsField-test.tsx.snap new file mode 100644 index 0000000000..565b199ab8 --- /dev/null +++ b/test/unit-tests/components/views/elements/__snapshots__/SettingsField-test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render with the default label 1`] = ` + +
+
+ +
+ +
+
+
+
+`; diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index c94792647e..c8f9d50334 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -49,8 +49,9 @@ import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagin import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../src/stores/ActiveWidgetStore"; import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions"; import SettingsStore from "../../../src/settings/SettingsStore"; -import { PosthogAnalytics } from "../../../src/PosthogAnalytics"; +import { Anonymity, PosthogAnalytics } from "../../../src/PosthogAnalytics"; import { type SettingKey } from "../../../src/settings/Settings.tsx"; +import SdkConfig from "../../../src/SdkConfig.ts"; jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ [MediaDeviceKindEnum.AudioInput]: [ @@ -664,6 +665,7 @@ describe("ElementCall", () => { beforeEach(() => { jest.useFakeTimers(); ({ client, room, alice } = setUpClientRoomAndStores()); + SdkConfig.reset(); }); afterEach(() => { @@ -683,6 +685,23 @@ describe("ElementCall", () => { Call.get(room)?.destroy(); }); + it("should use element call URL from developer settings if present", async () => { + const originalGetValue = SettingsStore.getValue; + SettingsStore.getValue = (name: SettingKey, roomId: string | null = null, excludeDefault = false): any => { + if (name === "Developer.elementCallUrl") { + return "https://call.element.dev"; + } + return excludeDefault + ? originalGetValue(name, roomId, excludeDefault) + : originalGetValue(name, roomId, excludeDefault); + }; + await ElementCall.create(room); + const call = ElementCall.get(room); + expect(call?.widget.url.startsWith("https://call.element.dev/")).toBeTruthy(); + SettingsStore.getValue = originalGetValue; + call?.destroy(); + }); + it("finds ongoing calls that are created by the session manager", async () => { // There is an existing session created by another user in this room. client.matrixRTC.getRoomSession.mockReturnValue({ @@ -758,7 +777,14 @@ describe("ElementCall", () => { SettingsStore.getValue = originalGetValue; }); - it("passes analyticsID through widget URL", async () => { + it("passes analyticsID and posthog params through widget URL", async () => { + SdkConfig.put({ + posthog: { + api_host: "https://posthog", + project_api_key: "DEADBEEF", + }, + }); + jest.spyOn(PosthogAnalytics.instance, "getAnonymity").mockReturnValue(Anonymity.Pseudonymous); client.getAccountData.mockImplementation((eventType: string) => { if (eventType === PosthogAnalytics.ANALYTICS_EVENT_TYPE) { return new MatrixEvent({ content: { id: "123456789987654321", pseudonymousAnalyticsOptIn: true } }); @@ -771,6 +797,9 @@ describe("ElementCall", () => { const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); expect(urlParams.get("analyticsID")).toBe("123456789987654321"); + expect(urlParams.get("posthogUserId")).toBe("123456789987654321"); + expect(urlParams.get("posthogApiHost")).toBe("https://posthog"); + expect(urlParams.get("posthogApiKey")).toBe("DEADBEEF"); call.destroy(); }); @@ -788,7 +817,7 @@ describe("ElementCall", () => { if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); - expect(urlParams.get("analyticsID")).toBe(""); + expect(urlParams.get("analyticsID")).toBeFalsy(); call.destroy(); }); @@ -827,7 +856,7 @@ describe("ElementCall", () => { if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); - expect(urlParams.get("analyticsID")).toBe(""); + expect(urlParams.get("analyticsID")).toBeFalsy(); }); }); diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index b730853a89..71bc57160d 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -196,6 +196,8 @@ describe("RoomViewStore", function () { stores.client = mockClient; stores._SlidingSyncManager = slidingSyncManager; stores._PosthogAnalytics = new MockPosthogAnalytics(); + // @ts-expect-error + MockPosthogAnalytics.instance = stores._PosthogAnalytics; stores._SpaceStore = new MockSpaceStore(); roomViewStore = new RoomViewStore(dis, stores); stores._RoomViewStore = roomViewStore; diff --git a/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts b/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts index 7d9da6e0d2..04f0ec1404 100644 --- a/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts @@ -21,7 +21,6 @@ import { } from "matrix-js-sdk/src/matrix"; import { Widget, - MatrixWidgetType, WidgetKind, type WidgetDriver, type ITurnServer, @@ -44,6 +43,7 @@ import { ModuleRunner } from "../../../../src/modules/ModuleRunner"; import dis from "../../../../src/dispatcher/dispatcher"; import Modal from "../../../../src/Modal"; import SettingsStore from "../../../../src/settings/SettingsStore"; +import { WidgetType } from "../../../../src/widgets/WidgetType.ts"; describe("StopGapWidgetDriver", () => { let client: MockedObject; @@ -79,7 +79,7 @@ describe("StopGapWidgetDriver", () => { new Widget({ id: "group_call", creatorUserId: "@alice:example.org", - type: MatrixWidgetType.Custom, + type: WidgetType.CALL.preferred, url: "https://call.element.io", }), WidgetKind.Room, diff --git a/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts b/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts index 84612fe236..81c7f35e7b 100644 --- a/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts +++ b/test/unit-tests/stores/widgets/WidgetPermissionStore-test.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { mocked } from "jest-mock"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { MatrixWidgetType, Widget, WidgetKind } from "matrix-widget-api"; +import { Widget, WidgetKind } from "matrix-widget-api"; import { OIDCState, WidgetPermissionStore } from "../../../../src/stores/widgets/WidgetPermissionStore"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -17,6 +17,7 @@ import { type SettingLevel } from "../../../../src/settings/SettingLevel"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { stubClient } from "../../../test-utils"; import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver"; +import { WidgetType } from "../../../../src/widgets/WidgetType.ts"; jest.mock("../../../../src/settings/SettingsStore"); @@ -34,7 +35,7 @@ describe("WidgetPermissionStore", () => { const elementCallWidget = new Widget({ id: "group_call", creatorUserId: "@alice:example.org", - type: MatrixWidgetType.Custom, + type: WidgetType.CALL.preferred, url: "https://call.element.io", }); let settings: Record = {}; // key value store diff --git a/test/unit-tests/utils/device/clientInformation-test.ts b/test/unit-tests/utils/device/clientInformation-test.ts index 8d73cde9f5..18478dbdaf 100644 --- a/test/unit-tests/utils/device/clientInformation-test.ts +++ b/test/unit-tests/utils/device/clientInformation-test.ts @@ -28,7 +28,7 @@ describe("recordClientInformation()", () => { const sdkConfig: DeepReadonly = { ...DEFAULTS, brand: "Test Brand", - element_call: { url: "", use_exclusively: false, brand: "Element Call" }, + element_call: { use_exclusively: false, brand: "Element Call" }, }; const platform = { diff --git a/webpack.config.js b/webpack.config.js index e8413f372c..531e9ea2b2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -674,6 +674,12 @@ module.exports = (env, argv) => { { from: "media/**", context: path.resolve(__dirname, "res/") }, { from: "config.json", noErrorOnMissing: true }, "contribute.json", + // Element Call embedded widget + { + from: "**", + context: path.resolve(__dirname, "node_modules/@element-hq/element-call-embedded/dist"), + to: path.join(__dirname, "webapp", "widgets", "element-call"), + }, ], }), diff --git a/yarn.lock b/yarn.lock index a83d9458ef..0e537e5e35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1560,6 +1560,11 @@ resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== +"@element-hq/element-call-embedded@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.9.0.tgz#459c5a304ad9f97e942120518c70eeaffbe45ac7" + integrity sha512-W/ocQqfzU1rqwjLBCjCpeIEJTX2iCqn8CR3GRF0kqd5wsehPDhIsGPnUDmFalY/iN15eRDx/hex2rgOOJzFoGg== + "@element-hq/element-web-module-api@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-0.1.1.tgz#e2b24aa38aa9f7b6af3c4993e6402a8b7e2f3cb5"