mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
24 Commits
hs/add-rep
...
t3chguy/em
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c869ceb2 | ||
|
|
a6151fec12 | ||
|
|
303f7fc33f | ||
|
|
c08503c672 | ||
|
|
6a7a252635 | ||
|
|
988f8e9a65 | ||
|
|
90ba0b4894 | ||
|
|
ba8f5a11df | ||
|
|
b3de784519 | ||
|
|
4bd41f9310 | ||
|
|
99c5d656b6 | ||
|
|
d4f8a754de | ||
|
|
4226db32b8 | ||
|
|
d8bc28868c | ||
|
|
7b376831be | ||
|
|
712d52c58c | ||
|
|
a8cc0ca125 | ||
|
|
b1d6fd5be6 | ||
|
|
47bfe096c5 | ||
|
|
6a5d436098 | ||
|
|
42818530cd | ||
|
|
513d42f726 | ||
|
|
de1c9c8603 | ||
|
|
a91777eaff |
1
.github/workflows/static_analysis.yaml
vendored
1
.github/workflows/static_analysis.yaml
vendored
@@ -51,6 +51,7 @@ jobs:
|
|||||||
error|invalid_json
|
error|invalid_json
|
||||||
error|misconfigured
|
error|misconfigured
|
||||||
welcome_to_element
|
welcome_to_element
|
||||||
|
devtools|settings|elementCallUrl
|
||||||
|
|
||||||
rethemendex_lint:
|
rethemendex_lint:
|
||||||
name: "Rethemendex Check"
|
name: "Rethemendex Check"
|
||||||
|
|||||||
@@ -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
|
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.
|
at any time without notice.
|
||||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
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
|
- `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`.
|
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
|
- `participant_limit`: The maximum number of users who can join a call; if
|
||||||
|
|||||||
2
knip.ts
2
knip.ts
@@ -40,6 +40,8 @@ export default {
|
|||||||
// Used by webpack
|
// Used by webpack
|
||||||
"process",
|
"process",
|
||||||
"util",
|
"util",
|
||||||
|
// Embedded into webapp
|
||||||
|
"@element-hq/element-call-embedded",
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used in scripts & workflows
|
// Used in scripts & workflows
|
||||||
|
|||||||
@@ -177,6 +177,7 @@
|
|||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
|
"@element-hq/element-call-embedded": "^0.9.0-rc.4",
|
||||||
"@element-hq/element-web-playwright-common": "^1.1.5",
|
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export interface IConfigOptions {
|
|||||||
obey_asserted_identity?: boolean; // MSC3086
|
obey_asserted_identity?: boolean; // MSC3086
|
||||||
};
|
};
|
||||||
element_call: {
|
element_call: {
|
||||||
url?: string;
|
|
||||||
guest_spa_url?: string;
|
guest_spa_url?: string;
|
||||||
use_exclusively?: boolean;
|
use_exclusively?: boolean;
|
||||||
participant_limit?: number;
|
participant_limit?: number;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
|
|||||||
preferred_domain: "meet.element.io",
|
preferred_domain: "meet.element.io",
|
||||||
},
|
},
|
||||||
element_call: {
|
element_call: {
|
||||||
url: "https://call.element.io",
|
|
||||||
use_exclusively: false,
|
use_exclusively: false,
|
||||||
participant_limit: 8,
|
participant_limit: 8,
|
||||||
brand: "Element Call",
|
brand: "Element Call",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import ServerInfo from "./devtools/ServerInfo";
|
|||||||
import CopyableText from "../elements/CopyableText";
|
import CopyableText from "../elements/CopyableText";
|
||||||
import RoomNotifications from "./devtools/RoomNotifications";
|
import RoomNotifications from "./devtools/RoomNotifications";
|
||||||
import { Crypto } from "./devtools/Crypto";
|
import { Crypto } from "./devtools/Crypto";
|
||||||
|
import SettingsField from "../elements/SettingsField.tsx";
|
||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
Room,
|
Room,
|
||||||
@@ -101,6 +102,7 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
|
|||||||
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
||||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||||
|
<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} />
|
||||||
</div>
|
</div>
|
||||||
</BaseTool>
|
</BaseTool>
|
||||||
);
|
);
|
||||||
|
|||||||
59
src/components/views/elements/SettingsField.tsx
Normal file
59
src/components/views/elements/SettingsField.tsx
Normal file
@@ -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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<EditInPlace
|
||||||
|
label={label ?? SettingsStore.getDisplayName(settingKey, level) ?? ""}
|
||||||
|
value={value}
|
||||||
|
saveButtonLabel={_t("common|save")}
|
||||||
|
cancelButtonLabel={_t("common|cancel")}
|
||||||
|
savedLabel={_t("common|saved")}
|
||||||
|
savingLabel={_t("common|updating")}
|
||||||
|
onChange={onChange}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSave={onSave}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsField;
|
||||||
@@ -834,6 +834,9 @@
|
|||||||
"setting_colon": "Setting:",
|
"setting_colon": "Setting:",
|
||||||
"setting_definition": "Setting definition:",
|
"setting_definition": "Setting definition:",
|
||||||
"setting_id": "Setting ID",
|
"setting_id": "Setting ID",
|
||||||
|
"settings": {
|
||||||
|
"elementCallUrl": "Element Call URL"
|
||||||
|
},
|
||||||
"settings_explorer": "Settings explorer",
|
"settings_explorer": "Settings explorer",
|
||||||
"show_hidden_events": "Show hidden events in timeline",
|
"show_hidden_events": "Show hidden events in timeline",
|
||||||
"spaces": {
|
"spaces": {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
|
|
||||||
import type EventEmitter from "events";
|
import type EventEmitter from "events";
|
||||||
import type { IApp } from "../stores/WidgetStore";
|
import type { IApp } from "../stores/WidgetStore";
|
||||||
import SdkConfig, { DEFAULTS } from "../SdkConfig";
|
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
||||||
import { timeout } from "../utils/promise";
|
import { timeout } from "../utils/promise";
|
||||||
@@ -43,12 +42,13 @@ import WidgetStore from "../stores/WidgetStore";
|
|||||||
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widgets/WidgetMessagingStore";
|
||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
|
||||||
import { getCurrentLanguage } from "../languageHandler";
|
import { getCurrentLanguage } from "../languageHandler";
|
||||||
import { PosthogAnalytics } from "../PosthogAnalytics";
|
import { Anonymity, PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
import { UPDATE_EVENT } from "../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../stores/AsyncStore";
|
||||||
import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers";
|
import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers";
|
||||||
import { isVideoRoom } from "../utils/video-rooms";
|
import { isVideoRoom } from "../utils/video-rooms";
|
||||||
import { FontWatcher } from "../settings/watchers/FontWatcher";
|
import { FontWatcher } from "../settings/watchers/FontWatcher";
|
||||||
import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
|
import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
|
||||||
|
import SdkConfig from "../SdkConfig.ts";
|
||||||
|
|
||||||
const TIMEOUT_MS = 16000;
|
const TIMEOUT_MS = 16000;
|
||||||
|
|
||||||
@@ -676,14 +676,12 @@ export class ElementCall extends Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static generateWidgetUrl(client: MatrixClient, roomId: string): URL {
|
private static generateWidgetUrl(client: MatrixClient, roomId: string): URL {
|
||||||
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
|
const baseUrl = window.location.href;
|
||||||
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
|
let url = new URL("./widgets/element-call/index.html#", baseUrl); // this strips hash fragment from baseUrl
|
||||||
// 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.
|
const elementCallUrl = SettingsStore.getValue("Developer.elementCallUrl");
|
||||||
// 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)
|
if (elementCallUrl) url = new URL(elementCallUrl);
|
||||||
const analyticsID: string = accountAnalyticsData?.getContent().pseudonymousAnalyticsOptIn
|
|
||||||
? accountAnalyticsData?.getContent().id
|
|
||||||
: "";
|
|
||||||
// Splice together the Element Call URL for this call
|
// Splice together the Element Call URL for this call
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
embed: "true", // We're embedding EC within another application
|
embed: "true", // We're embedding EC within another application
|
||||||
@@ -700,12 +698,44 @@ export class ElementCall extends Call {
|
|||||||
lang: getCurrentLanguage().replace("_", "-"),
|
lang: getCurrentLanguage().replace("_", "-"),
|
||||||
fontScale: (FontWatcher.getRootFontSize() / FontWatcher.getBrowserDefaultFontSize()).toString(),
|
fontScale: (FontWatcher.getRootFontSize() / FontWatcher.getBrowserDefaultFontSize()).toString(),
|
||||||
theme: "$org.matrix.msc2873.client_theme",
|
theme: "$org.matrix.msc2873.client_theme",
|
||||||
analyticsID,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (SettingsStore.getValue("fallbackICEServerAllowed")) params.append("allowIceFallback", "true");
|
const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url");
|
||||||
if (SettingsStore.getValue("feature_allow_screen_share_only_mode"))
|
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");
|
params.append("allowVoipWithNoMedia", "true");
|
||||||
|
}
|
||||||
|
|
||||||
// Set custom fonts
|
// Set custom fonts
|
||||||
if (SettingsStore.getValue("useSystemFont")) {
|
if (SettingsStore.getValue("useSystemFont")) {
|
||||||
@@ -720,8 +750,6 @@ export class ElementCall extends Call {
|
|||||||
.forEach((font) => params.append("font", font));
|
.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, "$");
|
const replacedUrl = params.toString().replace(/%24/g, "$");
|
||||||
url.hash = `#?${replacedUrl}`;
|
url.hash = `#?${replacedUrl}`;
|
||||||
return url;
|
return url;
|
||||||
|
|||||||
@@ -347,11 +347,13 @@ export interface Settings {
|
|||||||
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
|
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
|
||||||
"Electron.showTrayIcon": IBaseSetting<boolean>;
|
"Electron.showTrayIcon": IBaseSetting<boolean>;
|
||||||
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
||||||
|
"Developer.elementCallUrl": IBaseSetting<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SettingKey = keyof Settings;
|
export type SettingKey = keyof Settings;
|
||||||
export type FeatureSettingKey = Assignable<Settings, IFeature>;
|
export type FeatureSettingKey = Assignable<Settings, IFeature>;
|
||||||
export type BooleanSettingKey = Assignable<Settings, IBaseSetting<boolean>> | FeatureSettingKey;
|
export type BooleanSettingKey = Assignable<Settings, IBaseSetting<boolean>> | FeatureSettingKey;
|
||||||
|
export type StringSettingKey = Assignable<Settings, IBaseSetting<string>>;
|
||||||
|
|
||||||
export const SETTINGS: Settings = {
|
export const SETTINGS: Settings = {
|
||||||
"feature_video_rooms": {
|
"feature_video_rooms": {
|
||||||
@@ -1371,4 +1373,10 @@ export const SETTINGS: Settings = {
|
|||||||
displayName: _td("settings|preferences|enable_hardware_acceleration"),
|
displayName: _td("settings|preferences|enable_hardware_acceleration"),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"Developer.elementCallUrl": {
|
||||||
|
supportedLevels: [SettingLevel.DEVICE],
|
||||||
|
displayName: _td("devtools|settings|elementCallUrl"),
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import {
|
|||||||
WidgetLifecycle,
|
WidgetLifecycle,
|
||||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||||
|
|
||||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
|
||||||
import { iterableDiff, iterableIntersection } from "../../utils/iterables";
|
import { iterableDiff, iterableIntersection } from "../../utils/iterables";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import Modal from "../../Modal";
|
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.
|
// 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.
|
// Widgets don't technically need to request this capability, but Scalar still does.
|
||||||
this.allowedCapabilities.add("visibility");
|
this.allowedCapabilities.add("visibility");
|
||||||
} else if (
|
} else if (virtual && WidgetType.CALL.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) {
|
||||||
virtual &&
|
|
||||||
new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url!).origin === this.forWidget.origin
|
|
||||||
) {
|
|
||||||
// This is a trusted Element Call widget that we control
|
// This is a trusted Element Call widget that we control
|
||||||
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
||||||
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
||||||
|
|||||||
@@ -190,6 +190,31 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
@@ -49,8 +49,9 @@ import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagin
|
|||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../src/stores/ActiveWidgetStore";
|
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../src/stores/ActiveWidgetStore";
|
||||||
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
|
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
|
||||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
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 { type SettingKey } from "../../../src/settings/Settings.tsx";
|
||||||
|
import SdkConfig from "../../../src/SdkConfig.ts";
|
||||||
|
|
||||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||||
[MediaDeviceKindEnum.AudioInput]: [
|
[MediaDeviceKindEnum.AudioInput]: [
|
||||||
@@ -664,6 +665,7 @@ describe("ElementCall", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
({ client, room, alice } = setUpClientRoomAndStores());
|
({ client, room, alice } = setUpClientRoomAndStores());
|
||||||
|
SdkConfig.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -683,6 +685,23 @@ describe("ElementCall", () => {
|
|||||||
Call.get(room)?.destroy();
|
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 () => {
|
it("finds ongoing calls that are created by the session manager", async () => {
|
||||||
// There is an existing session created by another user in this room.
|
// There is an existing session created by another user in this room.
|
||||||
client.matrixRTC.getRoomSession.mockReturnValue({
|
client.matrixRTC.getRoomSession.mockReturnValue({
|
||||||
@@ -758,7 +777,14 @@ describe("ElementCall", () => {
|
|||||||
SettingsStore.getValue = originalGetValue;
|
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) => {
|
client.getAccountData.mockImplementation((eventType: string) => {
|
||||||
if (eventType === PosthogAnalytics.ANALYTICS_EVENT_TYPE) {
|
if (eventType === PosthogAnalytics.ANALYTICS_EVENT_TYPE) {
|
||||||
return new MatrixEvent({ content: { id: "123456789987654321", pseudonymousAnalyticsOptIn: true } });
|
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));
|
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
||||||
expect(urlParams.get("analyticsID")).toBe("123456789987654321");
|
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();
|
call.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -788,7 +817,7 @@ describe("ElementCall", () => {
|
|||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
||||||
expect(urlParams.get("analyticsID")).toBe("");
|
expect(urlParams.get("analyticsID")).toBeFalsy();
|
||||||
call.destroy();
|
call.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -827,7 +856,7 @@ describe("ElementCall", () => {
|
|||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
||||||
expect(urlParams.get("analyticsID")).toBe("");
|
expect(urlParams.get("analyticsID")).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ describe("RoomViewStore", function () {
|
|||||||
stores.client = mockClient;
|
stores.client = mockClient;
|
||||||
stores._SlidingSyncManager = slidingSyncManager;
|
stores._SlidingSyncManager = slidingSyncManager;
|
||||||
stores._PosthogAnalytics = new MockPosthogAnalytics();
|
stores._PosthogAnalytics = new MockPosthogAnalytics();
|
||||||
|
// @ts-expect-error
|
||||||
|
MockPosthogAnalytics.instance = stores._PosthogAnalytics;
|
||||||
stores._SpaceStore = new MockSpaceStore();
|
stores._SpaceStore = new MockSpaceStore();
|
||||||
roomViewStore = new RoomViewStore(dis, stores);
|
roomViewStore = new RoomViewStore(dis, stores);
|
||||||
stores._RoomViewStore = roomViewStore;
|
stores._RoomViewStore = roomViewStore;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
Widget,
|
Widget,
|
||||||
MatrixWidgetType,
|
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
type WidgetDriver,
|
type WidgetDriver,
|
||||||
type ITurnServer,
|
type ITurnServer,
|
||||||
@@ -44,6 +43,7 @@ import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
|||||||
import dis from "../../../../src/dispatcher/dispatcher";
|
import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
import Modal from "../../../../src/Modal";
|
import Modal from "../../../../src/Modal";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import { WidgetType } from "../../../../src/widgets/WidgetType.ts";
|
||||||
|
|
||||||
describe("StopGapWidgetDriver", () => {
|
describe("StopGapWidgetDriver", () => {
|
||||||
let client: MockedObject<MatrixClient>;
|
let client: MockedObject<MatrixClient>;
|
||||||
@@ -79,7 +79,7 @@ describe("StopGapWidgetDriver", () => {
|
|||||||
new Widget({
|
new Widget({
|
||||||
id: "group_call",
|
id: "group_call",
|
||||||
creatorUserId: "@alice:example.org",
|
creatorUserId: "@alice:example.org",
|
||||||
type: MatrixWidgetType.Custom,
|
type: WidgetType.CALL.preferred,
|
||||||
url: "https://call.element.io",
|
url: "https://call.element.io",
|
||||||
}),
|
}),
|
||||||
WidgetKind.Room,
|
WidgetKind.Room,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
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 { OIDCState, WidgetPermissionStore } from "../../../../src/stores/widgets/WidgetPermissionStore";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
@@ -17,6 +17,7 @@ import { type SettingLevel } from "../../../../src/settings/SettingLevel";
|
|||||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
import { stubClient } from "../../../test-utils";
|
import { stubClient } from "../../../test-utils";
|
||||||
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
|
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
|
||||||
|
import { WidgetType } from "../../../../src/widgets/WidgetType.ts";
|
||||||
|
|
||||||
jest.mock("../../../../src/settings/SettingsStore");
|
jest.mock("../../../../src/settings/SettingsStore");
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ describe("WidgetPermissionStore", () => {
|
|||||||
const elementCallWidget = new Widget({
|
const elementCallWidget = new Widget({
|
||||||
id: "group_call",
|
id: "group_call",
|
||||||
creatorUserId: "@alice:example.org",
|
creatorUserId: "@alice:example.org",
|
||||||
type: MatrixWidgetType.Custom,
|
type: WidgetType.CALL.preferred,
|
||||||
url: "https://call.element.io",
|
url: "https://call.element.io",
|
||||||
});
|
});
|
||||||
let settings: Record<string, any> = {}; // key value store
|
let settings: Record<string, any> = {}; // key value store
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe("recordClientInformation()", () => {
|
|||||||
const sdkConfig: DeepReadonly<IConfigOptions> = {
|
const sdkConfig: DeepReadonly<IConfigOptions> = {
|
||||||
...DEFAULTS,
|
...DEFAULTS,
|
||||||
brand: "Test Brand",
|
brand: "Test Brand",
|
||||||
element_call: { url: "", use_exclusively: false, brand: "Element Call" },
|
element_call: { use_exclusively: false, brand: "Element Call" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const platform = {
|
const platform = {
|
||||||
|
|||||||
@@ -674,6 +674,12 @@ module.exports = (env, argv) => {
|
|||||||
{ from: "media/**", context: path.resolve(__dirname, "res/") },
|
{ from: "media/**", context: path.resolve(__dirname, "res/") },
|
||||||
{ from: "config.json", noErrorOnMissing: true },
|
{ from: "config.json", noErrorOnMissing: true },
|
||||||
"contribute.json",
|
"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"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1546,6 +1546,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
|
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
|
||||||
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
||||||
|
|
||||||
|
"@element-hq/element-call-embedded@^0.9.0-rc.4":
|
||||||
|
version "0.9.0-rc.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.9.0-rc.4.tgz#de9326cccd746175bf47a6cdd0c5b65d5ad0df86"
|
||||||
|
integrity sha512-oiF7TQltYM2l3u7/8O2j8Hq7I+ROSB3+o1FDUNKkhgOisHf7A1P108WSOVXMS9GCzus1/ZwbweRgF977EQLXQA==
|
||||||
|
|
||||||
"@element-hq/element-web-module-api@^0.1.1":
|
"@element-hq/element-web-module-api@^0.1.1":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-0.1.1.tgz#e2b24aa38aa9f7b6af3c4993e6402a8b7e2f3cb5"
|
||||||
|
|||||||
Reference in New Issue
Block a user