Show all labs even if incompatible, with appropriate tooltip explaining requirements (#10369)

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
pull/28788/head^2
Michael Telatynski 2023-03-15 08:37:41 +00:00 committed by GitHub
parent 209b65243a
commit e3930fb8b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 156 additions and 120 deletions

View File

@ -284,7 +284,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};
private onShareClick = (e: React.MouseEvent): void => {
private onShareClick = (e: ButtonEvent): void => {
e.preventDefault();
Modal.createDialog(ShareDialog, {
target: this.props.mxEvent,

View File

@ -22,6 +22,7 @@ import { _t } from "../../../languageHandler";
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
import { defaultWatchManager } from "../../../settings/Settings";
interface IProps {
// The setting must be a boolean
@ -32,14 +33,14 @@ interface IProps {
isExplicit?: boolean;
// XXX: once design replaces all toggles make this the default
useCheckbox?: boolean;
disabled?: boolean;
disabledDescription?: string;
hideIfCannotSet?: boolean;
onChange?(checked: boolean): void;
}
interface IState {
value: boolean;
/** true if `SettingsStore.isEnabled` returned false. */
disabled: boolean;
}
export default class SettingsFlag extends React.Component<IProps, IState> {
@ -47,19 +48,43 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
super(props);
this.state = {
value: SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
),
value: this.getSettingValue(),
disabled: this.isSettingDisabled(),
};
}
public componentDidMount(): void {
defaultWatchManager.watchSetting(this.props.name, this.props.roomId ?? null, this.onSettingChange);
}
public componentWillUnmount(): void {
defaultWatchManager.unwatchSetting(this.onSettingChange);
}
private getSettingValue(): boolean {
return SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId ?? null,
this.props.isExplicit,
);
}
private isSettingDisabled(): boolean {
return !SettingsStore.isEnabled(this.props.name);
}
private onSettingChange = (): void => {
this.setState({
value: this.getSettingValue(),
disabled: this.isSettingDisabled(),
});
};
private onChange = async (checked: boolean): Promise<void> => {
await this.save(checked);
this.setState({ value: checked });
if (this.props.onChange) this.props.onChange(checked);
this.props.onChange?.(checked);
};
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
@ -86,19 +111,11 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
const description = SettingsStore.getDescription(this.props.name);
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
let disabledDescription: JSX.Element | null = null;
if (this.props.disabled && this.props.disabledDescription) {
disabledDescription = <div className="mx_SettingsFlag_microcopy">{this.props.disabledDescription}</div>;
}
const disabled = this.state.disabled || !canChange;
if (this.props.useCheckbox) {
return (
<StyledCheckbox
checked={this.state.value}
onChange={this.checkBoxOnChange}
disabled={this.props.disabled || !canChange}
>
<StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={disabled}>
{label}
</StyledCheckbox>
);
@ -117,18 +134,18 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
w: (sub) => (
<span className="mx_SettingsTab_microcopy_warning">{sub}</span>
),
description: description,
description,
},
)
: description}
</div>
)}
{disabledDescription}
</label>
<ToggleSwitch
checked={this.state.value}
onChange={this.onChange}
disabled={this.props.disabled || !canChange}
disabled={disabled}
tooltip={disabled ? SettingsStore.disabledMessage(this.props.name) : undefined}
title={label}
/>
</div>

View File

