From dc0b77389820ff1a32d2303934ff1d962bb9a193 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Mar 2023 10:03:12 +0000 Subject: [PATCH 1/5] Show all labs even if incompatible, with appropriate tooltip explaining requirements --- .../views/elements/SettingsFlag.tsx | 35 ++++++----- .../tabs/user/LabsUserSettingsTab.tsx | 63 ++++++++----------- src/i18n/strings/en_EN.json | 5 +- src/settings/Settings.tsx | 20 +++--- src/settings/SettingsStore.ts | 11 ++++ .../controllers/RustCryptoSdkController.ts | 5 +- .../ServerSupportUnstableFeatureController.ts | 8 ++- src/settings/controllers/SettingController.ts | 3 +- .../controllers/SlidingSyncController.ts | 9 ++- 9 files changed, 92 insertions(+), 67 deletions(-) diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index b1e6cf41c02..6ae07d298b0 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -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 @@ -47,19 +48,30 @@ export default class SettingsFlag extends React.Component { super(props); this.state = { - value: SettingsStore.getValueAt( - this.props.level, - this.props.name, - this.props.roomId, - this.props.isExplicit, - ), + value: this.settingValue, }; } + public componentDidMount(): void { + defaultWatchManager.watchSetting(this.props.name, this.props.roomId, this.onSettingChange); + } + + public componentWillUnmount(): void { + defaultWatchManager.unwatchSetting(this.onSettingChange); + } + + private get settingValue(): boolean { + return SettingsStore.getValueAt(this.props.level, this.props.name, this.props.roomId, this.props.isExplicit); + } + + private onSettingChange = (): void => { + this.setState({ value: this.settingValue }); + }; + private onChange = async (checked: boolean): Promise => { 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): void => { @@ -87,11 +99,6 @@ export default class SettingsFlag extends React.Component { 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 =
{this.props.disabledDescription}
; - } - if (this.props.useCheckbox) { return ( { w: (sub) => ( {sub} ), - description: description, + description, }, ) : description} )} - {disabledDescription} diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 889b1e43872..c75e30c39ca 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -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 = (
- {this.state.betas.map((f) => ( + {this.betas.map((f) => ( ))}
@@ -84,12 +63,20 @@ 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(); - this.state.labs.forEach((f) => { + this.labs.forEach((f) => { groups .getOrCreate(SettingsStore.getLabGroup(f), []) - .push(); + .push( + , + ); }); groups diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fb34c24501b..47931bc3f22 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -938,6 +938,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 stable version of MSC3827": "Requires your server to support 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.", @@ -961,6 +962,7 @@ "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", "Sliding Sync mode": "Sliding Sync mode", "Under active development, cannot be disabled.": "Under active development, cannot be disabled.", @@ -978,7 +980,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", @@ -1055,6 +1056,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", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index b8d69f6c262..a84aa0e42bf 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -227,9 +227,12 @@ 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"], + _td("Requires your server to support stable version of MSC3827"), + ), }, "feature_msc3531_hide_messages_pending_moderation": { isFeature: true, @@ -373,9 +376,12 @@ 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"], + _td("Requires your server to support MSC3030"), + ), }, "RoomList.backgroundImage": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, @@ -482,7 +488,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(), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 30c8a3c491c..f90ea465ca2 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -334,6 +334,17 @@ 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 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. diff --git a/src/settings/controllers/RustCryptoSdkController.ts b/src/settings/controllers/RustCryptoSdkController.ts index 983d2dc1171..c688f6e1679 100644 --- a/src/settings/controllers/RustCryptoSdkController.ts +++ b/src/settings/controllers/RustCryptoSdkController.ts @@ -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"); } } diff --git a/src/settings/controllers/ServerSupportUnstableFeatureController.ts b/src/settings/controllers/ServerSupportUnstableFeatureController.ts index 2cbf014f1d5..6f9af824ed6 100644 --- a/src/settings/controllers/ServerSupportUnstableFeatureController.ts +++ b/src/settings/controllers/ServerSupportUnstableFeatureController.ts @@ -32,6 +32,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient private readonly settingName: string, private readonly watchers: WatchManager, private readonly unstableFeatures: string[], + private readonly disabledMessage?: string, private readonly forcedValue: any = false, ) { super(); @@ -72,7 +73,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; } } diff --git a/src/settings/controllers/SettingController.ts b/src/settings/controllers/SettingController.ts index d3a52dbb8c8..cff7ce66046 100644 --- a/src/settings/controllers/SettingController.ts +++ b/src/settings/controllers/SettingController.ts @@ -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; } } diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 56149f2099f..5a56cb507f0 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -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 { @@ -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; } } From 70ed0e5f06704a51ecf146a36979e78dd110ecf3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Mar 2023 12:46:27 +0000 Subject: [PATCH 2/5] Iterate PR --- .../views/elements/SettingsFlag.tsx | 24 ++-- .../tabs/user/LabsUserSettingsTab.tsx | 10 +- .../tabs/user/PreferencesUserSettingsTab.tsx | 27 +--- src/settings/Settings.tsx | 10 ++ src/settings/SettingsStore.ts | 4 +- .../ServerSupportUnstableFeatureController.ts | 7 + .../tabs/user/LabsUserSettingsTab-test.tsx | 24 +++- .../LabsUserSettingsTab-test.tsx.snap | 123 ++++++++++++++++++ 8 files changed, 177 insertions(+), 52 deletions(-) diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 6ae07d298b0..8605c9bbdf7 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -33,14 +33,13 @@ 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; + disabled: boolean; } export default class SettingsFlag extends React.Component { @@ -49,6 +48,7 @@ export default class SettingsFlag extends React.Component { this.state = { value: this.settingValue, + disabled: this.settingDisabled, }; } @@ -64,8 +64,15 @@ export default class SettingsFlag extends React.Component { return SettingsStore.getValueAt(this.props.level, this.props.name, this.props.roomId, this.props.isExplicit); } + private get settingDisabled(): boolean { + return !SettingsStore.isEnabled(this.props.name); + } + private onSettingChange = (): void => { - this.setState({ value: this.settingValue }); + this.setState({ + value: this.settingValue, + disabled: this.settingDisabled, + }); }; private onChange = async (checked: boolean): Promise => { @@ -98,14 +105,11 @@ export default class SettingsFlag extends React.Component { : SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined; const description = SettingsStore.getDescription(this.props.name); const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name); + const disabled = this.state.disabled || !canChange; if (this.props.useCheckbox) { return ( - + {label} ); @@ -134,8 +138,8 @@ export default class SettingsFlag extends React.Component { diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index c75e30c39ca..498aaf03317 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -68,15 +68,7 @@ export default class LabsUserSettingsTab extends React.Component<{}> { this.labs.forEach((f) => { groups .getOrCreate(SettingsStore.getLabGroup(f), []) - .push( - , - ); + .push(); }); groups diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 83c8c9fc11a..d27b2a755bf 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -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; @@ -101,7 +99,6 @@ export default class PreferencesUserSettingsTab extends React.Component { - const cli = MatrixClientPeg.get(); - - this.setState({ - disablingReadReceiptsSupported: - (await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) || - (await cli.isVersionSupported("v1.4")), - }); - } - private onAutocompleteDelayChange = (e: React.ChangeEvent): void => { this.setState({ autocompleteDelay: e.target.value }); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); @@ -140,10 +127,7 @@ export default class PreferencesUserSettingsTab extends React.Component { - const disabled = !SettingsStore.isEnabled(i); - return ; - }); + return settingIds.map((i) => ); } private onKeyboardShortcutsClicked = (): void => { @@ -205,14 +189,7 @@ export default class PreferencesUserSettingsTab extends React.Component {_t("Share your activity and status with others.")} - + {this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a84aa0e42bf..77282e94c90 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -231,6 +231,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { "feature_exploring_public_spaces", defaultWatchManager, ["org.matrix.msc3827.stable"], + undefined, _td("Requires your server to support stable version of MSC3827"), ), }, @@ -380,6 +381,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { "feature_jump_to_date", defaultWatchManager, ["org.matrix.msc3030"], + undefined, _td("Requires your server to support MSC3030"), ), }, @@ -391,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, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index f90ea465ca2..86772eac696 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -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 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. */ diff --git a/src/settings/controllers/ServerSupportUnstableFeatureController.ts b/src/settings/controllers/ServerSupportUnstableFeatureController.ts index 6f9af824ed6..7dfe4bb69c4 100644 --- a/src/settings/controllers/ServerSupportUnstableFeatureController.ts +++ b/src/settings/controllers/ServerSupportUnstableFeatureController.ts @@ -32,6 +32,7 @@ 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, ) { @@ -54,10 +55,16 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient protected async initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient): Promise { 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; } diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index 7bd5062cc4a..384efce8cda 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -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("", () => { +describe("", () => { const sdkConfigSpy = jest.spyOn(SdkConfig, "get"); const defaultProps = { @@ -70,10 +70,10 @@ describe("", () => { 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("", () => { }); 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"); + }); }); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap index 8644f121046..c4ee371bcef 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap @@ -1,5 +1,128 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` renders settings marked as beta as beta cards 1`] = ` +
+
+
+
+

+ + Video rooms + + + Beta + +

+
+

+ A new way to chat over voice and video in . +

+

+ Video rooms are always-on VoIP channels embedded within a room in . +

+
+
+
+ Join the beta +
+
+
+ Joining the beta will reload . +
+
+
+
+ +
+
+
+
+
+
+

+ + New session manager + + + Beta + +

+
+

+ 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. +

+
+
+
+ Join the beta +
+
+
+
+ +
+
+
+
+`; + exports[` renders settings marked as beta as beta cards 1`] = `
Date: Tue, 14 Mar 2023 13:34:41 +0000 Subject: [PATCH 3/5] Update src/settings/SettingsStore.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- src/settings/SettingsStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 86772eac696..437cf4bbda2 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -325,8 +325,8 @@ export default class SettingsStore { /** * Determines if a setting is enabled. - * If a setting is disabled then it should 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 + * 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. From 1a05ea2392e86d3795ea866a3c10117f1969511e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Mar 2023 15:05:21 +0000 Subject: [PATCH 4/5] Iterate --- src/components/views/elements/SettingsFlag.tsx | 13 +++++++------ .../tabs/user/PreferencesUserSettingsTab.tsx | 6 +----- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- src/settings/SettingsStore.ts | 3 ++- .../tabs/user/PreferencesUserSettingsTab-test.tsx | 15 +++++++++------ ...ServerSupportUnstableFeatureController-test.ts | 2 ++ test/test-utils/client.ts | 1 + 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 8605c9bbdf7..f808d735ebc 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -39,6 +39,7 @@ interface IProps { interface IState { value: boolean; + /** true if `SettingsStore.isEnabled` returned false. */ disabled: boolean; } @@ -47,8 +48,8 @@ export default class SettingsFlag extends React.Component { super(props); this.state = { - value: this.settingValue, - disabled: this.settingDisabled, + value: this.getSettingValue(), + disabled: this.isSettingDisabled(), }; } @@ -60,18 +61,18 @@ export default class SettingsFlag extends React.Component { defaultWatchManager.unwatchSetting(this.onSettingChange); } - private get settingValue(): boolean { + private getSettingValue(): boolean { return SettingsStore.getValueAt(this.props.level, this.props.name, this.props.roomId, this.props.isExplicit); } - private get settingDisabled(): boolean { + private isSettingDisabled(): boolean { return !SettingsStore.isEnabled(this.props.name); } private onSettingChange = (): void => { this.setState({ - value: this.settingValue, - disabled: this.settingDisabled, + value: this.getSettingValue(), + disabled: this.isSettingDisabled(), }); }; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index d27b2a755bf..0d8acbca555 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -48,10 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component {_t("Share your activity and status with others.")} - {this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 47931bc3f22..2b8e1d35377 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -964,6 +964,7 @@ "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", @@ -1623,7 +1624,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", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 77282e94c90..0faae6a02cd 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -232,7 +232,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { defaultWatchManager, ["org.matrix.msc3827.stable"], undefined, - _td("Requires your server to support stable version of MSC3827"), + _td("Requires your server to support the stable version of MSC3827"), ), }, "feature_msc3531_hide_messages_pending_moderation": { diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 437cf4bbda2..a51faa2cee0 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -338,7 +338,8 @@ export default class SettingsStore { /** * Retrieves the reason a setting is disabled if one is assigned. - * If a setting is not disabled this will return undefined. + * 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. */ diff --git a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx index 6b574229743..379b1db8afa 100644 --- a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx @@ -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 () => { diff --git a/test/settings/controllers/ServerSupportUnstableFeatureController-test.ts b/test/settings/controllers/ServerSupportUnstableFeatureController-test.ts index 91f6083da6e..bff87d93b29 100644 --- a/test/settings/controllers/ServerSupportUnstableFeatureController-test.ts +++ b/test/settings/controllers/ServerSupportUnstableFeatureController-test.ts @@ -56,6 +56,8 @@ describe("ServerSupportUnstableFeatureController", () => { setting, watchers, ["feature"], + undefined, + undefined, "other_value", ); await prepareSetting(cli, controller); diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 47040dcb4b3..996682b190d 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -126,6 +126,7 @@ export const mockClientMethodsServer = (): Partial Date: Tue, 14 Mar 2023 15:24:33 +0000 Subject: [PATCH 5/5] Iterate --- .../context_menus/MessageContextMenu.tsx | 2 +- .../context_menus/ThreadListContextMenu.tsx | 2 +- .../views/elements/SettingsFlag.tsx | 9 +- src/i18n/strings/en_EN.json | 2 +- .../tabs/user/LabsUserSettingsTab-test.tsx | 8 +- .../LabsUserSettingsTab-test.tsx.snap | 123 ------------------ 6 files changed, 14 insertions(+), 132 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 8392e6a14fd..d73a2dea2d9 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -283,7 +283,7 @@ export default class MessageContextMenu extends React.Component this.closeMenu(); }; - private onShareClick = (e: React.MouseEvent): void => { + private onShareClick = (e: ButtonEvent): void => { e.preventDefault(); Modal.createDialog(ShareDialog, { target: this.props.mxEvent, diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index 674330a1759..abaef399f4a 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -104,7 +104,7 @@ const ThreadListContextMenu: React.FC = ({ className="mx_RoomTile_contextMenu" compact rightAligned - {...contextMenuBelow(button.current.getBoundingClientRect())} + {...contextMenuBelow(button.current!.getBoundingClientRect())} > {isMainSplitTimelineShown && ( diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index f808d735ebc..2bb1d4ddb77 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -54,7 +54,7 @@ export default class SettingsFlag extends React.Component { } public componentDidMount(): void { - defaultWatchManager.watchSetting(this.props.name, this.props.roomId, this.onSettingChange); + defaultWatchManager.watchSetting(this.props.name, this.props.roomId ?? null, this.onSettingChange); } public componentWillUnmount(): void { @@ -62,7 +62,12 @@ export default class SettingsFlag extends React.Component { } private getSettingValue(): boolean { - return SettingsStore.getValueAt(this.props.level, this.props.name, this.props.roomId, this.props.isExplicit); + return SettingsStore.getValueAt( + this.props.level, + this.props.name, + this.props.roomId ?? null, + this.props.isExplicit, + ); } private isSettingDisabled(): boolean { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2b8e1d35377..a4f6517303e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -938,7 +938,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 stable version of MSC3827": "Requires your server to support stable version of MSC3827", + "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.", diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index 384efce8cda..ddf6105274c 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -86,15 +86,15 @@ describe("", () => { const { queryByText } = render(getComponent()); expect( - queryByText("Explore public spaces in the new search dialog") - .closest(".mx_SettingsFlag") + queryByText("Explore public spaces in the new search dialog")! + .closest(".mx_SettingsFlag")! .querySelector(".mx_AccessibleButton"), ).toHaveAttribute("aria-disabled", "true"); deferred.resolve(true); await waitFor(() => { expect( - queryByText("Explore public spaces in the new search dialog") - .closest(".mx_SettingsFlag") + queryByText("Explore public spaces in the new search dialog")! + .closest(".mx_SettingsFlag")! .querySelector(".mx_AccessibleButton"), ).toHaveAttribute("aria-disabled", "false"); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap index c4ee371bcef..2d562b8c32f 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap @@ -122,126 +122,3 @@ exports[` renders settings marked as beta as beta cards 1
`; - -exports[` renders settings marked as beta as beta cards 1`] = ` -
-
-
-
-

- - Video rooms - - - Beta - -

-
-

- A new way to chat over voice and video in . -

-

- Video rooms are always-on VoIP channels embedded within a room in . -

-
-
-
- Join the beta -
-
-
- Joining the beta will reload . -
-
-
-
- -
-
-
-
-
-
-

- - New session manager - - - Beta - -

-
-

- 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. -

-
-
-
- Join the beta -
-
-
-
- -
-
-
-
-`;