Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 10 additions & 1 deletion apps/web/src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,16 @@ 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 === undefined ||
c["io.element.msc4452.preview_url"].enabled
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
) {
// If there is no capability, assume true.
return false;
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
}
return _t("common|disabled_by_homeserver");
}),
},
"urlPreviewsEnabled_e2ee": {
// Can only be enabled per-device to ensure neither the homeserver nor client config
Expand Down
50 changes: 43 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,66 @@
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();
}
}

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

Check failure on line 45 in apps/web/src/settings/controllers/RequiresSettingsController.ts

View workflow job for this annotation

GitHub Actions / Jest (Element Web) (2)

PreferencesUserSettingsTab › send read receipts › without server support › is forcibly enabled

TypeError: _this$client.getCachedCapabilities is not a function at RequiresSettingsController.getCachedCapabilities [as isBlockedByCapabilites] (src/settings/controllers/RequiresSettingsController.ts:45:41) at RequiresSettingsController.isBlockedByCapabilites [as settingDisabled] (src/settings/controllers/RequiresSettingsController.ts:57:21) at RequiresSettingsController.settingDisabled [as getValueOverride] (src/settings/controllers/RequiresSettingsController.ts:61:18) at SettingsStore.getValueOverride [as getFinalValue] (src/settings/SettingsStore.ts:475:52) at getFinalValue (src/settings/SettingsStore.ts:444:34) at copyOfGetValueAt (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:132:24) at SettingsStore.copyOfGetValueAt [as getValueAt] (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:132:24) at SettingsFlag.getValueAt [as getSettingValue] (src/components/views/elements/SettingsFlag.tsx:69:32) at new getSettingValue (src/components/views/elements/SettingsFlag.tsx:43:25) at updateClassComponent (../../node_modules/react-dom/cjs/react-dom-client.development.js:10279:21) at beginWork (../../node_modules/react-dom/cjs/react-dom-client.development.js:11792:13) at runWithFiberInDEV (../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13) at performUnitOfWork (../../node_modules/react-dom/cjs/react-dom-client.development.js:17641:22) at workLoopSync (../../node_modules/react-dom/cjs/react-dom-client.development.js:17469:41) at renderRootSync (../../node_modules/react-dom/cjs/react-dom-client.development.js:17450:11) at performWorkOnRoot (../../node_modules/react-dom/cjs/react-dom-client.development.js:16583:35) at performWorkOnRootViaSchedulerTask (../../node_modules/react-dom/cjs/react-dom-client.development.js:18957:7) at flushActQueue (../../node_modules/react/cjs/react.development.js:590:34) at process.env.NODE_ENV.exports.act (../../node_modules/react/cjs/react.development.js:884:10) at ../../node_modules/@testing-library/react/dist/act-compat.js:46:25 at renderRoot (../../node_modules/@testing-library/react/dist/pure.js:189:26) at render (../../node_modules/@testing-library/react/dist/pure.js:291:10) at customRender (test/test-utils/jest-matrix-react.tsx:45:18) at renderTab (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:28:22) at renderTab (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:110:33) at Object.getToggle (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:173:32)

Check failure on line 45 in apps/web/src/settings/controllers/RequiresSettingsController.ts

View workflow job for this annotation

GitHub Actions / Jest (Element Web) (2)

PreferencesUserSettingsTab › send read receipts › with server support › can be disabled

