diff --git a/package.json b/package.json index 8c3e95d92c..42a48823f7 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@matrix-org/emojibase-bindings": "^1.1.2", "@matrix-org/matrix-wysiwyg": "2.17.0", "@matrix-org/olm": "3.2.15", - "@matrix-org/react-sdk-module-api": "^2.3.0", + "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 3e87541ddd..61097c13c2 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -24,7 +24,7 @@ import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; -import SecurityCustomisations from "./customisations/Security"; +import { ModuleRunner } from "./modules/ModuleRunner"; import EventIndexPeg from "./indexing/EventIndexPeg"; import createMatrixClient from "./utils/createMatrixClient"; import Notifier from "./Notifier"; @@ -863,7 +863,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise void, ): Promise { - const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.getSecretStorageKey(); if (keyFromCustomisations) { - logger.log("Using key from security customisations (dehydration)"); + logger.log("CryptoSetupExtension: Using key from extension (dehydration)"); return keyFromCustomisations; } @@ -430,7 +430,7 @@ async function doAccessSecretStorage(func: () => Promise, forceReset: bool // inner operation completes. return await func(); } catch (e) { - SecurityCustomisations.catchAccessSecretStorageError?.(e); + ModuleRunner.instance.extensions.cryptoSetup.catchAccessSecretStorageError(e as Error); logger.error(e); // Re-throw so that higher level logic can abort as needed throw e; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 3fe64499ca..be49b43851 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -40,7 +40,7 @@ import { isSecureBackupRequired, SecureBackupSetupMethod, } from "../../../../utils/WellKnownUtils"; -import SecurityCustomisations from "../../../../customisations/Security"; +import { ModuleRunner } from "../../../../modules/ModuleRunner"; import Field from "../../../../components/views/elements/Field"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import Spinner from "../../../../components/views/elements/Spinner"; @@ -180,9 +180,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (crossSigningIsSetUp) { // if the user has previously set up cross-signing, verify this device so we can fetch the // private keys. - if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) { + + const cryptoExtension = ModuleRunner.instance.extensions.cryptoSetup; + if (cryptoExtension.SHOW_ENCRYPTION_SETUP_UI == false) { this.onLoggedIn(); } else { this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index 33170b494b..c5efe10e0f 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -79,7 +79,6 @@ export const CallGuestLinkButton: React.FC<{ room: Room }> = ({ room }) => { // If the user cannot invite the Knocking is not given as an option. canInvite, }).finished.then(() => { - // we need to use the function here because the callback got called before the state was updated. if (isRoomJoinable()) showLinkModal(); }); } diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index ec47cff714..fc86ee0952 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -58,6 +58,7 @@ import { Caption } from "../typography/Caption"; import { SettingsSubsectionHeading } from "./shared/SettingsSubsectionHeading"; import SettingsSubsection from "./shared/SettingsSubsection"; import { doesRoomHaveUnreadMessages } from "../../../Unread"; +import SettingsFlag from "../elements/SettingsFlag"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. @@ -200,6 +201,18 @@ const maximumVectorState = ( return vectorState; }; +const NotificationActivitySettings = (): JSX.Element => { + return ( +
+ + +
+ ); +}; + +/** + * The old, deprecated notifications tab view, only displayed if the user has the labs flag disabled. + */ export default class Notifications extends React.PureComponent { private settingWatchers: string[]; @@ -731,43 +744,10 @@ export default class Notifications extends React.PureComponent { } private renderCategory(category: RuleClass): ReactNode { - if (category !== RuleClass.VectorOther && this.isInhibited) { + if (this.isInhibited) { return null; // nothing to show for the section } - let clearNotifsButton: JSX.Element | undefined; - if ( - category === RuleClass.VectorOther && - MatrixClientPeg.safeGet() - .getRooms() - .some((r) => doesRoomHaveUnreadMessages(r, true)) - ) { - clearNotifsButton = ( - - {_t("notifications|mark_all_read")} - - ); - } - - if (category === RuleClass.VectorOther && this.isInhibited) { - // only render the utility buttons (if needed) - if (clearNotifsButton) { - return ( -
-
{_t("notifications|class_other")}
- {clearNotifsButton} -
- ); - } - return null; - } - let keywordComposer: JSX.Element | undefined; if (category === RuleClass.VectorMentions) { const tags = filterBoolean(this.state.vectorKeywordRuleInfo?.rules.map((r) => r.pattern) || []); @@ -842,7 +822,6 @@ export default class Notifications extends React.PureComponent { {VectorStateToLabel[VectorState.Loud]} {fieldsetRows} - {clearNotifsButton} {keywordComposer} ); @@ -878,6 +857,25 @@ export default class Notifications extends React.PureComponent { return

{_t("settings|notifications|error_loading")}

; } + let clearNotifsButton: JSX.Element | undefined; + if ( + MatrixClientPeg.safeGet() + .getRooms() + .some((r) => doesRoomHaveUnreadMessages(r, true)) + ) { + clearNotifsButton = ( + + {_t("notifications|mark_all_read")} + + ); + } + return ( <> {this.renderTopSection()} @@ -885,6 +883,8 @@ export default class Notifications extends React.PureComponent { {this.renderCategory(RuleClass.VectorMentions)} {this.renderCategory(RuleClass.VectorOther)} {this.renderTargets()} + + {clearNotifsButton} ); } diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index bdc021e486..a15fdd1d8b 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -41,6 +41,7 @@ import { SettingsBanner } from "../shared/SettingsBanner"; import { SettingsSection } from "../shared/SettingsSection"; import SettingsSubsection from "../shared/SettingsSubsection"; import { NotificationPusherSettings } from "./NotificationPusherSettings"; +import SettingsFlag from "../../elements/SettingsFlag"; enum NotificationDefaultLevels { AllMessages = "all_messages", @@ -71,6 +72,9 @@ function useHasUnreadNotifications(): boolean { return cli.getRooms().some((room) => room.getUnreadNotificationCount() > 0); } +/** + * The new notification settings tab view, only displayed if the user has Features.NotificationSettings2 enabled + */ export default function NotificationSettings2(): JSX.Element { const cli = useMatrixClientContext(); @@ -352,6 +356,9 @@ export default function NotificationSettings2(): JSX.Element { label={_t("notifications|keyword")} placeholder={_t("notifications|keyword_new")} /> + + + diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts index 72b5380fbd..725fcfd5be 100644 --- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts +++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts @@ -43,13 +43,14 @@ type Result = { */ export function useUnreadThreadRooms(forceComputation: boolean): Result { const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors"); + const settingTACOnlyNotifs = useSettingValue("Notifications.tac_only_notifications"); const mxClient = useMatrixClientContext(); const [result, setResult] = useState({ greatestNotificationLevel: NotificationLevel.None, rooms: [] }); const doUpdate = useCallback(() => { - setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor)); - }, [mxClient, msc3946ProcessDynamicPredecessor]); + setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs)); + }, [mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs]); // The exhautive deps lint rule can't compute dependencies here since it's not a plain inline func. // We make this as simple as possible so its only dep is doUpdate itself. @@ -83,7 +84,11 @@ export function useUnreadThreadRooms(forceComputation: boolean): Result { * @param mxClient - MatrixClient * @param msc3946ProcessDynamicPredecessor */ -function computeUnreadThreadRooms(mxClient: MatrixClient, msc3946ProcessDynamicPredecessor: boolean): Result { +function computeUnreadThreadRooms( + mxClient: MatrixClient, + msc3946ProcessDynamicPredecessor: boolean, + settingTACOnlyNotifs: boolean, +): Result { // Only count visible rooms to not torment the user with notification counts in rooms they can't see. // This will include highlights from the previous version of the room internally const visibleRooms = mxClient.getVisibleRooms(msc3946ProcessDynamicPredecessor); @@ -98,7 +103,7 @@ function computeUnreadThreadRooms(mxClient: MatrixClient, msc3946ProcessDynamicP const notificationLevel = getThreadNotificationLevel(room); // If the room has an activity notification or less, we ignore it - if (notificationLevel <= NotificationLevel.Activity) { + if (settingTACOnlyNotifs && notificationLevel <= NotificationLevel.Activity) { continue; } diff --git a/src/hooks/room/useGuestAccessInformation.ts b/src/hooks/room/useGuestAccessInformation.ts index efafcd18d7..38b83700bc 100644 --- a/src/hooks/room/useGuestAccessInformation.ts +++ b/src/hooks/room/useGuestAccessInformation.ts @@ -52,7 +52,10 @@ export const useGuestAccessInformation = (room: Room): GuestAccessInformation => [canChangeJoinRule, isRoomJoinable, guestSpaUrl], ); - const isRoomJoinableFunction = (): boolean => - room.getJoinRule() === JoinRule.Public || (joinRule === JoinRule.Knock && room.canInvite(room.myUserId)); + const isRoomJoinableFunction = (): boolean => { + const join = room.getJoinRule(); + return join === JoinRule.Public || (join === JoinRule.Knock && room.canInvite(room.myUserId)); + }; + return { canInviteGuests, guestSpaUrl, isRoomJoinable: isRoomJoinableFunction, canInvite }; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e1c6c23ac5..48e6e2f0ae 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2846,6 +2846,7 @@ "show_redaction_placeholder": "Show a placeholder for removed messages", "show_stickers_button": "Show stickers button", "show_typing_notifications": "Show typing notifications", + "showbold": "Show all activity in the room list (dots or number of unread messages)", "sidebar": { "metaspaces_favourites_description": "Group all your favourite rooms and people in one place.", "metaspaces_home_all_rooms": "Show all rooms", @@ -2862,6 +2863,7 @@ "title": "Sidebar" }, "start_automatically": "Start automatically after system login", + "tac_only_notifications": "Only show notifications in the thread activity centre", "use_12_hour_format": "Show timestamps in 12 hour format (e.g. 2:30pm)", "use_command_enter_send_message": "Use Command + Enter to send a message", "use_command_f_search": "Use Command + F to search timeline", diff --git a/src/modules/ModuleRunner.ts b/src/modules/ModuleRunner.ts index 1a94eb16b3..0c609f6f66 100644 --- a/src/modules/ModuleRunner.ts +++ b/src/modules/ModuleRunner.ts @@ -17,18 +17,108 @@ limitations under the License. import { safeSet } from "matrix-js-sdk/src/utils"; import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations"; import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types"; +import { + DefaultCryptoSetupExtensions, + ProvideCryptoSetupExtensions, +} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions"; +import { + DefaultExperimentalExtensions, + ProvideExperimentalExtensions, +} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions"; import { AppModule } from "./AppModule"; import { ModuleFactory } from "./ModuleFactory"; import "./ModuleComponents"; +/** + * Handles and manages extensions provided by modules. + */ +class ExtensionsManager { + // Private backing fields for extensions + private cryptoSetupExtension: ProvideCryptoSetupExtensions; + private experimentalExtension: ProvideExperimentalExtensions; + + /** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */ + private hasDefaultCryptoSetupExtension = true; + + /** `true` if `experimentalExtension` is the default implementation; `false` if it is implemented by a module. */ + private hasDefaultExperimentalExtension = true; + + /** + * Create a new instance. + */ + public constructor() { + // Set up defaults + this.cryptoSetupExtension = new DefaultCryptoSetupExtensions(); + this.experimentalExtension = new DefaultExperimentalExtensions(); + } + + /** + * Provides a crypto setup extension. + * + * @returns The registered extension. If no module provides this extension, a default implementation is returned. + */ + public get cryptoSetup(): ProvideCryptoSetupExtensions { + return this.cryptoSetupExtension; + } + + /** + * Provides an experimental extension. + * + * @remarks + * This method extension is provided to simplify experimentation and development, and is not intended for production code. + * + * @returns The registered extension. If no module provides this extension, a default implementation is returned. + */ + public get experimental(): ProvideExperimentalExtensions { + return this.experimentalExtension; + } + + /** + * Add any extensions provided by the module. + * + * @param module - The appModule to check for extensions. + * + * @throws if an extension is provided by more than one module. + */ + public addExtensions(module: AppModule): void { + const runtimeModule = module.module; + + /* Add the cryptoSetup extension if any */ + if (runtimeModule.extensions?.cryptoSetup) { + if (this.hasDefaultCryptoSetupExtension) { + this.cryptoSetupExtension = runtimeModule.extensions?.cryptoSetup; + this.hasDefaultCryptoSetupExtension = false; + } else { + throw new Error( + `adding cryptoSetup extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`, + ); + } + } + + /* Add the experimental extension if any */ + if (runtimeModule.extensions?.experimental) { + if (this.hasDefaultExperimentalExtension) { + this.experimentalExtension = runtimeModule.extensions?.experimental; + this.hasDefaultExperimentalExtension = false; + } else { + throw new Error( + `adding experimental extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`, + ); + } + } + } +} + /** * Handles and coordinates the operation of modules. */ export class ModuleRunner { public static readonly instance = new ModuleRunner(); + private extensionsManager = new ExtensionsManager(); + private modules: AppModule[] = []; private constructor() { @@ -36,12 +126,22 @@ export class ModuleRunner { } /** - * Resets the runner, clearing all known modules. + * Exposes all extensions which may be overridden/provided by modules. + * + * @returns An `ExtensionsManager` which exposes the extensions. + */ + public get extensions(): ExtensionsManager { + return this.extensionsManager; + } + + /** + * Resets the runner, clearing all known modules, and all extensions * * Intended for test usage only. */ public reset(): void { this.modules = []; + this.extensionsManager = new ExtensionsManager(); } /** @@ -72,7 +172,12 @@ export class ModuleRunner { * @param factory The module factory. */ public registerModule(factory: ModuleFactory): void { - this.modules.push(new AppModule(factory)); + const appModule = new AppModule(factory); + + this.modules.push(appModule); + + // Check if the new module provides any extensions, and also ensure a given extension is only provided by a single runtime module. + this.extensionsManager.addExtensions(appModule); } /** diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 2297437479..6be0a6b46f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,6 +1,6 @@ /* Copyright 2017 Travis Ralston -Copyright 2018 - 2023 The Matrix.org Foundation C.I.C. +Copyright 2018 - 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -586,14 +586,23 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: LEVELS_ROOM_OR_ACCOUNT, default: false, }, + // Used to be a feature, name kept for backwards compat "feature_hidebold": { - isFeature: true, - labsGroup: LabGroup.Rooms, - configDisablesSetting: true, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td("labs|hidebold"), default: false, }, + "Notifications.showbold": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + displayName: _td("settings|showbold"), + default: false, + invertedSettingName: "feature_hidebold", + }, + "Notifications.tac_only_notifications": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + displayName: _td("settings|tac_only_notifications"), + default: true, + }, "feature_ask_to_join": { isFeature: true, labsGroup: LabGroup.Rooms, diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index e55e665a27..3f78ad0c92 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -21,7 +21,8 @@ import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEnc import { accessSecretStorage } from "../SecurityManager"; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; -import SecurityCustomisations from "../customisations/Security"; +import { ModuleRunner } from "../modules/ModuleRunner"; +import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import Spinner from "../components/views/elements/Spinner"; const TOAST_KEY = "setupencryption"; @@ -79,7 +80,12 @@ const onReject = (): void => { }; export const showToast = (kind: Kind): void => { - if (SecurityCustomisations.setupEncryptionNeeded?.(kind)) { + if ( + ModuleRunner.instance.extensions.cryptoSetup.setupEncryptionNeeded({ + kind: kind as any, + storeProvider: { getInstance: () => SetupEncryptionStore.sharedInstance() }, + }) + ) { return; } diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index b94ec7cb01..2ed08e0a21 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -17,6 +17,10 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import fetchMockJest from "fetch-mock-jest"; import EventEmitter from "events"; +import { + ProvideCryptoSetupExtensions, + SecretStorageKeyDescription, +} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions"; import { advanceDateAndTime, stubClient } from "./test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg"; @@ -25,6 +29,7 @@ import Modal from "../src/Modal"; import PlatformPeg from "../src/PlatformPeg"; import { SettingLevel } from "../src/settings/SettingLevel"; import { Features } from "../src/settings/Settings"; +import { ModuleRunner } from "../src/modules/ModuleRunner"; jest.useFakeTimers(); @@ -77,6 +82,78 @@ describe("MatrixClientPeg", () => { expect(peg.userRegisteredWithinLastHours(24)).toBe(false); }); + describe(".start extensions", () => { + let testPeg: IMatrixClientPeg; + + beforeEach(() => { + // instantiate a MatrixClientPegClass instance, with a new MatrixClient + testPeg = new PegClass(); + fetchMockJest.get("http://example.com/_matrix/client/versions", {}); + }); + + describe("cryptoSetup extension", () => { + it("should call default cryptoSetup.getDehydrationKeyCallback", async () => { + const mockCryptoSetup = { + SHOW_ENCRYPTION_SETUP_UI: true, + examineLoginResponse: jest.fn(), + persistCredentials: jest.fn(), + getSecretStorageKey: jest.fn(), + createSecretStorageKey: jest.fn(), + catchAccessSecretStorageError: jest.fn(), + setupEncryptionNeeded: jest.fn(), + getDehydrationKeyCallback: jest.fn().mockReturnValue(null), + } as ProvideCryptoSetupExtensions; + + // Ensure we have an instance before we set up spies + const instance = ModuleRunner.instance; + jest.spyOn(instance.extensions, "cryptoSetup", "get").mockReturnValue(mockCryptoSetup); + + testPeg.replaceUsingCreds({ + accessToken: "SEKRET", + homeserverUrl: "http://example.com", + userId: "@user:example.com", + deviceId: "TEST_DEVICE_ID", + }); + + expect(mockCryptoSetup.getDehydrationKeyCallback).toHaveBeenCalledTimes(1); + }); + + it("should call overridden cryptoSetup.getDehydrationKeyCallback", async () => { + const mockDehydrationKeyCallback = () => Uint8Array.from([0x11, 0x22, 0x33]); + + const mockCryptoSetup = { + SHOW_ENCRYPTION_SETUP_UI: true, + examineLoginResponse: jest.fn(), + persistCredentials: jest.fn(), + getSecretStorageKey: jest.fn(), + createSecretStorageKey: jest.fn(), + catchAccessSecretStorageError: jest.fn(), + setupEncryptionNeeded: jest.fn(), + getDehydrationKeyCallback: jest.fn().mockReturnValue(mockDehydrationKeyCallback), + } as ProvideCryptoSetupExtensions; + + // Ensure we have an instance before we set up spies + const instance = ModuleRunner.instance; + jest.spyOn(instance.extensions, "cryptoSetup", "get").mockReturnValue(mockCryptoSetup); + + testPeg.replaceUsingCreds({ + accessToken: "SEKRET", + homeserverUrl: "http://example.com", + userId: "@user:example.com", + deviceId: "TEST_DEVICE_ID", + }); + expect(mockCryptoSetup.getDehydrationKeyCallback).toHaveBeenCalledTimes(1); + + const client = testPeg.get(); + const dehydrationKey = await client?.cryptoCallbacks.getDehydrationKey!( + {} as SecretStorageKeyDescription, + (key: Uint8Array) => true, + ); + expect(dehydrationKey).toEqual(Uint8Array.from([0x11, 0x22, 0x33])); + }); + }); + }); + describe(".start", () => { let testPeg: IMatrixClientPeg; diff --git a/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap b/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap index bf6c01e50b..25d06ffc23 100644 --- a/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap @@ -35,5 +35,61 @@ exports[` main notification switches renders only enable notifi /> +
+
+ +
+
+
+
+
+ +
+
+
+
+
`; diff --git a/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap b/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap index 08e0d86027..84b9188a7c 100644 --- a/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap +++ b/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap @@ -641,6 +641,60 @@ exports[` correctly handles the loading/disabled state 1`] = ` role="list" />
+
+ +
+
+
+
+
+ +
+
+
+
matches the snapshot 1`] = `
+
+ +
+
+
+
+
+ +
+
+
+
matches the snapshot 1`] = ` class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid" >