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 <hughns@element.io>
This commit is contained in:
Michael Telatynski
2025-03-28 20:34:32 +00:00
committed by GitHub
parent 209ab59978
commit 8bb4d44532
22 changed files with 264 additions and 33 deletions

View File

@@ -190,6 +190,31 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
/>
</div>
</div>
<form
class="_root_19upo_16"
>
<div
class="_field_19upo_26"
>
<label
class="_label_19upo_59"
for="radix-:r4:"
>
Element Call URL
</label>
<div
class="_controls_17lij_8"
>
<input
class="_control_sqdq4_10"
id="radix-:r4:"
name="input"
title=""
value=""
/>
</div>
</div>
</form>
</div>
</div>
<div

View File

@@ -0,0 +1,36 @@
/*
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 from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import SettingsField from "../../../../../src/components/views/elements/SettingsField";
import { SettingLevel } from "../../../../../src/settings/SettingLevel.ts";
describe("<SettingsField />", () => {
it("should render with the default label", () => {
const component = render(<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} />);
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(<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} onChange={fn} />);
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");
});
});
});

View File

@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SettingsField /> should render with the default label 1`] = `
<DocumentFragment>
<form
class="_root_19upo_16"
>
<div
class="_field_19upo_26"
>
<label
class="_label_19upo_59"
for="radix-:r0:"
>
Element Call URL
</label>
<div
class="_controls_17lij_8"
>
<input
class="_control_sqdq4_10"
id="radix-:r0:"
name="input"
title=""
value=""
/>
</div>
</div>
</form>
</DocumentFragment>
`;

View File

@@ -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();
});
});

View File

@@ -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;

View File

@@ -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<MatrixClient>;
@@ -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,

View File

@@ -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<string, any> = {}; // key value store

View File

@@ -28,7 +28,7 @@ describe("recordClientInformation()", () => {
const sdkConfig: DeepReadonly<IConfigOptions> = {
...DEFAULTS,
brand: "Test Brand",
element_call: { url: "", use_exclusively: false, brand: "Element Call" },
element_call: { use_exclusively: false, brand: "Element Call" },
};
const platform = {