Allow disabling legacy calls and make Voice & Video settings Legacy Voice & Video (#33692)

* Change `feature_group_calls` to default to true.
Change state to Beta

* Add `enableLegacyCallsVoip` (clarify: Audio settings relate to Legacy)
Change `element_call.use_exclusively` to just be the default of
`enableLegacyCallsVoip`.
Allow disabling in Legacy in the settings. (overwrite `use_exclusivly`
via `enableLegacyCallsVoip`)
Rename `Voice & Video` to `Legacy Voice & Video`

* improvements
 - settings title update to only show legacy when appropriate
 - hide menu all together if use_exclusivly is chosen

* simplify by removing the feature_group_calls flag

* smaller cleanup

* fix tests and cleanup

* clarify settings (disable) in config.md

* add back EC relevant settings in room settings dialog

* update screenshots (now default to EC enabled (not disabled)
-> show voice and video tab in room settings

* update screenshots

* always show option to control voice messages input

* lint

* fix test

* disable -> enabled And only have on mic dropdown

* update config.md

* Back to `disabled` (on by default/no config)

* remove element call hint from voice-video settings

* Update apps/web/src/i18n/strings/en_EN.json

Co-authored-by: Robin <robin@robin.town>

* Update apps/web/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx

Co-authored-by: Robin <robin@robin.town>

* review

* cleanup element call url setting (its deprecated/not used anymore)

* clarify, that also jisti will be enabled/disabled by legacy settings
flag

* remove double section

* remove unn exassary element_call configurations

* Update en_EN.json

* Update VoiceUserSettingsTab.tsx

* Make tests use new defaults (enable element call)

* Default events power levels

* format

* review "Enable Legacy voice/video calls" label

* more radical usage of defaults in element_call config.json's

* Update config.json

* Update en_EN.json

* Update RoomSettingsDialog-test.tsx.snap

* Update useRoomCall.tsx

* fix imports

* Update request permission promp to latest designs:
https://www.figma.com/design/aOEkaJtaBmPy058V7uoqVr/Element-Call-Updates---2026?node-id=45-12591&t=7takL39icZwt9YjL-0

* Update config.json.d.ts

* fix usage of `Element` in i18n

* Update General-room-settings-tab-should-be-rendered-properly-1-linux.png

---------

Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
Timo
2026-07-07 16:09:36 +00:00
committed by GitHub
co-authored by Robin
parent 467ab5e7d4
commit 1b10122a7b
32 changed files with 265 additions and 185 deletions
@@ -51,15 +51,11 @@
"features": { "features": {
"threadsActivityCentre": true, "threadsActivityCentre": true,
"feature_spotlight": true, "feature_spotlight": true,
"feature_group_calls": true,
"feature_video_rooms": true, "feature_video_rooms": true,
"feature_element_call_video_rooms": true "feature_element_call_video_rooms": true
}, },
"setting_defaults": { "setting_defaults": {
"RustCrypto.staged_rollout_percent": 100 "RustCrypto.staged_rollout_percent": 100
}, },
"element_call": {
"url": "https://call.element.dev"
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
} }
@@ -50,10 +50,6 @@
}, },
"features": { "features": {
"feature_video_rooms": true, "feature_video_rooms": true,
"feature_group_calls": true,
"feature_element_call_video_rooms": true "feature_element_call_video_rooms": true
},
"element_call": {
"url": "https://call.element.io"
} }
} }
+3 -2
View File
@@ -42,8 +42,9 @@
"preferred_domain": "meet.element.io" "preferred_domain": "meet.element.io"
}, },
"element_call": { "element_call": {
"url": "https://call.element.io", "brand": "Element Call",
"brand": "Element Call" "disable": false,
"use_exclusively": false
}, },
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
} }
-4
View File
@@ -47,10 +47,6 @@
}, },
"features": { "features": {
"feature_video_rooms": true, "feature_video_rooms": true,
"feature_group_calls": true,
"feature_element_call_video_rooms": true "feature_element_call_video_rooms": true
},
"element_call": {
"url": "https://call.element.io"
} }
} }
-4
View File
@@ -48,15 +48,11 @@
"features": { "features": {
"threadsActivityCentre": true, "threadsActivityCentre": true,
"feature_video_rooms": true, "feature_video_rooms": true,
"feature_group_calls": true,
"feature_element_call_video_rooms": true "feature_element_call_video_rooms": true
}, },
"setting_defaults": { "setting_defaults": {
"RustCrypto.staged_rollout_percent": 100, "RustCrypto.staged_rollout_percent": 100,
"Registration.mobileRegistrationHelper": true "Registration.mobileRegistrationHelper": true
}, },
"element_call": {
"url": "https://call.element.dev"
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
} }
@@ -8,8 +8,6 @@
import { type Locator } from "@playwright/test"; import { type Locator } from "@playwright/test";
import { test, expect } from "../../../element-web-test"; import { test, expect } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
test.describe("Voice & Video room settings tab", () => { test.describe("Voice & Video room settings tab", () => {
const roomName = "Test room"; const roomName = "Test room";
@@ -22,7 +20,6 @@ test.describe("Voice & Video room settings tab", () => {
test.beforeEach(async ({ user, app, page }) => { test.beforeEach(async ({ user, app, page }) => {
// Execute client actions before setting, as the setting will force a reload. // Execute client actions before setting, as the setting will force a reload.
await app.client.createRoom({ name: roomName }); await app.client.createRoom({ name: roomName });
await app.settings.setValue("feature_group_calls", null, SettingLevel.DEVICE, true);
await app.viewRoomByName(roomName); await app.viewRoomByName(roomName);
settings = await app.settings.openRoomSettings("Voice & Video"); settings = await app.settings.openRoomSettings("Voice & Video");
}); });
@@ -102,14 +102,6 @@ test.use({
test.describe("Element Call", () => { test.describe("Element Call", () => {
test.use({ test.use({
config: {
element_call: {
use_exclusively: false,
},
features: {
feature_group_calls: true,
},
},
displayName: "Alice", displayName: "Alice",
botCreateOpts: { botCreateOpts: {
autoAcceptInvites: true, autoAcceptInvites: true,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 89 KiB

-1
View File
@@ -35,7 +35,6 @@ export const DEFAULTS = {
preferred_domain: "meet.element.io", preferred_domain: "meet.element.io",
}, },
element_call: { element_call: {
use_exclusively: false,
brand: "Element Call", brand: "Element Call",
}, },
@@ -45,6 +45,7 @@ import { PeopleRoomSettingsTab } from "../settings/tabs/room/PeopleRoomSettingsT
import { SDKContext } from "../../../contexts/SDKContext"; import { SDKContext } from "../../../contexts/SDKContext";
import { type SDKContextClass } from "../../../contexts/SDKContextClass"; import { type SDKContextClass } from "../../../contexts/SDKContextClass";
import { RoomSettingsTab } from "./RoomSettingsDialog-tab.ts"; import { RoomSettingsTab } from "./RoomSettingsDialog-tab.ts";
import SdkConfig from "../../../SdkConfig";
interface IProps { interface IProps {
roomId: string; roomId: string;
@@ -147,7 +148,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
), ),
); );
} }
if (SettingsStore.getValue("feature_group_calls")) { if (!SdkConfig.get("element_call").disable) {
tabs.push( tabs.push(
new Tab( new Tab(
RoomSettingsTab.Voip, RoomSettingsTab.Voip,
@@ -18,7 +18,6 @@ import Modal from "../../../../../Modal";
import ErrorDialog from "../../../dialogs/ErrorDialog"; import ErrorDialog from "../../../dialogs/ErrorDialog";
import PowerSelector from "../../../elements/PowerSelector"; import PowerSelector from "../../../elements/PowerSelector";
import SettingsFieldset from "../../SettingsFieldset"; import SettingsFieldset from "../../SettingsFieldset";
import SettingsStore from "../../../../../settings/SettingsStore";
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
import { AddPrivilegedUsers } from "../../AddPrivilegedUsers"; import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
import SettingsTab from "../SettingsTab"; import SettingsTab from "../SettingsTab";
@@ -270,17 +269,14 @@ export default class RolesRoomSettingsTab extends React.Component<IProps, RolesR
[EventType.Reaction]: _td("room_settings|permissions|m.reaction"), [EventType.Reaction]: _td("room_settings|permissions|m.reaction"),
[EventType.RoomRedaction]: _td("room_settings|permissions|m.room.redaction"), [EventType.RoomRedaction]: _td("room_settings|permissions|m.room.redaction"),
[EventType.RoomPinnedEvents]: _td("room_settings|permissions|m.room.pinned_events"), [EventType.RoomPinnedEvents]: _td("room_settings|permissions|m.room.pinned_events"),
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"), "im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"),
}; };
// MSC3401: Native Group VoIP signaling // MSC3401: Native Group VoIP signaling
if (SettingsStore.getValue("feature_group_calls")) { if (!SdkConfig.get("element_call").disable) {
plEventsToLabels[ElementCallEventType.name] = _td("room_settings|permissions|m.call"); plEventsToLabels[ElementCallEventType.name] = _td("room_settings|permissions|m.call");
plEventsToLabels[ElementCallMemberEventType.name] = _td("room_settings|permissions|m.call.member"); plEventsToLabels[ElementCallMemberEventType.name] = _td("room_settings|permissions|m.call.member");
} }
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = { const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
"users_default": { "users_default": {
desc: _t("room_settings|permissions|users_default"), desc: _t("room_settings|permissions|users_default"),
@@ -11,7 +11,7 @@ import React, { type ChangeEventHandler, type JSX, type ReactNode } from "react"
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { FALLBACK_ICE_SERVER } from "matrix-js-sdk/src/webrtc/call"; import { FALLBACK_ICE_SERVER } from "matrix-js-sdk/src/webrtc/call";
import { type EmptyObject } from "matrix-js-sdk/src/matrix"; import { type EmptyObject } from "matrix-js-sdk/src/matrix";
import { Form, SettingsToggleInput } from "@vector-im/compound-web"; import { Alert, Form, SettingsToggleInput } from "@vector-im/compound-web";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import MediaDeviceHandler, { type IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler"; import MediaDeviceHandler, { type IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
@@ -19,11 +19,13 @@ import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from "../../../elements/SettingsFlag"; import SettingsFlag from "../../../elements/SettingsFlag";
import SettingsStore from "../../../../../settings/SettingsStore";
import { requestMediaPermissions } from "../../../../../utils/media/requestMediaPermissions"; import { requestMediaPermissions } from "../../../../../utils/media/requestMediaPermissions";
import SettingsTab from "../SettingsTab"; import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection"; import { SettingsSection } from "../../shared/SettingsSection";
import { SettingsSubsection } from "../../shared/SettingsSubsection"; import { SettingsSubsection } from "../../shared/SettingsSubsection";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SdkConfig from "../../../../../SdkConfig";
interface IState { interface IState {
mediaDevices: IMediaDevices | null; mediaDevices: IMediaDevices | null;
@@ -33,6 +35,7 @@ interface IState {
audioAutoGainControl: boolean; audioAutoGainControl: boolean;
audioEchoCancellation: boolean; audioEchoCancellation: boolean;
audioNoiseSuppression: boolean; audioNoiseSuppression: boolean;
enableLegacyCallsVoip: boolean;
} }
/** /**
@@ -65,16 +68,29 @@ export default class VoiceUserSettingsTab extends React.Component<EmptyObject, I
audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl(), audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl(),
audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation(), audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation(),
audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(), audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(),
enableLegacyCallsVoip: SettingsStore.getValue("enableLegacyCallsVoip"),
}; };
} }
private legacyCallsEnabledWatcherRef: string | undefined;
public async componentDidMount(): Promise<void> { public async componentDidMount(): Promise<void> {
this.legacyCallsEnabledWatcherRef = SettingsStore.watchSetting(
"enableLegacyCallsVoip",
null,
(...[, , , , value]) => this.setState({ enableLegacyCallsVoip: value as boolean }),
);
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices(); const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) { if (canSeeDeviceLabels) {
await this.refreshMediaDevices(); await this.refreshMediaDevices();
} }
} }
public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.legacyCallsEnabledWatcherRef);
}
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => { private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
this.setState({ this.setState({
mediaDevices: (await MediaDeviceHandler.getDevices()) ?? null, mediaDevices: (await MediaDeviceHandler.getDevices()) ?? null,
@@ -143,34 +159,46 @@ export default class VoiceUserSettingsTab extends React.Component<EmptyObject, I
private onAutoGainChanged: ChangeEventHandler<HTMLInputElement> = async (event) => { private onAutoGainChanged: ChangeEventHandler<HTMLInputElement> = async (event) => {
const enable = event.target.checked; const enable = event.target.checked;
await MediaDeviceHandler.setAudioAutoGainControl(enable); await MediaDeviceHandler.setAudioAutoGainControl(enable);
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() }); this.setState({
audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl(),
});
}; };
private onNoiseSuppressionChanged: ChangeEventHandler<HTMLInputElement> = async (event) => { private onNoiseSuppressionChanged: ChangeEventHandler<HTMLInputElement> = async (event) => {
const enable = event.target.checked; const enable = event.target.checked;
await MediaDeviceHandler.setAudioNoiseSuppression(enable); await MediaDeviceHandler.setAudioNoiseSuppression(enable);
this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() }); this.setState({
audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(),
});
}; };
private onEchoCancellationChanged: ChangeEventHandler<HTMLInputElement> = async (event) => { private onEchoCancellationChanged: ChangeEventHandler<HTMLInputElement> = async (event) => {
const enable = event.target.checked; const enable = event.target.checked;
await MediaDeviceHandler.setAudioEchoCancellation(enable); await MediaDeviceHandler.setAudioEchoCancellation(enable);
this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() }); this.setState({
audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation(),
});
}; };
public render(): ReactNode { public render(): JSX.Element {
let requestButton: ReactNode | undefined; let requestButton: ReactNode | undefined;
let speakerDropdown: ReactNode | undefined; let speakerDropdown: ReactNode | undefined;
let microphoneDropdown: ReactNode | undefined; let microphoneDropdown: ReactNode | undefined;
let webcamDropdown: ReactNode | undefined; let webcamDropdown: ReactNode | undefined;
if (!this.state.mediaDevices) { if (!this.state.mediaDevices) {
requestButton = ( requestButton = (
<div> <Alert
<p>{_t("settings|voip|missing_permissions_prompt")}</p> type="info"
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary"> title={_t("settings|voip|permissions")}
{_t("settings|voip|request_permissions")} actions={
</AccessibleButton> <AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
</div> {_t("settings|voip|request_permissions")}
</AccessibleButton>
}
>
{_t("settings|voip|missing_permissions_prompt", { brand: SdkConfig.get("brand") })}
</Alert>
); );
} else if (this.state.mediaDevices) { } else if (this.state.mediaDevices) {
speakerDropdown = this.renderDropdown( speakerDropdown = this.renderDropdown(
@@ -185,6 +213,44 @@ export default class VoiceUserSettingsTab extends React.Component<EmptyObject, I
); );
} }
const elementCallEnabled = !SdkConfig.get("element_call").disable;
// We also allow/use legacy if element call is not enabled.
const allowLegacyCalls = !SdkConfig.get("element_call").use_exclusively || !elementCallEnabled;
const showLegacySettings = this.state.enableLegacyCallsVoip;
const legacySettingsComponent = (
<>
<SettingsSubsection heading={_t("settings|voip|voice_section")} stretchContent>
{speakerDropdown}
<SettingsToggleInput
name="voice-auto-gain"
label={_t("settings|voip|voice_agc")}
checked={this.state.audioAutoGainControl}
onChange={this.onAutoGainChanged}
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("settings|voip|video_section")} stretchContent>
{webcamDropdown}
<SettingsFlag name="VideoView.flipVideoHorizontally" level={SettingLevel.ACCOUNT} />
</SettingsSubsection>
<SettingsSection heading={_t("common|advanced")}>
<SettingsFlag
name="webRtcAllowPeerToPeer"
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name="fallbackICEServerAllowed"
label={_t("settings|voip|enable_fallback_ice_server", {
server: new URL(FALLBACK_ICE_SERVER).pathname,
})}
level={SettingLevel.DEVICE}
hideIfCannotSet
/>
</SettingsSection>
</>
);
return ( return (
<SettingsTab> <SettingsTab>
<Form.Root <Form.Root
@@ -193,26 +259,22 @@ export default class VoiceUserSettingsTab extends React.Component<EmptyObject, I
evt.stopPropagation(); evt.stopPropagation();
}} }}
> >
<SettingsSection> <SettingsSection heading={_t("settings|voip|voice_input_section")}>
{_t("settings|voip|voice_input_description")}
{requestButton} {requestButton}
<SettingsSubsection heading={_t("settings|voip|voice_section")} stretchContent> {microphoneDropdown}
{speakerDropdown}
{microphoneDropdown}
<SettingsToggleInput
name="voice-auto-gain"
label={_t("settings|voip|voice_agc")}
checked={this.state.audioAutoGainControl}
onChange={this.onAutoGainChanged}
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("settings|voip|video_section")} stretchContent>
{webcamDropdown}
<SettingsFlag name="VideoView.flipVideoHorizontally" level={SettingLevel.ACCOUNT} />
</SettingsSubsection>
</SettingsSection> </SettingsSection>
{allowLegacyCalls && (
<SettingsSection heading={_t("common|advanced")}> <SettingsSection heading={_t("common|legacy_voice_and_video_settings")}>
<SettingsSubsection heading={_t("settings|voip|voice_processing")}> {elementCallEnabled && (
// allow configuring Legacy calls if EC is also avalibale (otherwise we always show the legacySettingsComponent)
<SettingsFlag name="enableLegacyCallsVoip" level={SettingLevel.DEVICE} />
)}
{(!elementCallEnabled || showLegacySettings) && legacySettingsComponent}
</SettingsSection>
)}
{(allowLegacyCalls || elementCallEnabled) && (
<SettingsSection heading={_t("settings|voip|voice_processing")}>
<SettingsToggleInput <SettingsToggleInput
name="voice-noise-suppression" name="voice-noise-suppression"
label={_t("settings|voip|noise_suppression")} label={_t("settings|voip|noise_suppression")}
@@ -227,23 +289,8 @@ export default class VoiceUserSettingsTab extends React.Component<EmptyObject, I
checked={this.state.audioEchoCancellation} checked={this.state.audioEchoCancellation}
onChange={this.onEchoCancellationChanged} onChange={this.onEchoCancellationChanged}
/> />
</SettingsSubsection> </SettingsSection>
<SettingsSubsection heading={_t("settings|voip|connection_section")}> )}
<SettingsFlag
name="webRtcAllowPeerToPeer"
level={SettingLevel.DEVICE}
onChange={this.changeWebRtcMethod}
/>
<SettingsFlag
name="fallbackICEServerAllowed"
label={_t("settings|voip|enable_fallback_ice_server", {
server: new URL(FALLBACK_ICE_SERVER).pathname,
})}
level={SettingLevel.DEVICE}
hideIfCannotSet
/>
</SettingsSubsection>
</SettingsSection>
</Form.Root> </Form.Root>
</SettingsTab> </SettingsTab>
); );
+21 -10
View File
@@ -45,10 +45,10 @@ import { privateShouldBeEncrypted } from "./utils/rooms";
import { shouldForceDisableEncryption } from "./utils/crypto/shouldForceDisableEncryption"; import { shouldForceDisableEncryption } from "./utils/crypto/shouldForceDisableEncryption";
import { waitForMember } from "./utils/membership"; import { waitForMember } from "./utils/membership";
import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/PreferredRoomVersions"; import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/PreferredRoomVersions";
import SettingsStore from "./settings/SettingsStore";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
import { ElementCallMemberEventType } from "./call-types"; import { ElementCallMemberEventType } from "./call-types";
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize"; import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
import SdkConfig from "./SdkConfig";
// we define a number of interfaces which take their names from the js-sdk // we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@@ -86,15 +86,26 @@ export interface IOpts {
joinRule?: JoinRule; joinRule?: JoinRule;
} }
const DEFAULT_EVENT_POWER_LEVELS = { /**
[EventType.RoomName]: 50, * The power levels set by synapse as of 30.06.2026.
* If power_level_content_override is used, these values should be used
* as a starting point to ensure the expected room power levels.
*
* NOTE: power_level_content_override does do a merge but NOT a deep merge.
* e.g. "ban": 50, will still be set by the server when `power_level_content_override = {events: {"custom": 0}}`
* BUT `events` will lose all the other default props listed below.
* So we have to do: `power_level_content_override = {events: {...POWER_LEVEL_EVENTS_DEFAULT, "custom": 0}}`
* This is very unfortunate if defaults change in synapse we have to keep the sdk in sync... (todo)
*/
export const DEFAULT_EVENTS_POWER_LEVEL = {
[EventType.RoomAvatar]: 50, [EventType.RoomAvatar]: 50,
[EventType.RoomPowerLevels]: 100,
[EventType.RoomHistoryVisibility]: 100,
[EventType.RoomCanonicalAlias]: 50, [EventType.RoomCanonicalAlias]: 50,
[EventType.RoomTombstone]: 100,
[EventType.RoomServerAcl]: 100,
[EventType.RoomEncryption]: 100, [EventType.RoomEncryption]: 100,
[EventType.RoomHistoryVisibility]: 100,
[EventType.RoomName]: 50,
[EventType.RoomPowerLevels]: 100,
[EventType.RoomServerAcl]: 100,
[EventType.RoomTombstone]: 100,
}; };
/** /**
@@ -173,7 +184,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
if (opts.roomType === RoomType.ElementVideo || opts.roomType === RoomType.UnstableCall) { if (opts.roomType === RoomType.ElementVideo || opts.roomType === RoomType.UnstableCall) {
createOpts.power_level_content_override = { createOpts.power_level_content_override = {
events: { events: {
...DEFAULT_EVENT_POWER_LEVELS, ...DEFAULT_EVENTS_POWER_LEVEL,
// Allow all users to send call membership updates // Allow all users to send call membership updates
[opts.roomType === RoomType.ElementVideo [opts.roomType === RoomType.ElementVideo
? JitsiCall.MEMBER_EVENT_TYPE ? JitsiCall.MEMBER_EVENT_TYPE
@@ -186,10 +197,10 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
}, },
}; };
} }
} else if (SettingsStore.getValue("feature_group_calls")) { } else if (!SdkConfig.get("element_call").disable) {
createOpts.power_level_content_override = { createOpts.power_level_content_override = {
events: { events: {
...DEFAULT_EVENT_POWER_LEVELS, ...DEFAULT_EVENTS_POWER_LEVEL,
// It should always (including non video rooms) be possible to join a group call. // It should always (including non video rooms) be possible to join a group call.
[ElementCallMemberEventType.name]: 0, [ElementCallMemberEventType.name]: 0,
}, },
+9 -9
View File
@@ -12,8 +12,7 @@ import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react
import { logger as rootLogger } from "matrix-js-sdk/src/logger"; 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 { useSettingValue } from "../useSettings";
import SdkConfig from "../../SdkConfig";
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter"; import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { useWidgets } from "../../utils/WidgetUtils"; import { useWidgets } from "../../utils/WidgetUtils";
@@ -38,6 +37,7 @@ 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 SdkConfig from "../../SdkConfig";
import { SDKContextClass } from "../../contexts/SDKContextClass"; import { SDKContextClass } from "../../contexts/SDKContextClass";
const logger = rootLogger.getChild("useRoomCall"); const logger = rootLogger.getChild("useRoomCall");
@@ -109,12 +109,13 @@ export const useRoomCall = (
} => { } => {
const roomViewStore = useScopedRoomContext("roomViewStore").roomViewStore; const roomViewStore = useScopedRoomContext("roomViewStore").roomViewStore;
// settings // settings
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets); const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets);
const voipFeatureEnabled = useSettingValue(UIFeature.Voip); const voipFeatureEnabled = useSettingValue(UIFeature.Voip);
const useElementCallExclusively = useMemo(() => { const enableLegacyCallsVoip = useSettingValue("enableLegacyCallsVoip");
const sdkConfigEcOnly = useMemo(() => {
return SdkConfig.get("element_call").use_exclusively; return SdkConfig.get("element_call").use_exclusively;
}, []); }, []);
const useElementCallExclusively = !enableLegacyCallsVoip || sdkConfigEcOnly;
const serverIsConfiguredForElementCall = useEventEmitterState( const serverIsConfiguredForElementCall = useEventEmitterState(
CallStore.instance, CallStore.instance,
@@ -175,13 +176,13 @@ export const useRoomCall = (
// 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[] => {
const options: PlatformCallType[] = []; const options: PlatformCallType[] = [];
if (groupCallsEnabled) { if (!SdkConfig.get("element_call").disable) {
if (hasGroupCall || mayCreateElementCalls) {
options.push(PlatformCallType.ElementCall);
}
if (useElementCallExclusively && !hasJitsiWidget) { if (useElementCallExclusively && !hasJitsiWidget) {
return [PlatformCallType.ElementCall]; return [PlatformCallType.ElementCall];
} }
if (hasGroupCall || mayCreateElementCalls) {
options.push(PlatformCallType.ElementCall);
}
} }
if (memberCount <= 2) { if (memberCount <= 2) {
options.push(PlatformCallType.LegacyCall); options.push(PlatformCallType.LegacyCall);
@@ -197,7 +198,6 @@ export const useRoomCall = (
memberCount, memberCount,
mayEditWidgets, mayEditWidgets,
hasJitsiWidget, hasJitsiWidget,
groupCallsEnabled,
hasGroupCall, hasGroupCall,
mayCreateElementCalls, mayCreateElementCalls,
useElementCallExclusively, useElementCallExclusively,
+8 -4
View File
@@ -495,6 +495,7 @@
"integration_manager": "Integration manager", "integration_manager": "Integration manager",
"joined": "Joined", "joined": "Joined",
"labs": "Labs", "labs": "Labs",
"legacy_voice_and_video_settings": "Matrix 1:1 Calls (Legacy)",
"legal": "Legal", "legal": "Legal",
"light": "Light", "light": "Light",
"loading": "Loading…", "loading": "Loading…",
@@ -1535,7 +1536,6 @@
"required_msc_support": "Requires MSC4429 (Profile Updates for Legacy Sync)" "required_msc_support": "Requires MSC4429 (Profile Updates for Legacy Sync)"
}, },
"feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.", "feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.",
"group_calls": "New group call experience",
"group_developer": "Developer", "group_developer": "Developer",
"group_encryption": "Encryption", "group_encryption": "Encryption",
"group_experimental": "Experimental", "group_experimental": "Experimental",
@@ -2995,21 +2995,25 @@
"audio_output": "Audio Output", "audio_output": "Audio Output",
"audio_output_empty": "No Audio Outputs detected", "audio_output_empty": "No Audio Outputs detected",
"auto_gain_control": "Automatic gain control", "auto_gain_control": "Automatic gain control",
"connection_section": "Connection",
"dialog_title": "<strong>Settings:</strong> Voice & Video", "dialog_title": "<strong>Settings:</strong> Voice & Video",
"echo_cancellation": "Echo cancellation", "echo_cancellation": "Echo cancellation",
"echo_cancellation_description": "Removes echo from your microphone input during calls. This setting also applies to Element Call.", "echo_cancellation_description": "Removes echo from your microphone input during calls. This setting also applies to Element Call.",
"enable_fallback_ice_server": "Allow fallback call assist server (%(server)s)", "enable_fallback_ice_server": "Allow fallback call assist server (%(server)s)",
"enable_fallback_ice_server_description": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", "enable_fallback_ice_server_description": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.",
"enable_legacy_calls": "Enable Legacy voice/video calls",
"enable_legacy_calls_description": "Applies to Jitsi and 1:1 WebRTC calls",
"mirror_local_feed": "Mirror local video feed", "mirror_local_feed": "Mirror local video feed",
"missing_permissions_prompt": "Missing media permissions, click the button below to request.", "missing_permissions_prompt": "To participate in calls, give %(brand)s permission to access your audio and video device.",
"noise_suppression": "Noise suppression", "noise_suppression": "Noise suppression",
"noise_suppression_description": "Reduces background noise in your microphone input during calls. This setting also applies to Element Call.", "noise_suppression_description": "Reduces background noise in your microphone input during calls. This setting also applies to Element Call.",
"request_permissions": "Request media permissions", "permissions": "Allow microphone and camera access",
"request_permissions": "Request",
"title": "Voice & Video", "title": "Voice & Video",
"video_input_empty": "No Webcams detected", "video_input_empty": "No Webcams detected",
"video_section": "Video settings", "video_section": "Video settings",
"voice_agc": "Automatically adjust the microphone volume", "voice_agc": "Automatically adjust the microphone volume",
"voice_input_description": "The audio input configured here is used for voice messages and calls (Legacy/1:1).",
"voice_input_section": "Audio Input",
"voice_processing": "Voice processing", "voice_processing": "Voice processing",
"voice_section": "Voice settings" "voice_section": "Voice settings"
}, },
+7 -10
View File
@@ -219,7 +219,6 @@ export interface Settings {
"feature_sliding_sync": IBaseSetting<boolean>; "feature_sliding_sync": IBaseSetting<boolean>;
"feature_simplified_sliding_sync": IFeature; "feature_simplified_sliding_sync": IFeature;
"feature_element_call_video_rooms": IFeature; "feature_element_call_video_rooms": IFeature;
"feature_group_calls": IFeature;
"feature_disable_call_per_sender_encryption": IFeature; "feature_disable_call_per_sender_encryption": IFeature;
"feature_location_share_live": IFeature; "feature_location_share_live": IFeature;
"feature_dynamic_room_predecessors": IFeature; "feature_dynamic_room_predecessors": IFeature;
@@ -282,6 +281,7 @@ export interface Settings {
"useSystemFont": IBaseSetting<boolean>; "useSystemFont": IBaseSetting<boolean>;
"systemFont": IBaseSetting<string>; "systemFont": IBaseSetting<string>;
"webRtcAllowPeerToPeer": IBaseSetting<boolean>; "webRtcAllowPeerToPeer": IBaseSetting<boolean>;
"enableLegacyCallsVoip": IBaseSetting<boolean>;
"webrtc_audiooutput": IBaseSetting<string>; "webrtc_audiooutput": IBaseSetting<string>;
"webrtc_audioinput": IBaseSetting<string>; "webrtc_audioinput": IBaseSetting<string>;
"webrtc_videoinput": IBaseSetting<string>; "webrtc_videoinput": IBaseSetting<string>;
@@ -587,15 +587,6 @@ export const SETTINGS: Settings = {
controller: new ReloadOnChangeController(), controller: new ReloadOnChangeController(),
default: false, default: false,
}, },
"feature_group_calls": {
isFeature: true,
labsGroup: LabGroup.VoiceAndVideo,
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
displayName: _td("labs|group_calls"),
controller: new ReloadOnChangeController(),
default: false,
},
"feature_disable_call_per_sender_encryption": { "feature_disable_call_per_sender_encryption": {
isFeature: true, isFeature: true,
labsGroup: LabGroup.VoiceAndVideo, labsGroup: LabGroup.VoiceAndVideo,
@@ -980,6 +971,12 @@ export const SETTINGS: Settings = {
displayName: _td("settings|appearance|custom_font_name"), displayName: _td("settings|appearance|custom_font_name"),
controller: new SystemFontController(), controller: new SystemFontController(),
}, },
"enableLegacyCallsVoip": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td("settings|voip|enable_legacy_calls"),
description: _td("settings|voip|enable_legacy_calls_description"),
default: true,
},
"webRtcAllowPeerToPeer": { "webRtcAllowPeerToPeer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td("settings|voip|allow_p2p"), displayName: _td("settings|voip|allow_p2p"),
@@ -31,7 +31,7 @@ describe("recordClientInformation()", () => {
const sdkConfig: DeepReadonly<IConfigOptions> = { const sdkConfig: DeepReadonly<IConfigOptions> = {
...DEFAULTS, ...DEFAULTS,
brand: "Test Brand", brand: "Test Brand",
element_call: { use_exclusively: false, brand: "Element Call" }, element_call: { brand: "Element Call" },
}; };
const platform = { const platform = {
+1 -1
View File
@@ -135,7 +135,7 @@ export function useMockedCalls() {
* Enables the feature flags required for call tests. * Enables the feature flags required for call tests.
*/ */
export function enableCalls(): { enabledSettings: Set<string> } { export function enableCalls(): { enabledSettings: Set<string> } {
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]); const enabledSettings = new Set(["feature_video_rooms", "feature_element_call_video_rooms"]);
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => {
if (settingName.startsWith("feature_")) return enabledSettings.has(settingName); if (settingName.startsWith("feature_")) return enabledSettings.has(settingName);
if (settingName === "activeCallRoomIds") return []; if (settingName === "activeCallRoomIds") return [];
@@ -68,7 +68,6 @@ describe("<LoggedInView />", () => {
hideToSRUsers: false, hideToSRUsers: false,
config: { config: {
brand: "Test", brand: "Test",
element_call: {},
}, },
currentRoomId: "", currentRoomId: "",
currentUserId: "@bob:server", currentUserId: "@bob:server",
@@ -242,7 +242,6 @@ describe("<MatrixChat />", () => {
brand: "Test", brand: "Test",
help_url: "help_url", help_url: "help_url",
help_encryption_url: "help_encryption_url", help_encryption_url: "help_encryption_url",
element_call: {},
feedback: { feedback: {
existing_issues_url: "https://feedback.org/existing", existing_issues_url: "https://feedback.org/existing",
new_issue_url: "https://feedback.org/new", new_issue_url: "https://feedback.org/new",
@@ -146,10 +146,7 @@ describe("<RoomSettingsDialog />", () => {
}); });
}); });
it("renders voip settings tab when enabled", () => { it("always renders voip settings tab when enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_group_calls",
);
getComponent(); getComponent();
expect(screen.getByTestId("settings-tab-ROOM_VOIP_TAB")).toBeInTheDocument(); expect(screen.getByTestId("settings-tab-ROOM_VOIP_TAB")).toBeInTheDocument();
}); });
@@ -32,6 +32,35 @@ NodeList [
General General
</span> </span>
</li>, </li>,
<li
aria-controls="mx_tabpanel_ROOM_VOIP_TAB"
aria-labelledby="mx_tabpanel_ROOM_VOIP_TAB_label"
aria-selected="false"
class="mx_AccessibleButton mx_TabbedView_tabLabel"
data-testid="settings-tab-ROOM_VOIP_TAB"
role="tab"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M8.929 15.1a13.6 13.6 0 0 0 4.654 3.066q2.62 1.036 5.492.923h.008l.003-.004.003-.002-.034-3.124-3.52-.483-1.791 1.792-.645-.322a13.5 13.5 0 0 1-3.496-2.52 13.4 13.4 0 0 1-2.52-3.496l-.322-.645 1.792-1.791-.483-3.52-3.123-.033-.003.002-.003.004v.002a13.65 13.65 0 0 0 .932 5.492A13.4 13.4 0 0 0 8.93 15.1m3.92 4.926a15.6 15.6 0 0 1-5.334-3.511 15.4 15.4 0 0 1-3.505-5.346 15.6 15.6 0 0 1-1.069-6.274 1.93 1.93 0 0 1 .589-1.366c.366-.366.84-.589 1.386-.589h.01l3.163.035a1.96 1.96 0 0 1 1.958 1.694v.005l.487 3.545v.003c.043.297.025.605-.076.907a2 2 0 0 1-.485.773l-.762.762a11.3 11.3 0 0 0 1.806 2.348 11.4 11.4 0 0 0 2.348 1.806l.762-.762a2 2 0 0 1 .774-.485c.302-.1.61-.118.907-.076l3.553.487a1.96 1.96 0 0 1 1.694 1.958l.034 3.174c0 .546-.223 1.02-.588 1.386-.36.36-.827.582-1.363.588a15.3 15.3 0 0 1-6.29-1.062"
fill-rule="evenodd"
/>
</svg>
<span
class="mx_TabbedView_tabLabel_text"
id="mx_tabpanel_ROOM_VOIP_TAB_label"
>
Voice & Video
</span>
</li>,
<li <li
aria-controls="mx_tabpanel_ROOM_SECURITY_TAB" aria-controls="mx_tabpanel_ROOM_SECURITY_TAB"
aria-labelledby="mx_tabpanel_ROOM_SECURITY_TAB_label" aria-labelledby="mx_tabpanel_ROOM_SECURITY_TAB_label"
@@ -409,11 +409,7 @@ describe("RoomHeader", () => {
describe("group call enabled", () => { describe("group call enabled", () => {
beforeEach(async () => { beforeEach(async () => {
SdkConfig.put({ SdkConfig.put({});
features: {
feature_group_calls: true,
},
});
// Enable Element Call // Enable Element Call
client._unstable_getRTCTransports = jest client._unstable_getRTCTransports = jest
.fn() .fn()
@@ -478,12 +474,10 @@ describe("RoomHeader", () => {
const user = userEvent.setup(); const user = userEvent.setup();
mockRoomMembers(room, 3); mockRoomMembers(room, 3);
SdkConfig.add({ SdkConfig.add({
element_call: { disable: true }, // This test is about Jitsi widget re-pinning, not Element Call
setting_defaults: { setting_defaults: {
[UIFeature.Widgets]: true, [UIFeature.Widgets]: true,
}, },
features: {
feature_group_calls: false,
},
}); });
// allow calls // allow calls
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
@@ -44,39 +44,17 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button> </button>
<button <button
aria-disabled="false" aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Video call" aria-label="Video call"
class="_icon-button_1215g_8" class="_icon-button_1215g_8"
data-kind="primary" data-kind="primary"
data-state="closed"
id="radix-react-use-id-1"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
tabindex="0" tabindex="0"
> type="button"
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="react-use-id-1"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
</div>
</button>
<button
aria-disabled="false"
aria-label="Voice call"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
> >
<div <div
class="_indicator-icon_147l5_17" class="_indicator-icon_147l5_17"
@@ -89,6 +67,38 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="1em" width="1em"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
</div>
</button>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Voice call"
class="_icon-button_1215g_8"
data-kind="primary"
data-state="closed"
id="radix-react-use-id-3"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="react-use-id-4"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727" d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
@@ -98,7 +108,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button> </button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby="react-use-id-3" aria-labelledby="react-use-id-5"
class="_icon-button_1215g_8" class="_icon-button_1215g_8"
data-kind="primary" data-kind="primary"
role="button" role="button"
@@ -125,7 +135,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button> </button>
<button <button
aria-label="Room info" aria-label="Room info"
aria-labelledby="react-use-id-4" aria-labelledby="react-use-id-6"
class="_icon-button_1215g_8" class="_icon-button_1215g_8"
data-kind="primary" data-kind="primary"
role="button" role="button"
@@ -23,7 +23,7 @@ import userEvent from "@testing-library/user-event";
import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab"; import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils"; import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../../../src/settings/SettingsStore"; import SdkConfig from "../../../../../../../src/SdkConfig";
import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../../../src/call-types"; import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../../../src/call-types";
describe("RolesRoomSettingsTab", () => { describe("RolesRoomSettingsTab", () => {
@@ -75,11 +75,13 @@ describe("RolesRoomSettingsTab", () => {
describe("Element Call", () => { describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => { const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => { SdkConfig.put({ element_call: { disable: !val } });
if (name === "feature_group_calls") return val;
});
}; };
afterEach(() => {
SdkConfig.reset();
});
const getStartCallSelect = (tab: RenderResult): HTMLElement => { const getStartCallSelect = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Start Element Call calls']")!; return tab.container.querySelector("select[label='Start Element Call calls']")!;
}; };
+33 -6
View File
@@ -36,10 +36,29 @@ import createRoom, {
canEncryptToAllUsers, canEncryptToAllUsers,
waitForRoomEncryption, waitForRoomEncryption,
} from "../../src/createRoom"; } from "../../src/createRoom";
import SettingsStore from "../../src/settings/SettingsStore";
import { ElementCallMemberEventType } from "../../src/call-types"; import { ElementCallMemberEventType } from "../../src/call-types";
import DMRoomMap from "../../src/utils/DMRoomMap"; import DMRoomMap from "../../src/utils/DMRoomMap";
import { PreferredRoomVersions } from "../../src/utils/PreferredRoomVersions"; import { PreferredRoomVersions } from "../../src/utils/PreferredRoomVersions";
import SdkConfig from "../../src/SdkConfig";
/**
* This should be the same as
* { POWER_LEVEL_EVENTS_DEFAULT, [ElementCallMemberEventType.name]: 0 }
* to get the most regression coverage we provide the string based snapshot here.
*/
const POWER_LEVELS_WITH_CALL_MEMBER = {
// Default events power levels we always expect
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
// Custom rtc.member event we expect to be 0
[ElementCallMemberEventType.name]: 0,
};
describe("createRoom", () => { describe("createRoom", () => {
mockPlatformPeg(); mockPlatformPeg();
@@ -51,7 +70,10 @@ describe("createRoom", () => {
DMRoomMap.makeShared(client); DMRoomMap.makeShared(client);
}); });
afterEach(() => jest.clearAllMocks()); afterEach(() => {
jest.clearAllMocks();
SdkConfig.reset();
});
it("creates a private room", async () => { it("creates a private room", async () => {
await createRoom(client, { createOpts: { preset: Preset.PrivateChat } }); await createRoom(client, { createOpts: { preset: Preset.PrivateChat } });
@@ -63,6 +85,7 @@ describe("createRoom", () => {
{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }, { state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } },
{ type: "m.room.history_visibility", content: { history_visibility: "invited" } }, { type: "m.room.history_visibility", content: { history_visibility: "invited" } },
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
}); });
@@ -83,6 +106,7 @@ describe("createRoom", () => {
}, },
{ type: "m.room.history_visibility", content: { history_visibility: "invited" } }, { type: "m.room.history_visibility", content: { history_visibility: "invited" } },
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
}); });
@@ -112,6 +136,7 @@ describe("createRoom", () => {
{ type: "m.room.history_visibility", content: { history_visibility: "invited" } }, { type: "m.room.history_visibility", content: { history_visibility: "invited" } },
// Room name is NOT included, since it needs to be encrypted. // Room name is NOT included, since it needs to be encrypted.
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
// And the room name, topic and avatar are set later // And the room name, topic and avatar are set later
@@ -155,6 +180,7 @@ describe("createRoom", () => {
{ type: "m.room.history_visibility", content: { history_visibility: "invited" } }, { type: "m.room.history_visibility", content: { history_visibility: "invited" } },
// Room name is NOT included, since it needs to be encrypted. // Room name is NOT included, since it needs to be encrypted.
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
// And the avatar is set later // And the avatar is set later
@@ -188,6 +214,7 @@ describe("createRoom", () => {
{ type: "m.room.history_visibility", content: { history_visibility: "invited" } }, { type: "m.room.history_visibility", content: { history_visibility: "invited" } },
// Room name is NOT included, since it needs to be encrypted. // Room name is NOT included, since it needs to be encrypted.
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
// And the room name, topic and avatar were not set since we didn't // And the room name, topic and avatar were not set since we didn't
@@ -228,6 +255,7 @@ describe("createRoom", () => {
{ type: "m.space.parent", state_key: parentSpace.roomId, content: { canonical: true, via: [] } }, { type: "m.space.parent", state_key: parentSpace.roomId, content: { canonical: true, via: [] } },
{ type: "m.room.history_visibility", content: { history_visibility: "invited" } }, { type: "m.room.history_visibility", content: { history_visibility: "invited" } },
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
}); });
@@ -238,6 +266,7 @@ describe("createRoom", () => {
preset: "public_chat", preset: "public_chat",
visibility: "private", visibility: "private",
initial_state: [{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }], initial_state: [{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
}); });
@@ -249,6 +278,7 @@ describe("createRoom", () => {
visibility: "private", visibility: "private",
topic: "My topic", topic: "My topic",
initial_state: [{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }], initial_state: [{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
}); });
@@ -266,6 +296,7 @@ describe("createRoom", () => {
{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }, { state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } },
{ type: "m.space.parent", state_key: parentSpace.roomId, content: { canonical: true, via: [] } }, { type: "m.space.parent", state_key: parentSpace.roomId, content: { canonical: true, via: [] } },
], ],
power_level_content_override: { events: POWER_LEVELS_WITH_CALL_MEMBER },
}); });
}); });
@@ -324,10 +355,6 @@ describe("createRoom", () => {
}); });
it("correctly sets up MSC3401 power levels", async () => { it("correctly sets up MSC3401 power levels", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
if (name === "feature_group_calls") return true;
});
await createRoom(client, {}); await createRoom(client, {});
const callMemberPower = const callMemberPower =
@@ -22,9 +22,8 @@ import { ScopedRoomContextProvider } from "../../../src/contexts/ScopedRoomConte
import RoomContext, { type RoomContextType } from "../../../src/contexts/RoomContext"; import RoomContext, { type RoomContextType } from "../../../src/contexts/RoomContext";
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider"; import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
import type LegacyCallHandler from "../../../src/LegacyCallHandler"; import type LegacyCallHandler from "../../../src/LegacyCallHandler";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass";
import SettingsStore from "../../../src/settings/SettingsStore";
import { CallStore } from "../../../src/stores/CallStore"; import { CallStore } from "../../../src/stores/CallStore";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass";
describe("useRoomCall", () => { describe("useRoomCall", () => {
const client = getMockClientWithEventEmitter({ const client = getMockClientWithEventEmitter({
@@ -62,11 +61,6 @@ describe("useRoomCall", () => {
jest.spyOn(SDKContextClass.instance, "legacyCallHandler", "get").mockReturnValue( jest.spyOn(SDKContextClass.instance, "legacyCallHandler", "get").mockReturnValue(
callHandler as unknown as LegacyCallHandler, 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(() => { afterEach(() => {
+6 -1
View File
@@ -390,7 +390,12 @@ The VoIP and Jitsi options are:
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:
- `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 enable legacy 1:1 calls or Jitsi calls.
It is a misconfiguration if its set to true while `element_call.disable` is also true!
Defaults to `false`.
- `disabled`: A boolean flag specifying whether Element Call should be disabled.
If it is disabled `element_call.use_exclusively` has no effect!
Defaults to `false`.
- `brand`: Optional name for the app. Defaults to `Element Call`. This is - `brand`: Optional name for the app. Defaults to `Element Call`. This is
used throughout the application in various strings/locations. used throughout the application in various strings/locations.
- `guest_spa_url`: Optional URL for an Element Call single-page app (SPA), - `guest_spa_url`: Optional URL for an Element Call single-page app (SPA),
-6
View File
@@ -81,12 +81,6 @@ Enables support for video rooms that use Element Call rather than Jitsi, and cau
This flag will not have any effect unless `feature_video_rooms` is also enabled. This flag will not have any effect unless `feature_video_rooms` is also enabled.
## New group call experience (`feature_group_calls`) [In Development]
This feature allows users to place native [MSC3401](https://github.com/matrix-org/matrix-spec-proposals/pull/3401) group calls in compatible rooms, using Element Call.
If you're enabling this at the deployment level, you may also want to reference the docs for the `element_call` config section.
## Disable per-sender encryption for Element Call (`feature_disable_call_per_sender_encryption`) ## Disable per-sender encryption for Element Call (`feature_disable_call_per_sender_encryption`)
The default for embedded Element Call in Element Web is per-participant encryption. The default for embedded Element Call in Element Web is per-participant encryption.
+2 -1
View File
@@ -114,8 +114,9 @@ export interface WebConfigJson {
}; };
element_call?: { element_call?: {
guest_spa_url?: string; guest_spa_url?: string;
use_exclusively?: boolean;
brand?: string; brand?: string;
use_exclusively?: boolean;
disable?: boolean;
}; };
logout_redirect_url?: string; logout_redirect_url?: string;