TypeError: _this$client.getCachedCapabilities is not a function at RequiresSettingsController.getCachedCapabilities [as isBlockedByCapabilites] (src/settings/controllers/RequiresSettingsController.ts:45:41) at RequiresSettingsController.isBlockedByCapabilites [as settingDisabled] (src/settings/controllers/RequiresSettingsController.ts:57:21) at RequiresSettingsController.settingDisabled [as getValueOverride] (src/settings/controllers/RequiresSettingsController.ts:61:18) at SettingsStore.getValueOverride [as getFinalValue] (src/settings/SettingsStore.ts:475:52) at getFinalValue (src/settings/SettingsStore.ts:444:34) at copyOfGetValueAt (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:132:24) at SettingsStore.copyOfGetValueAt [as getValueAt] (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:132:24) at SettingsFlag.getValueAt [as getSettingValue] (src/components/views/elements/SettingsFlag.tsx:69:32) at new getSettingValue (src/components/views/elements/SettingsFlag.tsx:43:25) at updateClassComponent (../../node_modules/react-dom/cjs/react-dom-client.development.js:10279:21) at beginWork (../../node_modules/react-dom/cjs/react-dom-client.development.js:11792:13) at runWithFiberInDEV (../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13) at performUnitOfWork (../../node_modules/react-dom/cjs/react-dom-client.development.js:17641:22) at workLoopSync (../../node_modules/react-dom/cjs/react-dom-client.development.js:17469:41) at renderRootSync (../../node_modules/react-dom/cjs/react-dom-client.development.js:17450:11) at performWorkOnRoot (../../node_modules/react-dom/cjs/react-dom-client.development.js:16583:35) at performWorkOnRootViaSchedulerTask (../../node_modules/react-dom/cjs/react-dom-client.development.js:18957:7) at flushActQueue (../../node_modules/react/cjs/react.development.js:590:34) at process.env.NODE_ENV.exports.act (../../node_modules/react/cjs/react.development.js:884:10) at ../../node_modules/@testing-library/react/dist/act-compat.js:46:25 at renderRoot (../../node_modules/@testing-library/react/dist/pure.js:189:26) at render (../../node_modules/@testing-library/react/dist/pure.js:291:10) at customRender (test/test-utils/jest-matrix-react.tsx:45:18) at renderTab (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:28:22) at renderTab (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:110:33) at Object.getToggle (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:159:32)

Check failure on line 45 in apps/web/src/settings/controllers/RequiresSettingsController.ts

View workflow job for this annotation

GitHub Actions / Jest (Element Web) (2)

PreferencesUserSettingsTab › send read receipts › with server support › can be enabled

TypeError: _this$client.getCachedCapabilities is not a function at RequiresSettingsController.getCachedCapabilities [as isBlockedByCapabilites] (src/settings/controllers/RequiresSettingsController.ts:45:41) at RequiresSettingsController.isBlockedByCapabilites [as settingDisabled] (src/settings/controllers/RequiresSettingsController.ts:57:21) at RequiresSettingsController.settingDisabled [as getValueOverride] (src/settings/controllers/RequiresSettingsController.ts:61:18) at SettingsStore.getValueOverride [as getFinalValue] (src/settings/SettingsStore.ts:475:52) at getFinalValue (src/settings/SettingsStore.ts:444:34) at SettingsStore.copyOfGetValueAt [as getValueAt] (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:132:24) at SettingsFlag.getValueAt [as getSettingValue] (src/components/views/elements/SettingsFlag.tsx:69:32) at new getSettingValue (src/components/views/elements/SettingsFlag.tsx:43:25) at updateClassComponent (../../node_modules/react-dom/cjs/react-dom-client.development.js:10279:21) at beginWork (../../node_modules/react-dom/cjs/react-dom-client.development.js:11792:13) at runWithFiberInDEV (../../node_modules/react-dom/cjs/react-dom-client.development.js:874:13) at performUnitOfWork (../../node_modules/react-dom/cjs/react-dom-client.development.js:17641:22) at workLoopSync (../../node_modules/react-dom/cjs/react-dom-client.development.js:17469:41) at renderRootSync (../../node_modules/react-dom/cjs/react-dom-client.development.js:17450:11) at performWorkOnRoot (../../node_modules/react-dom/cjs/react-dom-client.development.js:16583:35) at performWorkOnRootViaSchedulerTask (../../node_modules/react-dom/cjs/react-dom-client.development.js:18957:7) at flushActQueue (../../node_modules/react/cjs/react.development.js:590:34) at process.env.NODE_ENV.exports.act (../../node_modules/react/cjs/react.development.js:884:10) at ../../node_modules/@testing-library/react/dist/act-compat.js:46:25 at renderRoot (../../node_modules/@testing-library/react/dist/pure.js:189:26) at render (../../node_modules/@testing-library/react/dist/pure.js:291:10) at customRender (test/test-utils/jest-matrix-react.tsx:45:18) at renderTab (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:28:22) at renderTab (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:110:33) at Object.getToggle (test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx:150:32)
if (!cachedCaps) {
// return forced value if caps aren't available yet.
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));
}
}
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