From 2432bdb9cf8abe1620f1ba58e33ef79f36a3988b Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 14 Jun 2023 02:45:45 +0200 Subject: [PATCH 01/38] Implement new notification settings UI --- res/css/_components.pcss | 3 + .../settings/_NotificationSettings2.pcss | 80 ++++ .../views/settings/tabs/_SettingsBanner.pcss | 19 + .../views/settings/tabs/_SettingsIndent.pcss | 21 ++ res/img/element-icons/new-and-improved.svg | 4 + src/components/views/elements/TagComposer.tsx | 7 +- .../NotificationPusherSettings.tsx | 131 +++++++ .../notifications/NotificationSettings2.tsx | 348 ++++++++++++++++++ .../views/settings/shared/SettingsBanner.tsx | 39 ++ .../views/settings/shared/SettingsIndent.tsx | 27 ++ .../views/settings/shared/SettingsSection.tsx | 5 +- .../tabs/user/NotificationUserSettingsTab.tsx | 15 +- src/i18n/strings/en_EN.json | 29 ++ src/settings/Settings.tsx | 20 + 14 files changed, 742 insertions(+), 6 deletions(-) create mode 100644 res/css/views/settings/_NotificationSettings2.pcss create mode 100644 res/css/views/settings/tabs/_SettingsBanner.pcss create mode 100644 res/css/views/settings/tabs/_SettingsIndent.pcss create mode 100644 res/img/element-icons/new-and-improved.svg create mode 100644 src/components/views/settings/notifications/NotificationPusherSettings.tsx create mode 100644 src/components/views/settings/notifications/NotificationSettings2.tsx create mode 100644 src/components/views/settings/shared/SettingsBanner.tsx create mode 100644 src/components/views/settings/shared/SettingsIndent.tsx diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 3e8e4cd80b7..3b43a241a22 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -322,6 +322,7 @@ @import "./views/settings/_JoinRuleSettings.pcss"; @import "./views/settings/_KeyboardShortcut.pcss"; @import "./views/settings/_LayoutSwitcher.pcss"; +@import "./views/settings/_NotificationSettings2.pcss"; @import "./views/settings/_Notifications.pcss"; @import "./views/settings/_PhoneNumbers.pcss"; @import "./views/settings/_ProfileSettings.pcss"; @@ -332,6 +333,8 @@ @import "./views/settings/_SpellCheckLanguages.pcss"; @import "./views/settings/_ThemeChoicePanel.pcss"; @import "./views/settings/_UpdateCheckButton.pcss"; +@import "./views/settings/tabs/_SettingsBanner.pcss"; +@import "./views/settings/tabs/_SettingsIndent.pcss"; @import "./views/settings/tabs/_SettingsSection.pcss"; @import "./views/settings/tabs/_SettingsTab.pcss"; @import "./views/settings/tabs/room/_NotificationSettingsTab.pcss"; diff --git a/res/css/views/settings/_NotificationSettings2.pcss b/res/css/views/settings/_NotificationSettings2.pcss new file mode 100644 index 00000000000..02edf23d380 --- /dev/null +++ b/res/css/views/settings/_NotificationSettings2.pcss @@ -0,0 +1,80 @@ +.mx_NotificationSettings2 { + .mx_SettingsSection_subSections { + color: $primary-content; + gap: 32px; + display: flex; + flex-direction: column; + } + + .mx_SettingsSubsection_description { + margin-bottom: 20px; + + .mx_SettingsSubsection_text { + font-size: 1.2rem; + + .mx_NotificationBadge { + vertical-align: baseline; + display: inline-flex; + margin: 0 2px; + } + } + } + + .mx_SettingsSubsection_content { + margin-top: 12px; + grid-gap: 12px; + justify-items: stretch; + justify-content: stretch; + } + + .mx_SettingsBanner { + margin-bottom: 32px; + } + + .mx_NotificationSettings2_flags { + grid-gap: 4px; + } + + .mx_StyledRadioButton_content { + margin-left: 10px; + margin-right: 10px; + } + + .mx_TagComposer { + margin-top: 16px; + + &.mx_TagComposer_disabled { + opacity: 0.7; + } + + .mx_TagComposer_tags { + margin-top: 16px; + gap: 8px; + + .mx_Tag { + border-radius: 18px; + line-height: 2.4rem; + padding: 6px 12px; + background: $panel-actions; + margin: 0; + + .mx_Tag_delete { + background: $tertiary-content; + color: #fff; + align-self: initial; + } + } + } + } +} + +.mx_NotificationPusherSettings { + .mx_NotificationPusherSettings_description { + color: $primary-content; + } + + .mx_NotificationPusherSettings_detail { + margin-top: -4px; + margin-bottom: 12px; + } +} diff --git a/res/css/views/settings/tabs/_SettingsBanner.pcss b/res/css/views/settings/tabs/_SettingsBanner.pcss new file mode 100644 index 00000000000..084bea6e3eb --- /dev/null +++ b/res/css/views/settings/tabs/_SettingsBanner.pcss @@ -0,0 +1,19 @@ +.mx_SettingsBanner { + background: $system; + line-height: 2.25rem; + border-radius: 8px; + padding: 12px 16px; + gap: 12px; + display: flex; + flex-direction: row; + align-items: center; + + .mx_SettingsBanner_content { + margin: 0; + } + + .mx_AccessibleButton { + align-self: initial; + white-space: nowrap; + } +} diff --git a/res/css/views/settings/tabs/_SettingsIndent.pcss b/res/css/views/settings/tabs/_SettingsIndent.pcss new file mode 100644 index 00000000000..e95f0900b5e --- /dev/null +++ b/res/css/views/settings/tabs/_SettingsIndent.pcss @@ -0,0 +1,21 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +.mx_SettingsIndent { + padding-left: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} diff --git a/res/img/element-icons/new-and-improved.svg b/res/img/element-icons/new-and-improved.svg new file mode 100644 index 00000000000..113dfe8d6bd --- /dev/null +++ b/res/img/element-icons/new-and-improved.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx index 0fdd5e98cdb..c0fe8b7264e 100644 --- a/src/components/views/elements/TagComposer.tsx +++ b/src/components/views/elements/TagComposer.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import classNames from "classnames"; import React, { ChangeEvent, FormEvent } from "react"; import Field from "./Field"; @@ -67,7 +68,11 @@ export default class TagComposer extends React.PureComponent { public render(): React.ReactNode { return ( -
+
= { + kind: "email", + app_id: "m.email", + app_display_name: "Email Notifications", + lang: navigator.language, + data: { + brand: SdkConfig.get().brand, + }, +}; + +function generalTabButton(content: string): JSX.Element { + return ( + { + dispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.General, + }); + }} + > + {content} + + ); +} + +export function NotificationPusherSettings(): JSX.Element { + const cli = useMatrixClientContext(); + const [pushers, refreshPushers] = usePushers(cli); + const [threepids, refreshThreepids] = useThreepids(cli); + + const setEmailEnabled = useCallback( + (email: string, enabled: boolean) => { + if (enabled) { + cli.setPusher({ + ...EmailPusherTemplate, + pushkey: email, + device_display_name: email, + // We always append for email pushers since we don't want to stop other + // accounts notifying to the same email address + append: true, + }).catch((err) => console.error(err)); + } else { + const pusher = pushers.find((p) => p.kind === "email" && p.pushkey === email); + if (pusher) { + cli.removePusher(pusher.pushkey, pusher.app_id).catch((err) => console.error(err)); + } + } + refreshThreepids(); + refreshPushers(); + }, + [cli, pushers, refreshPushers, refreshThreepids], + ); + + const notificationTargets = pushers.filter((it) => it.kind !== "email"); + + return ( + <> + + + {_t("Receive an email summary of missed notifications")} + +
+ + {_t( + "Select which emails you want to send summaries to. Manage your emails in .", + {}, + { button: generalTabButton }, + )} + +
+ + {threepids + .filter((t) => t.medium === ThreepidMedium.Email) + .map((email) => ( + it.pushkey === email.address) !== undefined} + onChange={(value) => setEmailEnabled(email.address, value)} + /> + ))} + +
+ {notificationTargets.length > 0 && ( + +
    + {pushers + .filter((it) => it.kind !== "email") + .map((pusher) => ( +
  • {pusher.device_display_name || pusher.app_display_name}
  • + ))} +
+
+ )} + + ); +} diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx new file mode 100644 index 00000000000..f1be8d2c00f --- /dev/null +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -0,0 +1,348 @@ +/* +Copyright 2023 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import NewAndImprovedIcon from "../../../../../res/img/element-icons/new-and-improved.svg"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { useNotificationSettings } from "../../../../hooks/useNotificationSettings"; +import { useSettingValue } from "../../../../hooks/useSettings"; +import { _t } from "../../../../languageHandler"; +import { + DefaultNotificationSettings, + NotificationSettings, +} from "../../../../models/notificationsettings/NotificationSettings"; +import { RoomNotifState } from "../../../../RoomNotifs"; +import { SettingLevel } from "../../../../settings/SettingLevel"; +import SettingsStore from "../../../../settings/SettingsStore"; +import { NotificationColor } from "../../../../stores/notifications/NotificationColor"; +import { clearAllNotifications } from "../../../../utils/notifications"; +import AccessibleButton from "../../elements/AccessibleButton"; +import LabelledCheckbox from "../../elements/LabelledCheckbox"; +import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; +import StyledRadioGroup from "../../elements/StyledRadioGroup"; +import TagComposer from "../../elements/TagComposer"; +import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge"; +import { SettingsBanner } from "../shared/SettingsBanner"; +import { SettingsSection } from "../shared/SettingsSection"; +import SettingsSubsection from "../shared/SettingsSubsection"; +import { NotificationPusherSettings } from "./NotificationPusherSettings"; + +enum NotificationDefaultLevels { + ALL_MESSAGES = "ALL_MESSAGES", + PEOPLE_MENTIONS_KEYWORDS = "PEOPLE_MENTIONS_KEYWORDS", + MENTIONS_KEYWORDS = "MENTIONS_KEYWORDS", +} + +function toDefaultLevels(levels: NotificationSettings["defaultLevels"]): NotificationDefaultLevels { + if (levels.room === RoomNotifState.AllMessages) { + return NotificationDefaultLevels.ALL_MESSAGES; + } else if (levels.dm === RoomNotifState.AllMessages) { + return NotificationDefaultLevels.PEOPLE_MENTIONS_KEYWORDS; + } else { + return NotificationDefaultLevels.MENTIONS_KEYWORDS; + } +} + +const NotificationOptions = [ + { + value: NotificationDefaultLevels.ALL_MESSAGES, + label: _t("All messages"), + }, + { + value: NotificationDefaultLevels.PEOPLE_MENTIONS_KEYWORDS, + label: _t("People, Mentions and Keywords"), + }, + { + value: NotificationDefaultLevels.MENTIONS_KEYWORDS, + label: _t("Mentions and Keywords only"), + }, +]; + +function boldText(text: string): JSX.Element { + return {text}; +} + +export default function NotificationSettings2(): JSX.Element { + const cli = useMatrixClientContext(); + + const desktopNotifications = useSettingValue("notificationsEnabled"); + const desktopShowBody = useSettingValue("notificationBodyEnabled"); + const audioNotifications = useSettingValue("audioNotificationsEnabled"); + + const { model, hasPendingChanges, reconcile } = useNotificationSettings(cli); + + const disabled = model === null || hasPendingChanges; + const settings = model ?? DefaultNotificationSettings; + + return ( +
+ {hasPendingChanges && ( + } + action={_t("Switch now")} + onAction={() => reconcile(model)} + > + {_t( + "Update: We have updated our notification settings. This won’t affect your previously selected settings.", + {}, + { strong: boldText }, + )} + + )} + +
+ + reconcile({ + ...model, + globalMute: !value, + }) + } + /> + + SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, value) + } + /> + + SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, value) + } + /> + + SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, value) + } + /> +
+ + + reconcile({ + ...model, + defaultLevels: { + ...model.defaultLevels, + dm: + value !== NotificationDefaultLevels.MENTIONS_KEYWORDS + ? RoomNotifState.AllMessages + : RoomNotifState.MentionsOnly, + room: + value === NotificationDefaultLevels.ALL_MESSAGES + ? RoomNotifState.AllMessages + : RoomNotifState.MentionsOnly, + }, + }) + } + /> + + + + reconcile({ + ...model, + sound: { + ...model.sound, + people: value ? "default" : undefined, + }, + }) + } + /> + + reconcile({ + ...model, + sound: { + ...model.sound, + mentions: value ? "default" : undefined, + }, + }) + } + /> + + reconcile({ + ...model, + sound: { + ...model.sound, + calls: value ? "ring" : undefined, + }, + }) + } + /> + + + + reconcile({ + ...model, + activity: { + ...model.activity, + invite: value, + }, + }) + } + /> + + reconcile({ + ...model, + activity: { + ...model.activity, + status_event: value, + }, + }) + } + /> + + reconcile({ + ...model, + activity: { + ...model.activity, + bot_notices: value, + }, + }) + } + /> + + when keywords are used in a room.", + {}, + { + badge: , + }, + )} + > + + reconcile({ + ...model, + mentions: { + ...model.mentions, + room: value, + }, + }) + } + /> + + reconcile({ + ...model, + mentions: { + ...model.mentions, + user: value, + }, + }) + } + /> + + reconcile({ + ...model, + mentions: { + ...model.mentions, + keywords: value, + }, + }) + } + /> + { + reconcile({ + ...model, + keywords: [...model.keywords, keyword], + }); + }} + onRemove={(keyword) => { + reconcile({ + ...model, + keywords: model.keywords.filter((it) => it !== keyword), + }); + }} + label={_t("Keyword")} + placeholder={_t("New keyword")} + /> + + + + { + await clearAllNotifications(cli); + }} + > + {_t("Mark all messages as read")} + + reconcile(DefaultNotificationSettings)}> + {_t("Reset to default settings")} + + +
+
+ ); +} diff --git a/src/components/views/settings/shared/SettingsBanner.tsx b/src/components/views/settings/shared/SettingsBanner.tsx new file mode 100644 index 00000000000..fbc4a3e2501 --- /dev/null +++ b/src/components/views/settings/shared/SettingsBanner.tsx @@ -0,0 +1,39 @@ +/* +Copyright 2023 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { PropsWithChildren, ReactNode } from "react"; + +import AccessibleButton from "../../elements/AccessibleButton"; + +interface Props { + icon?: ReactNode; + action?: ReactNode; + onAction?: () => void; +} + +export function SettingsBanner({ children, icon, action, onAction }: PropsWithChildren): JSX.Element { + return ( +
+ {icon} +
{children}
+ {action && ( + + {action} + + )} +
+ ); +} diff --git a/src/components/views/settings/shared/SettingsIndent.tsx b/src/components/views/settings/shared/SettingsIndent.tsx new file mode 100644 index 00000000000..049f2303f88 --- /dev/null +++ b/src/components/views/settings/shared/SettingsIndent.tsx @@ -0,0 +1,27 @@ +/* +Copyright 2022 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { HTMLAttributes } from "react"; + +export interface SettingsIndentProps extends HTMLAttributes { + children?: React.ReactNode; +} + +export const SettingsIndent: React.FC = ({ children, ...rest }) => ( +
+ {children} +
+); diff --git a/src/components/views/settings/shared/SettingsSection.tsx b/src/components/views/settings/shared/SettingsSection.tsx index 1fc00905653..33e54aa155f 100644 --- a/src/components/views/settings/shared/SettingsSection.tsx +++ b/src/components/views/settings/shared/SettingsSection.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import classnames from "classnames"; import React, { HTMLAttributes } from "react"; import Heading from "../../typography/Heading"; @@ -40,8 +41,8 @@ export interface SettingsSectionProps extends HTMLAttributes { * * ``` */ -export const SettingsSection: React.FC = ({ heading, children, ...rest }) => ( -
+export const SettingsSection: React.FC = ({ className, heading, children, ...rest }) => ( +
{typeof heading === "string" ? {heading} : <>{heading}}
{children}
diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx index 4e95220df1f..02421d0a340 100644 --- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx @@ -17,17 +17,26 @@ limitations under the License. import React from "react"; import { _t } from "../../../../../languageHandler"; +import { Features } from "../../../../../settings/Settings"; +import SettingsStore from "../../../../../settings/SettingsStore"; import Notifications from "../../Notifications"; +import NotificationSettings2 from "../../notifications/NotificationSettings2"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsTab from "../SettingsTab"; export default class NotificationUserSettingsTab extends React.Component { public render(): React.ReactNode { + const newNotificationSettingsEnabled = SettingsStore.getValue(Features.NotificationSettings2); + return ( - - - + {newNotificationSettingsEnabled ? ( + + ) : ( + + + + )} ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d9f74d40a56..bfc1f0fb6f3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -953,6 +953,9 @@ "Can I use text chat alongside the video call?": "Can I use text chat alongside the video call?", "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.", + "New Notification Settings": "New Notification Settings", + "Notification Settings": "Notification Settings", + "A simpler way to customize notification settings in %(brand)s just the way you like.": "A simpler way to customize notification settings in %(brand)s just the way you like.", "Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog", "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.", @@ -1767,6 +1770,32 @@ "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", "You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.", "Call type": "Call type", + "Email summary": "Email summary", + "Receive an email summary of missed notifications": "Receive an email summary of missed notifications", + "Select which emails you want to send summaries to. Manage your emails in .": "Select which emails you want to send summaries to. Manage your emails in .", + "People, Mentions and Keywords": "People, Mentions and Keywords", + "Mentions and Keywords only": "Mentions and Keywords only", + "Switch now": "Switch now", + "Update: We have updated our notification settings. This won’t affect your previously selected settings.": "Update: We have updated our notification settings. This won’t affect your previously selected settings.", + "Show message preview in desktop notification": "Show message preview in desktop notification", + "I want to be notified for (Default Setting)": "I want to be notified for (Default Setting)", + "This setting will be applied by default to all your rooms.": "This setting will be applied by default to all your rooms.", + "Play a sound for": "Play a sound for", + "Applied by default to all rooms on all devices.": "Applied by default to all rooms on all devices.", + "Mentions and Keywords": "Mentions and Keywords", + "Audio and Video calls": "Audio and Video calls", + "Other things we think you might be interested in:": "Other things we think you might be interested in:", + "Invited to a room": "Invited to a room", + "New room activity, upgrades and status messages occur": "New room activity, upgrades and status messages occur", + "Messages are sent by a bot": "Messages are sent by a bot", + "Show a badge when keywords are used in a room.": "Show a badge when keywords are used in a room.", + "Notify when someone mentions using @room": "Notify when someone mentions using @room", + "Notify when someone mentions using @displayname or @mxid": "Notify when someone mentions using @displayname or @mxid", + "Notify when someone uses a keyword": "Notify when someone uses a keyword", + "Enter keywords here, or use for spelling variations or nicknames": "Enter keywords here, or use for spelling variations or nicknames", + "Quick Actions": "Quick Actions", + "Mark all messages as read": "Mark all messages as read", + "Reset to default settings": "Reset to default settings", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 52bcac185ad..7efddf8e49d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -93,6 +93,7 @@ export enum LabGroup { export enum Features { VoiceBroadcast = "feature_voice_broadcast", VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks", + NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", } @@ -220,6 +221,25 @@ export const SETTINGS: { [setting: string]: ISetting } = { requiresRefresh: true, }, }, + [Features.NotificationSettings2]: { + isFeature: true, + labsGroup: LabGroup.Experimental, + supportedLevels: LEVELS_FEATURE, + displayName: _td("New Notification Settings"), + default: false, + betaInfo: { + title: _td("Notification Settings"), + caption: () => ( + <> +

+ {_t("A simpler way to customize notification settings in %(brand)s just the way you like.", { + brand: SdkConfig.get().brand, + })} +

+ + ), + }, + }, "feature_exploring_public_spaces": { isFeature: true, labsGroup: LabGroup.Spaces, From 57499df47fd3970df72f834d62ff469ed5abce2e Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 14 Jun 2023 02:56:25 +0200 Subject: [PATCH 02/38] Sort new keywords at the front --- .../views/settings/notifications/NotificationSettings2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index f1be8d2c00f..1fc3efdcb89 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -315,7 +315,7 @@ export default function NotificationSettings2(): JSX.Element { onAdd={(keyword) => { reconcile({ ...model, - keywords: [...model.keywords, keyword], + keywords: [keyword, ...model.keywords], }); }} onRemove={(keyword) => { From 1845df610c11d5cbbd195a000dca0c4534ad0f98 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 19 Jun 2023 02:38:57 +0200 Subject: [PATCH 03/38] Make ts-strict happier --- .../notifications/NotificationSettings2.tsx | 258 ++++++++++-------- 1 file changed, 145 insertions(+), 113 deletions(-) diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index 1fc3efdcb89..a81d6711292 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -90,7 +90,7 @@ export default function NotificationSettings2(): JSX.Element { return (
- {hasPendingChanges && ( + {hasPendingChanges && model !== null && ( } action={_t("Switch now")} @@ -109,12 +109,14 @@ export default function NotificationSettings2(): JSX.Element { label={_t("Enable notifications for this account")} value={!settings.globalMute} disabled={disabled} - onChange={(value) => - reconcile({ - ...model, - globalMute: !value, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + globalMute: !value, + }); + } + }} /> - reconcile({ - ...model, - defaultLevels: { - ...model.defaultLevels, - dm: - value !== NotificationDefaultLevels.MENTIONS_KEYWORDS - ? RoomNotifState.AllMessages - : RoomNotifState.MentionsOnly, - room: - value === NotificationDefaultLevels.ALL_MESSAGES - ? RoomNotifState.AllMessages - : RoomNotifState.MentionsOnly, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + defaultLevels: { + ...model.defaultLevels, + dm: + value !== NotificationDefaultLevels.MENTIONS_KEYWORDS + ? RoomNotifState.AllMessages + : RoomNotifState.MentionsOnly, + room: + value === NotificationDefaultLevels.ALL_MESSAGES + ? RoomNotifState.AllMessages + : RoomNotifState.MentionsOnly, + }, + }); + } + }} /> - reconcile({ - ...model, - sound: { - ...model.sound, - people: value ? "default" : undefined, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + sound: { + ...model.sound, + people: value ? "default" : undefined, + }, + }); + } + }} /> - reconcile({ - ...model, - sound: { - ...model.sound, - mentions: value ? "default" : undefined, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + sound: { + ...model.sound, + mentions: value ? "default" : undefined, + }, + }); + } + }} /> - reconcile({ - ...model, - sound: { - ...model.sound, - calls: value ? "ring" : undefined, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + sound: { + ...model.sound, + calls: value ? "ring" : undefined, + }, + }); + } + }} /> @@ -217,43 +227,49 @@ export default function NotificationSettings2(): JSX.Element { label={_t("Invited to a room")} value={settings.activity.invite} disabled={disabled} - onChange={(value) => - reconcile({ - ...model, - activity: { - ...model.activity, - invite: value, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + activity: { + ...model.activity, + invite: value, + }, + }); + } + }} /> - reconcile({ - ...model, - activity: { - ...model.activity, - status_event: value, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + activity: { + ...model.activity, + status_event: value, + }, + }); + } + }} /> - reconcile({ - ...model, - activity: { - ...model.activity, - bot_notices: value, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + activity: { + ...model.activity, + bot_notices: value, + }, + }); + } + }} /> - reconcile({ - ...model, - mentions: { - ...model.mentions, - room: value, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + mentions: { + ...model.mentions, + room: value, + }, + }); + } + }} /> - reconcile({ - ...model, - mentions: { - ...model.mentions, - user: value, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + mentions: { + ...model.mentions, + user: value, + }, + }); + } + }} /> - reconcile({ - ...model, - mentions: { - ...model.mentions, - keywords: value, - }, - }) - } + onChange={(value) => { + if (model !== null) { + reconcile({ + ...model, + mentions: { + ...model.mentions, + keywords: value, + }, + }); + } + }} /> { - reconcile({ - ...model, - keywords: [keyword, ...model.keywords], - }); + if (model !== null) { + reconcile({ + ...model, + keywords: [keyword, ...model.keywords], + }); + } }} onRemove={(keyword) => { - reconcile({ - ...model, - keywords: model.keywords.filter((it) => it !== keyword), - }); + if (model !== null) { + reconcile({ + ...model, + keywords: model.keywords.filter((it) => it !== keyword), + }); + } }} label={_t("Keyword")} placeholder={_t("New keyword")} @@ -338,7 +364,13 @@ export default function NotificationSettings2(): JSX.Element { > {_t("Mark all messages as read")} - reconcile(DefaultNotificationSettings)}> + { + if (model !== null) { + reconcile(DefaultNotificationSettings); + } + }}> {_t("Reset to default settings")} From 130900bf652f8c13e665ec4e1bfa4d5a1d500a8c Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 19 Jun 2023 22:14:29 +0200 Subject: [PATCH 04/38] Make ts-strict happier --- src/components/views/settings/shared/SettingsBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/shared/SettingsBanner.tsx b/src/components/views/settings/shared/SettingsBanner.tsx index fbc4a3e2501..85dfb1d89d8 100644 --- a/src/components/views/settings/shared/SettingsBanner.tsx +++ b/src/components/views/settings/shared/SettingsBanner.tsx @@ -30,7 +30,7 @@ export function SettingsBanner({ children, icon, action, onAction }: PropsWithCh {icon}
{children}
{action && ( - + {action} )} From f878d32f600920b7ab55db8316ca28a63aa2a4a2 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 19 Jun 2023 22:19:26 +0200 Subject: [PATCH 05/38] chore: fixed lint issues --- .../views/settings/notifications/NotificationSettings2.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index a81d6711292..a94d435aac8 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -370,7 +370,8 @@ export default function NotificationSettings2(): JSX.Element { if (model !== null) { reconcile(DefaultNotificationSettings); } - }}> + }} + > {_t("Reset to default settings")} From a814b86998567adc2083f6af72869aa72291339b Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 20 Jun 2023 15:16:38 +0200 Subject: [PATCH 06/38] update beta card --- res/img/betas/notification_settings.png | Bin 0 -> 57547 bytes src/settings/Settings.tsx | 1 + .../LabsUserSettingsTab-test.tsx.snap | 51 ++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 res/img/betas/notification_settings.png diff --git a/res/img/betas/notification_settings.png b/res/img/betas/notification_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..5e021de5663d044cc23b8ef0022295db4e12a67b GIT binary patch literal 57547 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 I)P4Ow07o`+MF0Q* literal 0 HcmV?d00001 diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 7efddf8e49d..69e63409dde 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -238,6 +238,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {

), + image: require("../../res/img/betas/notification_settings.png"), }, }, "feature_exploring_public_spaces": { 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 bf94e477272..b560f0a874b 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 @@ -79,6 +79,57 @@ exports[` renders settings marked as beta as beta cards 1
+
+
+
+

