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 ( + <div> + <SettingsFlag name="Notifications.showbold" level={SettingLevel.DEVICE} /> + <SettingsFlag name="Notifications.tac_only_notifications" level={SettingLevel.DEVICE} /> + </div> + ); +}; + +/** + * The old, deprecated notifications tab view, only displayed if the user has the labs flag disabled. + */ export default class Notifications extends React.PureComponent<IProps, IState> { private settingWatchers: string[]; @@ -731,43 +744,10 @@ export default class Notifications extends React.PureComponent<IProps, IState> { } 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 = ( - <AccessibleButton - onClick={this.onClearNotificationsClicked} - disabled={this.state.clearingNotifications} - kind="danger" - className="mx_UserNotifSettings_clearNotifsButton" - data-testid="clear-notifications" - > - {_t("notifications|mark_all_read")} - </AccessibleButton> - ); - } - - if (category === RuleClass.VectorOther && this.isInhibited) { - // only render the utility buttons (if needed) - if (clearNotifsButton) { - return ( - <div className="mx_UserNotifSettings_floatingSection"> - <div>{_t("notifications|class_other")}</div> - {clearNotifsButton} - </div> - ); - } - return null; - } - let keywordComposer: JSX.Element | undefined; if (category === RuleClass.VectorMentions) { const tags = filterBoolean<string>(this.state.vectorKeywordRuleInfo?.rules.map((r) => r.pattern) || []); @@ -842,7 +822,6 @@ export default class Notifications extends React.PureComponent<IProps, IState> { <span className="mx_UserNotifSettings_gridColumnLabel">{VectorStateToLabel[VectorState.Loud]}</span> {fieldsetRows} </div> - {clearNotifsButton} {keywordComposer} </div> ); @@ -878,6 +857,25 @@ export default class Notifications extends React.PureComponent<IProps, IState> { return <p data-testid="error-message">{_t("settings|notifications|error_loading")}</p>; } + let clearNotifsButton: JSX.Element | undefined; + if ( + MatrixClientPeg.safeGet() + .getRooms() + .some((r) => doesRoomHaveUnreadMessages(r, true)) + ) { + clearNotifsButton = ( + <AccessibleButton + onClick={this.onClearNotificationsClicked} + disabled={this.state.clearingNotifications} + kind="danger" + className="mx_UserNotifSettings_clearNotifsButton" + data-testid="clear-notifications" + > + {_t("notifications|mark_all_read")} + </AccessibleButton> + ); + } + return ( <> {this.renderTopSection()} @@ -885,6 +883,8 @@ export default class Notifications extends React.PureComponent<IProps, IState> { {this.renderCategory(RuleClass.VectorMentions)} {this.renderCategory(RuleClass.VectorOther)} {this.renderTargets()} + <NotificationActivitySettings /> + {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")} /> + + <SettingsFlag name="Notifications.showbold" level={SettingLevel.DEVICE} /> + <SettingsFlag name="Notifications.tac_only_notifications" level={SettingLevel.DEVICE} /> </SettingsSubsection> <NotificationPusherSettings /> <SettingsSubsection heading={_t("settings|notifications|quick_actions_section")}> 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<boolean>("feature_dynamic_room_predecessors"); + const settingTACOnlyNotifs = useSettingValue<boolean>("Notifications.tac_only_notifications"); const mxClient = useMatrixClientContext(); const [result, setResult] = useState<Result>({ 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/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/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/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[`<Notifications /> main notification switches renders only enable notifi /> </div> </div> + <div> + <div + class="mx_SettingsFlag" + > + <label + class="mx_SettingsFlag_label" + for="mx_SettingsFlag_testid_1" + > + <span + class="mx_SettingsFlag_labelText" + > + Show all activity in the room list (dots or number of unread messages) + </span> + </label> + <div + aria-checked="true" + aria-disabled="false" + aria-label="Show all activity in the room list (dots or number of unread messages)" + class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" + id="mx_SettingsFlag_testid_1" + role="switch" + tabindex="0" + > + <div + class="mx_ToggleSwitch_ball" + /> + </div> + </div> + <div + class="mx_SettingsFlag" + > + <label + class="mx_SettingsFlag_label" + for="mx_SettingsFlag_testid_2" + > + <span + class="mx_SettingsFlag_labelText" + > + Only show notifications in the thread activity centre + </span> + </label> + <div + aria-checked="true" + aria-disabled="false" + aria-label="Only show notifications in the thread activity centre" + class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" + id="mx_SettingsFlag_testid_2" + role="switch" + tabindex="0" + > + <div + class="mx_ToggleSwitch_ball" + /> + </div> + </div> + </div> </div> `; 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[`<Notifications /> correctly handles the loading/disabled state 1`] = ` role="list" /> </div> + <div + class="mx_SettingsFlag" + > + <label + class="mx_SettingsFlag_label" + for="mx_SettingsFlag_QRlYy75nfv5b" + > + <span + class="mx_SettingsFlag_labelText" + > + Show all activity in the room list (dots or number of unread messages) + </span> + </label> + <div + aria-checked="true" + aria-disabled="false" + aria-label="Show all activity in the room list (dots or number of unread messages)" + class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" + id="mx_SettingsFlag_QRlYy75nfv5b" + role="switch" + tabindex="0" + > + <div + class="mx_ToggleSwitch_ball" + /> + </div> + </div> + <div + class="mx_SettingsFlag" + > + <label + class="mx_SettingsFlag_label" + for="mx_SettingsFlag_OEPN1su1JYVt" + > + <span + class="mx_SettingsFlag_labelText" + > + Only show notifications in the thread activity centre + </span> + </label> + <div + aria-checked="true" + aria-disabled="false" + aria-label="Only show notifications in the thread activity centre" + class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" + id="mx_SettingsFlag_OEPN1su1JYVt" + role="switch" + tabindex="0" + > + <div + class="mx_ToggleSwitch_ball" + /> + </div> + </div> </div> </div> <div @@ -1472,6 +1526,60 @@ exports[`<Notifications /> matches the snapshot 1`] = ` </div> </div> </div> + <div + class="mx_SettingsFlag" + > + <label + class="mx_SettingsFlag_label" + for="mx_SettingsFlag_QRlYy75nfv5b" + > + <span + class="mx_SettingsFlag_labelText" + > + Show all activity in the room list (dots or number of unread messages) + </span> + </label> + <div + aria-checked="true" + aria-disabled="false" + aria-label="Show all activity in the room list (dots or number of unread messages)" + class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" + id="mx_SettingsFlag_QRlYy75nfv5b" + role="switch" + tabindex="0" + > + <div + class="mx_ToggleSwitch_ball" + /> + </div> + </div> + <div + class="mx_SettingsFlag" + > + <label + class="mx_SettingsFlag_label" + for="mx_SettingsFlag_OEPN1su1JYVt" + > + <span + class="mx_SettingsFlag_labelText" + > + Only show notifications in the thread activity centre + </span> + </label> + <div + aria-checked="true" + aria-disabled="false" + aria-label="Only show notifications in the thread activity centre" + class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" + id="mx_SettingsFlag_OEPN1su1JYVt" + role="switch" + tabindex="0" + > + <div + class="mx_ToggleSwitch_ball" + /> + </div> + </div> </div> </div> <div @@ -1523,11 +1631,11 @@ exports[`<Notifications /> matches the snapshot 1`] = ` class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid" > <input - id="checkbox_QRlYy75nfv" + id="checkbox_OyR5kbu3pE" type="checkbox" /> <label - for="checkbox_QRlYy75nfv" + for="checkbox_OyR5kbu3pE" > <div class="mx_Checkbox_background" diff --git a/test/components/views/spaces/useUnreadThreadRooms-test.tsx b/test/components/views/spaces/useUnreadThreadRooms-test.tsx index e8c9b97861..d4dc04da96 100644 --- a/test/components/views/spaces/useUnreadThreadRooms-test.tsx +++ b/test/components/views/spaces/useUnreadThreadRooms-test.tsx @@ -30,6 +30,7 @@ import { stubClient } from "../../../test-utils"; import { populateThread } from "../../../test-utils/threads"; import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel"; import { useUnreadThreadRooms } from "../../../../src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms"; +import SettingsStore from "../../../../src/settings/SettingsStore"; describe("useUnreadThreadRooms", () => { let client: MatrixClient; @@ -43,6 +44,10 @@ describe("useUnreadThreadRooms", () => { }); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it("has no notifications with no rooms", async () => { const { result } = renderHook(() => useUnreadThreadRooms(false)); const { greatestNotificationLevel, rooms } = result.current; @@ -51,7 +56,7 @@ describe("useUnreadThreadRooms", () => { expect(rooms.length).toEqual(0); }); - it("an activity notification is ignored", async () => { + it("an activity notification is ignored by default", async () => { const notifThreadInfo = await populateThread({ room: room, client: client, @@ -73,6 +78,30 @@ describe("useUnreadThreadRooms", () => { expect(rooms.length).toEqual(0); }); + it("an activity notification is displayed with the setting enabled", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + + const notifThreadInfo = await populateThread({ + room: room, + client: client, + authorId: "@foo:bar", + participantUserIds: ["@fee:bar"], + }); + room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 0); + + client.getVisibleRooms = jest.fn().mockReturnValue([room]); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + <MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider> + ); + + const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper }); + const { greatestNotificationLevel, rooms } = result.current; + + expect(greatestNotificationLevel).toBe(NotificationLevel.Activity); + expect(rooms.length).toEqual(1); + }); + it("a notification and a highlight summarise to a highlight", async () => { const notifThreadInfo = await populateThread({ room: room,