diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index be2151044d..d09b8467fd 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -52,6 +52,7 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; +import { Features } from "./settings/Settings"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -301,7 +302,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { throw new Error("createClient must be called first"); } - const useRustCrypto = SettingsStore.getValue("feature_rust_crypto"); + const useRustCrypto = SettingsStore.getValue(Features.RustCrypto); // we want to make sure that the same crypto implementation is used throughout the lifetime of a device, // so persist the setting at the device layer diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7dc6391982..5521b74331 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1447,7 +1447,10 @@ "report_to_moderators": "Report to moderators", "report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", "rust_crypto": "Rust cryptography implementation", - "rust_crypto_disabled_notice": "Can currently only be enabled via config.json", + "rust_crypto_in_config": "Rust cryptography cannot be disabled on this deployment of %(brand)s", + "rust_crypto_in_config_description": "Switching to the Rust cryptography requires a migration process that may take several minutes. It cannot be disabled; use with caution!", + "rust_crypto_optin_warning": "Switching to the Rust cryptography requires a migration process that may take several minutes. To disable you will need to log out and back in; use with caution!", + "rust_crypto_requires_logout": "Once enabled, Rust cryptography can only be disabled by logging out and in again", "sliding_sync": "Sliding Sync mode", "sliding_sync_checking": "Checking…", "sliding_sync_configuration": "Sliding Sync configuration", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 18a5ecb9ab..3a21d6f6dd 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -46,6 +46,7 @@ import RustCryptoSdkController from "./controllers/RustCryptoSdkController"; import ServerSupportUnstableFeatureController from "./controllers/ServerSupportUnstableFeatureController"; import { WatchManager } from "./WatchManager"; import { CustomTheme } from "../theme"; +import SettingsStore from "./SettingsStore"; export const defaultWatchManager = new WatchManager(); @@ -94,6 +95,7 @@ export enum Features { VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks", NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", + RustCrypto = "feature_rust_crypto", } export const labGroupNames: Record<LabGroup, TranslationKey> = { @@ -480,15 +482,22 @@ export const SETTINGS: { [setting: string]: ISetting } = { description: _td("labs|oidc_native_flow_description"), default: false, }, - "feature_rust_crypto": { - // use the rust matrix-sdk-crypto-js for crypto. + [Features.RustCrypto]: { + // use the rust matrix-sdk-crypto-wasm for crypto. isFeature: true, labsGroup: LabGroup.Developer, - configDisablesSetting: true, + // unlike most features, `configDisablesSetting` is false here. supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td("labs|rust_crypto"), - description: _td("labs|under_active_development"), - // shouldWarn: true, + description: () => { + if (SettingsStore.getValueAt(SettingLevel.CONFIG, Features.RustCrypto)) { + // It's enabled in the config, so you can't get rid of it even by logging out. + return _t("labs|rust_crypto_in_config_description"); + } else { + return _t("labs|rust_crypto_optin_warning"); + } + }, + shouldWarn: true, default: false, controller: new RustCryptoSdkController(), }, diff --git a/src/settings/controllers/RustCryptoSdkController.ts b/src/settings/controllers/RustCryptoSdkController.ts index 8de2fb3d87..3bf8526feb 100644 --- a/src/settings/controllers/RustCryptoSdkController.ts +++ b/src/settings/controllers/RustCryptoSdkController.ts @@ -15,12 +15,35 @@ limitations under the License. */ import { _t } from "../../languageHandler"; +import SettingsStore from "../SettingsStore"; +import { SettingLevel } from "../SettingLevel"; +import PlatformPeg from "../../PlatformPeg"; import SettingController from "./SettingController"; +import { Features } from "../Settings"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import SdkConfig from "../../SdkConfig"; export default class RustCryptoSdkController extends SettingController { + public onChange(level: SettingLevel, roomId: string | null, newValue: any): void { + // If the crypto stack has already been initialized, we'll need to reload the app to make it take effect. + if (MatrixClientPeg.get()?.getCrypto()) { + PlatformPeg.get()?.reload(); + } + } + 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 _t("labs|rust_crypto_disabled_notice"); + if (!SettingsStore.getValueAt(SettingLevel.DEVICE, Features.RustCrypto)) { + // If rust crypto has not yet been enabled for this device, you can turn it on, IF YOU DARE + return false; + } + + if (SettingsStore.getValueAt(SettingLevel.CONFIG, Features.RustCrypto)) { + // It's enabled in the config, so you can't get rid of it even by logging out. + return _t("labs|rust_crypto_in_config", { brand: SdkConfig.get().brand }); + } + + // The setting is enabled at the device level, but not mandated at the config level. + // You can only turn it off by logging out and in again. + return _t("labs|rust_crypto_requires_logout"); } } diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 69306f09f3..8b137b310b 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -38,6 +38,9 @@ describe("MatrixClientPeg", () => { afterEach(() => { localStorage.clear(); jest.restoreAllMocks(); + + // some of the tests assign `MatrixClientPeg.matrixClient`: clear it, to prevent leakage between tests + peg.unset(); }); it("setJustRegisteredUserId", () => { diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index ac832b88b2..0b34ef6343 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -15,15 +15,14 @@ limitations under the License. */ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import SdkConfig from "../../../../../../src/SdkConfig"; +import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; describe("<LabsUserSettingsTab />", () => { - const sdkConfigSpy = jest.spyOn(SdkConfig, "get"); - const defaultProps = { closeSettingsFn: jest.fn(), }; @@ -34,7 +33,9 @@ describe("<LabsUserSettingsTab />", () => { beforeEach(() => { jest.clearAllMocks(); settingsValueSpy.mockReturnValue(false); - sdkConfigSpy.mockReturnValue(false); + SdkConfig.reset(); + SdkConfig.add({ brand: "BrandedClient" }); + localStorage.clear(); }); it("renders settings marked as beta as beta cards", () => { @@ -43,6 +44,7 @@ describe("<LabsUserSettingsTab />", () => { }); it("does not render non-beta labs settings when disabled in config", () => { + const sdkConfigSpy = jest.spyOn(SdkConfig, "get"); render(getComponent()); expect(sdkConfigSpy).toHaveBeenCalledWith("show_labs_settings"); @@ -52,7 +54,7 @@ describe("<LabsUserSettingsTab />", () => { it("renders non-beta labs settings when enabled in config", () => { // enable labs - sdkConfigSpy.mockImplementation((configName) => configName === "show_labs_settings"); + SdkConfig.add({ show_labs_settings: true }); const { container } = render(getComponent()); // non-beta labs section @@ -60,4 +62,82 @@ describe("<LabsUserSettingsTab />", () => { const labsSections = container.getElementsByClassName("mx_SettingsSubsection"); expect(labsSections).toHaveLength(9); }); + + describe("Rust crypto setting", () => { + const SETTING_NAME = "Rust cryptography implementation"; + + beforeEach(() => { + SdkConfig.add({ show_labs_settings: true }); + }); + + describe("Not enabled in config", () => { + it("can be turned on if not already", async () => { + // By the time the settings panel is shown, `MatrixClientPeg.initClientCrypto` has saved the current + // value to the settings store. + await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, false); + + const rendered = render(getComponent()); + const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); + expect(toggle.getAttribute("aria-disabled")).toEqual("false"); + expect(toggle.getAttribute("aria-checked")).toEqual("false"); + + const description = toggle.closest(".mx_SettingsFlag")?.querySelector(".mx_SettingsFlag_microcopy"); + expect(description).toHaveTextContent(/To disable you will need to log out and back in/); + }); + + it("cannot be turned off once enabled", async () => { + await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, true); + + const rendered = render(getComponent()); + const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); + expect(toggle.getAttribute("aria-disabled")).toEqual("true"); + expect(toggle.getAttribute("aria-checked")).toEqual("true"); + + // Hover over the toggle to make it show the tooltip + fireEvent.mouseOver(toggle); + + const tooltip = rendered.getByRole("tooltip"); + expect(tooltip).toHaveTextContent( + "Once enabled, Rust cryptography can only be disabled by logging out and in again", + ); + }); + }); + + describe("Enabled in config", () => { + beforeEach(() => { + SdkConfig.add({ features: { feature_rust_crypto: true } }); + }); + + it("can be turned on if not already", async () => { + // By the time the settings panel is shown, `MatrixClientPeg.initClientCrypto` has saved the current + // value to the settings store. + await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, false); + + const rendered = render(getComponent()); + const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); + expect(toggle.getAttribute("aria-disabled")).toEqual("false"); + expect(toggle.getAttribute("aria-checked")).toEqual("false"); + + const description = toggle.closest(".mx_SettingsFlag")?.querySelector(".mx_SettingsFlag_microcopy"); + expect(description).toHaveTextContent(/It cannot be disabled/); + }); + + it("cannot be turned off once enabled", async () => { + await SettingsStore.setValue("feature_rust_crypto", null, SettingLevel.DEVICE, true); + + const rendered = render(getComponent()); + const toggle = rendered.getByRole("switch", { name: SETTING_NAME }); + expect(toggle.getAttribute("aria-disabled")).toEqual("true"); + expect(toggle.getAttribute("aria-checked")).toEqual("true"); + + // Hover over the toggle to make it show the tooltip + fireEvent.mouseOver(toggle); + + const tooltip = rendered.getByRole("tooltip"); + expect(tooltip).toHaveTextContent( + "Rust cryptography cannot be disabled on this deployment of BrandedClient", + ); + }); + }); + }); }); 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 32a8facee1..4e65f7b61f 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 @@ -15,7 +15,7 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1 <div class="mx_SettingsSubsection_text" > - What's next for false? Labs are the best way to get things early, test out new features and help shape them before they actually launch. + What's next for BrandedClient? Labs are the best way to get things early, test out new features and help shape them before they actually launch. </div> <div class="mx_BetaCard" @@ -42,10 +42,10 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1 class="mx_BetaCard_caption" > <p> - A new way to chat over voice and video in . + A new way to chat over voice and video in BrandedClient. </p> <p> - Video rooms are always-on VoIP channels embedded within a room in . + Video rooms are always-on VoIP channels embedded within a room in BrandedClient. </p> </div> <div @@ -62,7 +62,7 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1 <div class="mx_BetaCard_refreshWarning" > - Joining the beta will reload . + Joining the beta will reload BrandedClient. </div> <div class="mx_BetaCard_faq" @@ -104,7 +104,7 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1 class="mx_BetaCard_caption" > <p> - Introducing a simpler way to change your notification settings. Customize your , just the way you like. + Introducing a simpler way to change your notification settings. Customize your BrandedClient, just the way you like. </p> </div> <div