@ -23,41 +23,18 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
import SettingsFlag from "../../../elements/SettingsFlag";
import { defaultWatchManager, LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { EnhancedMap } from "../../../../../utils/maps";
import { arrayHasDiff } from "../../../../../utils/arrays";
interface State {
labs: string[];
betas: string[];
}
export default class LabsUserSettingsTab extends React.Component<{}, State> {
private readonly features = SettingsStore.getFeatureSettingNames();
export default class LabsUserSettingsTab extends React.Component<{}> {
private readonly labs: string[];
private readonly betas: string[];
public constructor(props: {}) {
super(props);
this.state = {
betas: [],
labs: [],
};
}
public componentDidMount(): void {
this.features.forEach((feature) => {
defaultWatchManager.watchSetting(feature, null, this.onChange);
});
this.onChange();
}
public componentWillUnmount(): void {
defaultWatchManager.unwatchSetting(this.onChange);
}
private onChange = (): void => {
const features = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.isEnabled(f));
const [_labs, betas] = features.reduce(
const features = SettingsStore.getFeatureSettingNames();
const [labs, betas] = features.reduce(
(arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
return arr;
@ -65,18 +42,20 @@ export default class LabsUserSettingsTab extends React.Component<{}, State> {
[[], []] as [string[], string[]],
);
const labs = SdkConfig.get("show_labs_settings") ? _labs : [];
if (arrayHasDiff(labs, this.state.labs) || arrayHasDiff(betas, this.state.betas)) {
this.setState({ labs, betas });
this.labs = labs;
this.betas = betas;
if (!SdkConfig.get("show_labs_settings")) {
this.labs = [];
}
};
}
public render(): React.ReactNode {
let betaSection: JSX.Element | undefined;
if (this.state.betas.length) {
if (this.betas.length) {
betaSection = (
<div data-testid="labs-beta-section" className="mx_SettingsTab_section">
{this.state.betas.map((f) => (
{this.betas.map((f) => (
<BetaCard key={f} featureId={f} />
))}
</div>
@ -84,9 +63,9 @@ export default class LabsUserSettingsTab extends React.Component<{}, State> {
}
let labsSections: JSX.Element | undefined;
if (this.state.labs.length) {
if (this.labs.length) {
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
this.state.labs.forEach((f) => {
this.labs.forEach((f) => {
groups
.getOrCreate(SettingsStore.getLabGroup(f), [])
.push(<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />);

View File

@ -29,7 +29,6 @@ import { UserTab } from "../../../dialogs/UserTab";
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../../../dispatcher/actions";
import SdkConfig from "../../../../../SdkConfig";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage";
interface IProps {
@ -37,7 +36,6 @@ interface IProps {
}
interface IState {
disablingReadReceiptsSupported: boolean;
autocompleteDelay: string;
readMarkerInViewThresholdMs: string;
readMarkerOutOfViewThresholdMs: string;
@ -50,10 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
private static KEYBINDINGS_SETTINGS = ["ctrlFForSearch"];
private static PRESENCE_SETTINGS = [
"sendTypingNotifications",
// sendReadReceipts - handled specially due to server needing support
];
private static PRESENCE_SETTINGS = ["sendReadReceipts", "sendTypingNotifications"];
private static COMPOSER_SETTINGS = [
"MessageComposerInput.autoReplaceEmoji",
@ -101,7 +96,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
super(props);
this.state = {
disablingReadReceiptsSupported: false,
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
SettingLevel.DEVICE,
@ -114,16 +108,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
}
public async componentDidMount(): Promise<void> {
const cli = MatrixClientPeg.get();
this.setState({
disablingReadReceiptsSupported:
(await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) ||
(await cli.isVersionSupported("v1.4")),
});
}
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ autocompleteDelay: e.target.value });
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
@ -140,10 +124,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
private renderGroup(settingIds: string[], level = SettingLevel.ACCOUNT): React.ReactNodeArray {
return settingIds.map((i) => {
const disabled = !SettingsStore.isEnabled(i);
return <SettingsFlag key={i} name={i} level={level} disabled={disabled} />;
});
return settingIds.map((i) => <SettingsFlag key={i} name={i} level={level} />);
}
private onKeyboardShortcutsClicked = (): void => {
@ -205,14 +186,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
<span className="mx_SettingsTab_subsectionText">
{_t("Share your activity and status with others.")}
</span>
<SettingsFlag
disabled={
!this.state.disablingReadReceiptsSupported && SettingsStore.getValue("sendReadReceipts") // Make sure the feature can always be enabled
}
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
name="sendReadReceipts"
level={SettingLevel.ACCOUNT}
/>
{this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
</div>

View File

@ -939,6 +939,7 @@
"Yes, the chat timeline is displayed alongside the video.": "Yes, the chat timeline is displayed alongside the video.",
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
"Requires your server to support the stable version of MSC3827": "Requires your server to support the stable version of MSC3827",
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
"Report to moderators": "Report to moderators",
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
@ -962,7 +963,9 @@
"Polls history": "Polls history",
"View a list of polls in a room. (Under active development)": "View a list of polls in a room. (Under active development)",
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
"Requires your server to support MSC3030": "Requires your server to support MSC3030",
"Send read receipts": "Send read receipts",
"Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.",
"Sliding Sync mode": "Sliding Sync mode",
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
"Element Call video rooms": "Element Call video rooms",
@ -979,7 +982,6 @@
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.",
"Rust cryptography implementation": "Rust cryptography implementation",
"Under active development. Can currently only be enabled via config.json": "Under active development. Can currently only be enabled via config.json",
"Font size": "Font size",
"Use custom size": "Use custom size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
@ -1056,6 +1058,8 @@
"Always show the window menu bar": "Always show the window menu bar",
"Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close",
"Enable hardware acceleration": "Enable hardware acceleration",
"Can currently only be enabled via config.json": "Can currently only be enabled via config.json",
"Log out and back in to disable": "Log out and back in to disable",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading logs": "Uploading logs",
@ -1621,7 +1625,6 @@
"Displaying time": "Displaying time",
"Presence": "Presence",
"Share your activity and status with others.": "Share your activity and status with others.",
"Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.",
"Composer": "Composer",
"Code blocks": "Code blocks",
"Images, GIFs and videos": "Images, GIFs and videos",

View File

@ -227,9 +227,13 @@ export const SETTINGS: { [setting: string]: ISetting } = {
displayName: _td("Explore public spaces in the new search dialog"),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new ServerSupportUnstableFeatureController("feature_exploring_public_spaces", defaultWatchManager, [
"org.matrix.msc3827.stable",
]),
controller: new ServerSupportUnstableFeatureController(
"feature_exploring_public_spaces",
defaultWatchManager,
["org.matrix.msc3827.stable"],
undefined,
_td("Requires your server to support the stable version of MSC3827"),
),
},
"feature_msc3531_hide_messages_pending_moderation": {
isFeature: true,
@ -373,9 +377,13 @@ export const SETTINGS: { [setting: string]: ISetting } = {
displayName: _td("Jump to date (adds /jumptodate and jump to date headers)"),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new ServerSupportUnstableFeatureController("feature_jump_to_date", defaultWatchManager, [
"org.matrix.msc3030",
]),
controller: new ServerSupportUnstableFeatureController(
"feature_jump_to_date",
defaultWatchManager,
["org.matrix.msc3030"],
undefined,
_td("Requires your server to support MSC3030"),
),
},
"RoomList.backgroundImage": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@ -385,6 +393,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Send read receipts"),
default: true,
controller: new ServerSupportUnstableFeatureController(
"sendReadReceipts",
defaultWatchManager,
["org.matrix.msc2285.stable"],
"v1.4",
_td("Your server doesn't support disabling sending read receipts."),
true,
),
},
"feature_sliding_sync": {
isFeature: true,
@ -482,7 +498,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
labsGroup: LabGroup.Developer,
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td("Rust cryptography implementation"),
description: _td("Under active development. Can currently only be enabled via config.json"),
description: _td("Under active development."),
// shouldWarn: true,
default: false,
controller: new RustCryptoSdkController(),

View File

@ -325,7 +325,9 @@ export default class SettingsStore {
/**
* Determines if a setting is enabled.
* If a setting is disabled then it should be hidden from the user.
* If a setting is disabled then it should normally be hidden from the user to de-clutter the user interface.
* This rule is intentionally ignored for labs flags to unveil what features are available with
* the right server support.
* @param {string} settingName The setting to look up.
* @return {boolean} True if the setting is enabled.
*/
@ -334,6 +336,18 @@ export default class SettingsStore {
return !SETTINGS[settingName].controller?.settingDisabled ?? true;
}
/**
* Retrieves the reason a setting is disabled if one is assigned.
* If a setting is not disabled, or no reason is given by the `SettingController`,
* this will return undefined.
* @param {string} settingName The setting to look up.
* @return {string} The reason the setting is disabled.
*/
public static disabledMessage(settingName: string): string | undefined {
const disabled = SETTINGS[settingName].controller?.settingDisabled;
return typeof disabled === "string" ? disabled : undefined;
}
/**
* Gets the value of a setting. The room ID is optional if the setting is not to
* be applied to any particular room, otherwise it should be supplied.

View File

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { _t } from "../../languageHandler";
import SettingController from "./SettingController";
export default class RustCryptoSdkController extends SettingController {
public get settingDisabled(): boolean {
public get settingDisabled(): boolean | string {
// Currently this can only be changed via config.json. In future, we'll allow the user to *enable* this setting
// via labs, which will migrate their existing device to the rust-sdk implementation.
return true;
return _t("Can currently only be enabled via config.json");
}
}

View File

@ -32,6 +32,8 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
private readonly settingName: string,
private readonly watchers: WatchManager,
private readonly unstableFeatures: string[],
private readonly stableVersion?: string,
private readonly disabledMessage?: string,
private readonly forcedValue: any = false,
) {
super();
@ -53,10 +55,16 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
protected async initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient): Promise<void> {
this.disabled = true;
let supported = true;
if (this.stableVersion && (await this.client.isVersionSupported(this.stableVersion))) {
this.disabled = false;
return;
}
for (const feature of this.unstableFeatures) {
supported = await this.client.doesServerSupportUnstableFeature(feature);
if (!supported) break;
}
this.disabled = !supported;
}
@ -72,7 +80,10 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
return null; // no override
}
public get settingDisabled(): boolean {
return this.disabled;
public get settingDisabled(): boolean | string {
if (this.disabled) {
return this.disabledMessage ?? true;
}
return false;
}
}

View File

@ -69,8 +69,9 @@ export default abstract class SettingController {
/**
* Gets whether the setting has been disabled due to this controller.
* Can also return a string with the reason the setting is disabled.
*/
public get settingDisabled(): boolean {
public get settingDisabled(): boolean | string {
return false;
}
}

View File

@ -20,6 +20,7 @@ import { SettingLevel } from "../SettingLevel";
import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog";
import Modal from "../../Modal";
import SettingsStore from "../SettingsStore";
import { _t } from "../../languageHandler";
export default class SlidingSyncController extends SettingController {
public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
@ -32,8 +33,12 @@ export default class SlidingSyncController extends SettingController {
PlatformPeg.get()?.reload();
}
public get settingDisabled(): boolean {
public get settingDisabled(): boolean | string {
// Cannot be disabled once enabled, user has been warned and must log out and back in.
return SettingsStore.getValue("feature_sliding_sync");
if (SettingsStore.getValue("feature_sliding_sync")) {
return _t("Log out and back in to disable");
}
return false;
}
}

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from "react";
import { render } from "@testing-library/react";
import { render, waitFor } from "@testing-library/react";
import { defer } from "matrix-js-sdk/src/utils";
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
@ -28,7 +28,7 @@ import {
import SdkConfig from "../../../../../../src/SdkConfig";
import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController";
describe("<SecurityUserSettingsTab />", () => {
describe("<LabsUserSettingsTab />", () => {
const sdkConfigSpy = jest.spyOn(SdkConfig, "get");
const defaultProps = {
@ -70,10 +70,10 @@ describe("<SecurityUserSettingsTab />", () => {
const { container } = render(getComponent());
const labsSections = container.getElementsByClassName("mx_SettingsTab_section");
expect(labsSections.length).toEqual(11);
expect(labsSections).toHaveLength(12);
});
it("renders a labs flag which requires unstable support once support is confirmed", async () => {
it("allow setting a labs flag which requires unstable support once support is confirmed", async () => {
// enable labs
sdkConfigSpy.mockImplementation((configName) => configName === "show_labs_settings");
@ -83,10 +83,20 @@ describe("<SecurityUserSettingsTab />", () => {
});
MatrixClientBackedController.matrixClient = cli;
const { queryByText, findByText } = render(getComponent());
const { queryByText } = render(getComponent());
expect(queryByText("Explore public spaces in the new search dialog")).toBeFalsy();
expect(
queryByText("Explore public spaces in the new search dialog")!
.closest(".mx_SettingsFlag")!
.querySelector(".mx_AccessibleButton"),
).toHaveAttribute("aria-disabled", "true");
deferred.resolve(true);
await expect(findByText("Explore public spaces in the new search dialog")).resolves.toBeDefined();
await waitFor(() => {
expect(
queryByText("Explore public spaces in the new search dialog")!
.closest(".mx_SettingsFlag")!
.querySelector(".mx_AccessibleButton"),
).toHaveAttribute("aria-disabled", "false");
});
});
});

View File

@ -22,6 +22,7 @@ import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { mockPlatformPeg, stubClient } from "../../../../../test-utils";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController";
describe("PreferencesUserSettingsTab", () => {
beforeEach(() => {
@ -36,6 +37,7 @@ describe("PreferencesUserSettingsTab", () => {
beforeEach(() => {
stubClient();
jest.spyOn(SettingsStore, "setValue");
jest.spyOn(SettingsStore, "canSetValue").mockReturnValue(true);
jest.spyOn(window, "matchMedia").mockReturnValue({ matches: false } as MediaQueryList);
});
@ -47,10 +49,12 @@ describe("PreferencesUserSettingsTab", () => {
const mockIsVersionSupported = (val: boolean) => {
const client = MatrixClientPeg.get();
jest.spyOn(client, "doesServerSupportUnstableFeature").mockResolvedValue(false);
jest.spyOn(client, "isVersionSupported").mockImplementation(async (version: string) => {
if (version === "v1.4") return val;
return false;
});
MatrixClientBackedController.matrixClient = client;
};
const mockGetValue = (val: boolean) => {
@ -98,13 +102,12 @@ describe("PreferencesUserSettingsTab", () => {
mockIsVersionSupported(false);
});
it("can be enabled", async () => {
mockGetValue(false);
it("is forcibly enabled", async () => {
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
fireEvent.click(toggle);
expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true);
await waitFor(() => {
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle).toHaveAttribute("aria-disabled", "true");
});
});
it("cannot be disabled", async () => {

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SecurityUserSettingsTab /> renders settings marked as beta as beta cards 1`] = `
exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1`] = `
<div
class="mx_SettingsTab_section"
data-testid="labs-beta-section"

View File

@ -56,6 +56,8 @@ describe("ServerSupportUnstableFeatureController", () => {
setting,
watchers,
["feature"],
undefined,
undefined,
"other_value",
);
await prepareSetting(cli, controller);

View File

@ -126,6 +126,7 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
getCapabilities: jest.fn().mockReturnValue({}),
getClientWellKnown: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
isVersionSupported: jest.fn().mockResolvedValue(false),
getVersions: jest.fn().mockResolvedValue({}),
isFallbackICEServerAllowed: jest.fn(),
});