+ + Notification Settings + + + Beta + +

+
+

+ A simpler way to customize notification settings in just the way you like. +

+
+
+
+ Join the beta +
+
+
+
+ +
+
+
`; From 1bc79258d2a824cbc0010d93ee54f2b9303f8ec2 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 20 Jun 2023 15:52:49 +0200 Subject: [PATCH 07/38] Fix issue with the user settings test --- .../NotificationPusherSettings.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/views/settings/notifications/NotificationPusherSettings.tsx b/src/components/views/settings/notifications/NotificationPusherSettings.tsx index 794dd63e37c..e963218b5fe 100644 --- a/src/components/views/settings/notifications/NotificationPusherSettings.tsx +++ b/src/components/views/settings/notifications/NotificationPusherSettings.tsx @@ -16,7 +16,7 @@ limitations under the License. import { ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; import { IPusher } from "matrix-js-sdk/src/matrix"; -import React, { useCallback } from "react"; +import React, { useCallback, useMemo } from "react"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import { Action } from "../../../../dispatcher/actions"; @@ -31,16 +31,6 @@ import LabelledCheckbox from "../../elements/LabelledCheckbox"; import { SettingsIndent } from "../shared/SettingsIndent"; import SettingsSubsection, { SettingsSubsectionText } from "../shared/SettingsSubsection"; -const EmailPusherTemplate: Omit = { - kind: "email", - app_id: "m.email", - app_display_name: "Email Notifications", - lang: navigator.language, - data: { - brand: SdkConfig.get().brand, - }, -}; - function generalTabButton(content: string): JSX.Element { return ( = useMemo(() => ({ + kind: "email", + app_id: "m.email", + app_display_name: "Email Notifications", + lang: navigator.language, + data: { + brand: SdkConfig.get().brand, + }, + }), []); + const cli = useMatrixClientContext(); const [pushers, refreshPushers] = usePushers(cli); const [threepids, refreshThreepids] = useThreepids(cli); From e96793f1ddebf14f4a568c06ea2d8fc8605f36d5 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 20 Jun 2023 15:53:01 +0200 Subject: [PATCH 08/38] chore: fixed lint issues --- .../NotificationPusherSettings.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/views/settings/notifications/NotificationPusherSettings.tsx b/src/components/views/settings/notifications/NotificationPusherSettings.tsx index e963218b5fe..8f788feea1c 100644 --- a/src/components/views/settings/notifications/NotificationPusherSettings.tsx +++ b/src/components/views/settings/notifications/NotificationPusherSettings.tsx @@ -48,15 +48,18 @@ function generalTabButton(content: string): JSX.Element { } export function NotificationPusherSettings(): JSX.Element { - const EmailPusherTemplate: Omit = useMemo(() => ({ - kind: "email", - app_id: "m.email", - app_display_name: "Email Notifications", - lang: navigator.language, - data: { - brand: SdkConfig.get().brand, - }, - }), []); + const EmailPusherTemplate: Omit = useMemo( + () => ({ + kind: "email", + app_id: "m.email", + app_display_name: "Email Notifications", + lang: navigator.language, + data: { + brand: SdkConfig.get().brand, + }, + }), + [], + ); const cli = useMatrixClientContext(); const [pushers, refreshPushers] = usePushers(cli); @@ -82,7 +85,7 @@ export function NotificationPusherSettings(): JSX.Element { refreshThreepids(); refreshPushers(); }, - [cli, pushers, refreshPushers, refreshThreepids], + [EmailPusherTemplate, cli, pushers, refreshPushers, refreshThreepids], ); const notificationTargets = pushers.filter((it) => it.kind !== "email"); From 5f2801a55784220b2b07cd7173b7104c3978a0ae Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 22 Jun 2023 11:30:55 +0200 Subject: [PATCH 09/38] Add tests for notification settings --- .../notifications/NotificationSettings2.tsx | 31 +- .../notifications/Notifications2-test.tsx | 390 ++++++++ .../Notifications2-test.tsx.snap | 875 ++++++++++++++++++ test/predictableRandom.ts | 25 + 4 files changed, 1312 insertions(+), 9 deletions(-) create mode 100644 test/components/views/settings/notifications/Notifications2-test.tsx create mode 100644 test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap create mode 100644 test/predictableRandom.ts diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index a94d435aac8..3841b805674 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useState } from "react"; import NewAndImprovedIcon from "../../../../../res/img/element-icons/new-and-improved.svg"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; @@ -76,6 +76,11 @@ function boldText(text: string): JSX.Element { return {text}; } +function useHasUnreadNotifications(): boolean { + const cli = useMatrixClientContext(); + return cli.getRooms().some(room => room.getUnreadNotificationCount() > 0); +} + export default function NotificationSettings2(): JSX.Element { const cli = useMatrixClientContext(); @@ -88,6 +93,9 @@ export default function NotificationSettings2(): JSX.Element { const disabled = model === null || hasPendingChanges; const settings = model ?? DefaultNotificationSettings; + const [updatingUnread, setUpdatingUnread] = useState(false); + const hasUnreadNotifications = useHasUnreadNotifications(); + return (
{hasPendingChanges && model !== null && ( @@ -356,14 +364,19 @@ export default function NotificationSettings2(): JSX.Element { - { - await clearAllNotifications(cli); - }} - > - {_t("Mark all messages as read")} - + {hasUnreadNotifications && ( + { + setUpdatingUnread(true); + await clearAllNotifications(cli); + setUpdatingUnread(false); + }} + > + {_t("Mark all messages as read")} + + )} { diff --git a/test/components/views/settings/notifications/Notifications2-test.tsx b/test/components/views/settings/notifications/Notifications2-test.tsx new file mode 100644 index 00000000000..8378f75a4e0 --- /dev/null +++ b/test/components/views/settings/notifications/Notifications2-test.tsx @@ -0,0 +1,390 @@ +/* +Copyright 2022, 2023 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. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { act, findByRole, fireEvent, queryByRole, render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { IPushRules, MatrixClient, NotificationCountType, PushRuleKind, Room, RuleId } from "matrix-js-sdk/src/matrix"; +import React from "react"; +import { ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; + +import NotificationSettings2 from "../../../../../src/components/views/settings/notifications/NotificationSettings2"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import { StandardActions } from "../../../../../src/notifications/StandardActions"; +import { mockRandom } from "../../../../predictableRandom"; +import { mkMessage, stubClient } from "../../../../test-utils"; + +mockRandom(); + +const waitForUpdate = (): Promise => new Promise(resolve => setTimeout(resolve)); + +describe("", () => { + let cli: MatrixClient; + let pushRules: IPushRules; + + beforeAll(async () => { + pushRules = (await import("../../../../models/notificationsettings/pushrules_sample.json")) as IPushRules; + }); + + beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.safeGet(); + cli.getPushRules = jest.fn(cli.getPushRules).mockResolvedValue(pushRules); + cli.supportsIntentionalMentions = jest.fn(cli.supportsIntentionalMentions).mockReturnValue(false); + cli.setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled); + cli.setPushRuleActions = jest.fn(cli.setPushRuleActions); + cli.removePusher = jest.fn(cli.removePusher).mockResolvedValue({}); + cli.setPusher = jest.fn(cli.setPusher).mockResolvedValue({}); + }); + + it("matches the snapshot", async () => { + cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({ + pushers: [ + { + "app_display_name": "Element", + "app_id": "im.vector.app", + "data": {}, + "device_display_name": "My EyeFon", + "kind": "http", + "lang": "en", + "pushkey": "", + "enabled": true, + }, + ], + }) + cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({ + threepids: [ + { + medium: ThreepidMedium.Email, + address: "test@example.tld", + validated_at: 1656633600, + added_at: 1656633600, + }, + ], + }); + + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.container).toMatchSnapshot(); + }) + + describe("form elements actually toggle the model value", () => { + it("global mute", async () => { + const label = "Enable notifications for this account"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.Master, true); + }) + + it("notification level", async () => { + const label = "All messages"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.EncryptedMessage, true); + expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true); + }) + + describe("play a sound for", () => { + it("people", async () => { + const label = "People"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.EncryptedDM, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.InviteToSelf, StandardActions.ACTION_NOTIFY_DEFAULT_SOUND); + }) + + it("mentions", async () => { + const label = "Mentions and Keywords"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.ContainsDisplayName, StandardActions.ACTION_HIGHLIGHT); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, RuleId.ContainsUserName, StandardActions.ACTION_HIGHLIGHT); + }) + + it("calls", async () => { + const label = "Audio and Video calls"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.IncomingCall, StandardActions.ACTION_NOTIFY); + }) + }) + + describe("activity", () => { + it("invite", async () => { + const label = "Invited to a room"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.InviteToSelf, StandardActions.ACTION_NOTIFY); + }) + it("status messages", async () => { + const label = "New room activity, upgrades and status messages occur"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.MemberEvent, StandardActions.ACTION_NOTIFY); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.Tombstone, StandardActions.ACTION_HIGHLIGHT); + }) + it("notices", async () => { + const label = "Messages are sent by a bot"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.SuppressNotices, StandardActions.ACTION_DONT_NOTIFY); + }) + }) + describe("mentions", () => { + it("room mentions", async () => { + const label = "Notify when someone mentions using @room"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.AtRoomNotification, StandardActions.ACTION_DONT_NOTIFY); + }) + it("user mentions", async () => { + const label = "Notify when someone mentions using @displayname or @mxid"; + + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.ContainsDisplayName, StandardActions.ACTION_DONT_NOTIFY); + expect(cli.setPushRuleActions).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, RuleId.ContainsUserName, StandardActions.ACTION_DONT_NOTIFY); + }) + }) + }) + + describe("pusher settings", () => { + it("can create email pushers", async () => { + cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({ + pushers: [ + { + "app_display_name": "Element", + "app_id": "im.vector.app", + "data": {}, + "device_display_name": "My EyeFon", + "kind": "http", + "lang": "en", + "pushkey": "", + "enabled": true, + }, + ], + }) + cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({ + threepids: [ + { + medium: ThreepidMedium.Email, + address: "test@example.tld", + validated_at: 1656633600, + added_at: 1656633600, + }, + ], + }); + + const label = "test@example.tld"; + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.setPusher).toHaveBeenCalledWith({ + "app_display_name": "Email Notifications", + "app_id": "m.email", + "append": true, + "data": { "brand": "Element" }, + "device_display_name": "test@example.tld", + "kind": "email", + "lang": "en-US", + "pushkey": "test@example.tld", + }); + }) + + it("can remove email pushers", async () => { + cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({ + pushers: [ + { + "app_display_name": "Element", + "app_id": "im.vector.app", + "data": {}, + "device_display_name": "My EyeFon", + "kind": "http", + "lang": "en", + "pushkey": "abctest", + }, + { + "app_display_name": "Email Notifications", + "app_id": "m.email", + "data": { "brand": "Element" }, + "device_display_name": "test@example.tld", + "kind": "email", + "lang": "en-US", + "pushkey": "test@example.tld", + }, + ], + }) + cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({ + threepids: [ + { + medium: ThreepidMedium.Email, + address: "test@example.tld", + validated_at: 1656633600, + added_at: 1656633600, + }, + ], + }); + + const label = "test@example.tld"; + const user = userEvent.setup() + const screen = render( + + + , + ); + await act(waitForUpdate); + expect(screen.getByLabelText(label)).not.toBeDisabled(); + await act(() => user.click(screen.getByLabelText(label))); + expect(cli.removePusher).toHaveBeenCalledWith("test@example.tld", "m.email"); + }) + }) + + describe("clear all notifications", () => { + it("is hidden when no notifications exist", async () => { + const room = new Room("room123", cli, "@alice:example.org"); + cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]); + + const { container } = render( + + + , + ); + await waitForUpdate(); + expect(queryByRole(container, "button", { + name: "Mark all messages as read", + })).not.toBeInTheDocument(); + }); + + it("clears all notifications", async () => { + const room = new Room("room123", cli, "@alice:example.org"); + cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]); + + const message = mkMessage({ + event: true, + room: "room123", + user: "@alice:example.org", + ts: 1, + }); + room.addLiveEvents([message]); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + + const { container } = render( + + + , + ); + await waitForUpdate(); + const clearNotificationEl = await findByRole(container, "button", { + name: "Mark all messages as read", + }); + + fireEvent.click(clearNotificationEl); + expect(cli.sendReadReceipt).toHaveBeenCalled(); + + await waitFor(() => { + expect(clearNotificationEl).not.toBeInTheDocument(); + }); + }); + }); +}); diff --git a/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap b/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap new file mode 100644 index 00000000000..dd53f76d2ac --- /dev/null +++ b/test/components/views/settings/notifications/__snapshots__/Notifications2-test.tsx.snap @@ -0,0 +1,875 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` matches the snapshot 1`] = ` +
+
+
+

+ Notifications +

+
+
+
+ +
+ Enable notifications for this account +
+
+
+
+
+
+
+ +
+ Enable desktop notifications for this session +
+
+
+
+
+
+
+ +
+ Show message preview in desktop notification +
+
+
+
+
+
+
+ +
+ Enable audible notifications for this session +
+
+
+
+
+
+
+
+
+

+ I want to be notified for (Default Setting) +

+
+
+
+ This setting will be applied by default to all your rooms. +
+
+
+