Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@
"description": "Description",
"deselect_all": "Deselect all",
"device": "Device",
"disabled_by_homeserver": "Disabled by homeserver",
"edited": "edited",
"email_address": "Email address",
"emoji": "Emoji",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,13 @@ export const SETTINGS: Settings = {
supportedLevelsAreOrdered: true,
displayName: _td("settings|inline_url_previews_default"),
default: true,
controller: new UIFeatureController(UIFeature.URLPreviews),
controller: new RequiresSettingsController([UIFeature.URLPreviews], false, (c) => {
if (c["io.element.msc4452.preview_url"]?.enabled !== false) {
// If the capability is not listed, or explicitly true then do not disable.
return false;
}
return _t("common|disabled_by_homeserver");
}),
},
"urlPreviewsEnabled_e2ee": {
// Can only be enabled per-device to ensure neither the homeserver nor client config
Expand Down
54 changes: 47 additions & 7 deletions apps/web/src/settings/controllers/RequiresSettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,70 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import SettingController from "./SettingController";
import type { Capabilities } from "matrix-js-sdk/src/matrix";
import SettingsStore from "../SettingsStore";
import type { BooleanSettingKey } from "../Settings.tsx";
import MatrixClientBackedController from "./MatrixClientBackedController.ts";

/**
* Disables a setting & forces it's value if one or more settings are not enabled
* and/or a capability on the client check does not pass.
*/
export default class RequiresSettingsController extends SettingController {
export default class RequiresSettingsController extends MatrixClientBackedController {
public constructor(
public readonly settingNames: BooleanSettingKey[],
private forcedValue = false,
private readonly forcedValue = false,
/**
* Function to check the capabilites of the client.
* If defined this will be called when the MatrixClient is instantiated to check
* the returned capabilites.
* @returns `true` or a string if the feature is disabled by the feature, otherwise false.
*/
private readonly isCapabilityDisabled?: (caps: Capabilities) => boolean | string,
) {
super();
}

protected initMatrixClient(): void {
if (this.client && this.isCapabilityDisabled) {
// Ensure we fetch capabilies at least once.
void this.client.getCapabilities();
}
}

/**
* Checks if the `isCapabilityDisabled` function blocks the setting.
* @returns `true` or a string if the feature is disabled by the feature, otherwise false.
*/
private get isBlockedByCapabilites(): boolean | string {
if (!this.isCapabilityDisabled) {
return false;
}
// This works because the cached caps are stored forever, and we have made
// at least one call to get capaibilies.
const cachedCaps = this.client?.getCachedCapabilities();
if (!cachedCaps) {
// If we do not have any capabilites yet, then assume the setting IS blocked.
return true;
}
return this.isCapabilityDisabled(cachedCaps);
}

public get settingDisabled(): boolean | string {
if (this.settingNames.some((s) => !SettingsStore.getValue(s))) {
return true;
}
return this.isBlockedByCapabilites;
}

public getValueOverride(): any {
if (this.settingDisabled) {
// per the docs: we force a disabled state when the feature isn't active
return this.forcedValue;
}
if (this.isBlockedByCapabilites) {
return this.forcedValue;
}
return null; // no override
}

public get settingDisabled(): boolean {
return this.settingNames.some((s) => !SettingsStore.getValue(s));
}
}
1 change: 1 addition & 0 deletions apps/web/test/test-utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockResolvedValue({}),
getCachedCapabilities: jest.fn().mockResolvedValue({}),
getClientWellKnown: jest.fn().mockReturnValue({}),
waitForClientWellKnown: jest.fn().mockResolvedValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
Expand Down
1 change: 1 addition & 0 deletions apps/web/test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export function createTestClient(): MatrixClient {
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
getCapabilities: jest.fn().mockResolvedValue({}),
getCachedCapabilities: jest.fn().mockReturnValue({}),
supportsThreads: jest.fn().mockReturnValue(false),
supportsIntentionalMentions: jest.fn().mockReturnValue(false),
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog.ts
import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents";
import { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
import { ModuleApi } from "../../../../src/modules/Api";
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController.ts";

// Used by group calls
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
Expand All @@ -92,6 +93,7 @@ describe("RoomView", () => {
beforeEach(() => {
mockPlatformPeg({ reload: () => {} });
cli = mocked(stubClient());
MatrixClientBackedController.matrixClient = cli;

const roomName = (expect.getState().currentTestName ?? "").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,12 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</div>
<div
aria-labelledby="_r_1c3_"
class="_banner_193k4_8"
class="_banner_n7ud0_8"
data-type="critical"
role="status"
>
<div
class="_icon_193k4_50"
class="_icon_n7ud0_50"
>
<svg
fill="currentColor"
Expand All @@ -234,7 +234,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</svg>
</div>
<div
class="_content_193k4_38"
class="_content_n7ud0_38"
>
<p
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _title_1xryk_24"
Expand All @@ -244,7 +244,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</p>
</div>
<div
class="_actions_193k4_60"
class="_actions_n7ud0_61"
>
<button
class="_button_13vu4_8 _primaryAction_1xryk_20 _has-icon_13vu4_60"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import userEvent from "@testing-library/user-event";

import PreferencesUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/PreferencesUserSettingsTab";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { mockPlatformPeg, stubClient } from "../../../../../../test-utils";
import {
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
mockPlatformPeg,
stubClient,
} from "../../../../../../test-utils";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
import MatrixClientBackedController from "../../../../../../../src/settings/controllers/MatrixClientBackedController";
Expand All @@ -29,6 +35,10 @@ describe("PreferencesUserSettingsTab", () => {
};

it("should render", () => {
MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
...mockClientMethodsServer(),
...mockClientMethodsUser(),
});
const { asFragment } = renderTab();
expect(asFragment()).toMatchSnapshot();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import type { Capabilities } from "matrix-js-sdk/src/matrix";
import RequiresSettingsController from "../../../../src/settings/controllers/RequiresSettingsController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
import { getMockClientWithEventEmitter, mockClientMethodsServer } from "../../../test-utils";

describe("RequiresSettingsController", () => {
afterEach(() => {
Expand All @@ -30,4 +33,100 @@ describe("RequiresSettingsController", () => {
expect(controller.settingDisabled).toEqual(false);
expect(controller.getValueOverride()).toEqual(null);
});

describe("with capabilites", () => {
let caps: Capabilities;
let client: ReturnType<typeof getMockClientWithEventEmitter>;
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsServer(),
getCachedCapabilities: jest.fn().mockImplementation(() => caps!),
getCapabilities: jest.fn(),
});
MatrixClientBackedController["_matrixClient"] = client;
});

it("will disable setting if capability check is true", async () => {
caps = {
"m.change_password": {
enabled: false,
},
};
const controller = new RequiresSettingsController([], false, (c: Capabilities) => {
expect(c).toEqual(caps);
return !c["m.change_password"]?.enabled;
});

// Test that we fetch caps
controller["initMatrixClient"]();
expect(client.getCapabilities).toHaveBeenCalled();

// Test that we check caps.
expect(controller.settingDisabled).toEqual(true);
expect(controller.getValueOverride()).toEqual(false);
expect(client.getCachedCapabilities).toHaveBeenCalled();
});

it("will not disable setting if capability check is false", async () => {
caps = {
"m.change_password": {
enabled: true,
},
};
const controller = new RequiresSettingsController([], false, (c: Capabilities) => {
expect(c).toEqual(caps);
return !c["m.change_password"]?.enabled;
});

// Test that we fetch caps
controller["initMatrixClient"]();
expect(client.getCapabilities).toHaveBeenCalled();

// Test that we check caps.
expect(controller.settingDisabled).toEqual(false);
expect(controller.getValueOverride()).toEqual(null);
expect(client.getCachedCapabilities).toHaveBeenCalled();
});

it("will check dependency settings before checking capabilites", async () => {
caps = {
"m.change_password": {
enabled: false,
},
};
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, false);
const controller = new RequiresSettingsController(["useCompactLayout"], false, (c: Capabilities) => false);

// Test that we fetch caps
controller["initMatrixClient"]();
expect(client.getCapabilities).toHaveBeenCalled();

// Test that we check caps.
expect(controller.settingDisabled).toEqual(true);
expect(controller.getValueOverride()).toEqual(false);
expect(client.getCachedCapabilities).not.toHaveBeenCalled();
});

it("will disable setting if capability check is true and dependency settings are true", async () => {
caps = {
"m.change_password": {
enabled: false,
},
};
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
const controller = new RequiresSettingsController(["useCompactLayout"], false, (c: Capabilities) => {
expect(c).toEqual(caps);
return !c["m.change_password"]?.enabled;
});

// Test that we fetch caps
controller["initMatrixClient"]();
expect(client.getCapabilities).toHaveBeenCalled();

// Test that we check caps.
expect(controller.settingDisabled).toEqual(true);
expect(controller.getValueOverride()).toEqual(false);
expect(client.getCachedCapabilities).toHaveBeenCalled();
});
});
});
Loading