diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 2851313c54e..0fd24fbaf2a 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -321,6 +321,8 @@
@import "./views/settings/_JoinRuleSettings.pcss";
@import "./views/settings/_KeyboardShortcut.pcss";
@import "./views/settings/_LayoutSwitcher.pcss";
+@import "./views/settings/_NotificationPusherSettings.pcss";
+@import "./views/settings/_NotificationSettings2.pcss";
@import "./views/settings/_Notifications.pcss";
@import "./views/settings/_PhoneNumbers.pcss";
@import "./views/settings/_ProfileSettings.pcss";
@@ -331,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/_NotificationPusherSettings.pcss b/res/css/views/settings/_NotificationPusherSettings.pcss
new file mode 100644
index 00000000000..db573bb4eed
--- /dev/null
+++ b/res/css/views/settings/_NotificationPusherSettings.pcss
@@ -0,0 +1,26 @@
+/*
+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.
+*/
+
+.mx_NotificationPusherSettings {
+ .mx_NotificationPusherSettings_description {
+ color: $primary-content;
+ }
+
+ .mx_NotificationPusherSettings_detail {
+ margin-top: -4px;
+ margin-bottom: 12px;
+ }
+}
diff --git a/res/css/views/settings/_NotificationSettings2.pcss b/res/css/views/settings/_NotificationSettings2.pcss
new file mode 100644
index 00000000000..0bad2e992c6
--- /dev/null
+++ b/res/css/views/settings/_NotificationSettings2.pcss
@@ -0,0 +1,85 @@
+/*
+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.
+*/
+
+.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;
+ }
+ }
+ }
+ }
+}
diff --git a/res/css/views/settings/tabs/_SettingsBanner.pcss b/res/css/views/settings/tabs/_SettingsBanner.pcss
new file mode 100644
index 00000000000..f0b9e7106bb
--- /dev/null
+++ b/res/css/views/settings/tabs/_SettingsBanner.pcss
@@ -0,0 +1,35 @@
+/*
+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.
+*/
+
+.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..fe328d43643
--- /dev/null
+++ b/res/css/views/settings/tabs/_SettingsIndent.pcss
@@ -0,0 +1,22 @@
+/*
+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.
+*/
+
+.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/Tag.tsx b/src/components/views/elements/Tag.tsx
index d7d46fe7e11..6ce2ba69fcb 100644
--- a/src/components/views/elements/Tag.tsx
+++ b/src/components/views/elements/Tag.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+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.
@@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import AccessibleButton from "./AccessibleButton";
import { Icon as CancelRounded } from "../../../../res/img/element-icons/cancel-rounded.svg";
-interface IProps {
+interface IProps extends DetailedHTMLProps, HTMLDivElement> {
icon?: () => JSX.Element;
label: string;
onDeleteClick?: () => void;
disabled?: boolean;
}
-export const Tag: React.FC = ({ icon, label, onDeleteClick, disabled = false }) => {
+export const Tag: React.FC = ({ icon, label, onDeleteClick, disabled = false, ...other }) => {
return (
-
+
{icon?.()}
{label}
{onDeleteClick && (
diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx
index 0fdd5e98cdb..4e7ed302f08 100644
--- a/src/components/views/elements/TagComposer.tsx
+++ b/src/components/views/elements/TagComposer.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021-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.
@@ -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";
@@ -22,6 +23,7 @@ import AccessibleButton from "./AccessibleButton";
import { Tag } from "./Tag";
interface IProps {
+ id?: string;
tags: string[];
onAdd: (tag: string) => void;
onRemove: (tag: string) => void;
@@ -67,9 +69,14 @@ export default class TagComposer extends React.PureComponent {
public render(): React.ReactNode {
return (
-
+
-
+
{this.props.tags.map((t, i) => (
))}
diff --git a/src/components/views/settings/notifications/NotificationPusherSettings.tsx b/src/components/views/settings/notifications/NotificationPusherSettings.tsx
new file mode 100644
index 00000000000..5035e585509
--- /dev/null
+++ b/src/components/views/settings/notifications/NotificationPusherSettings.tsx
@@ -0,0 +1,134 @@
+/*
+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 { ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
+import { IPusher } from "matrix-js-sdk/src/matrix";
+import React, { useCallback, useMemo } from "react";
+
+import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
+import { Action } from "../../../../dispatcher/actions";
+import dispatcher from "../../../../dispatcher/dispatcher";
+import { usePushers } from "../../../../hooks/usePushers";
+import { useThreepids } from "../../../../hooks/useThreepids";
+import { _t } from "../../../../languageHandler";
+import SdkConfig from "../../../../SdkConfig";
+import { UserTab } from "../../dialogs/UserTab";
+import AccessibleButton from "../../elements/AccessibleButton";
+import LabelledCheckbox from "../../elements/LabelledCheckbox";
+import { SettingsIndent } from "../shared/SettingsIndent";
+import SettingsSubsection, { SettingsSubsectionText } from "../shared/SettingsSubsection";
+
+function generalTabButton(content: string): JSX.Element {
+ return (
+ {
+ dispatcher.dispatch({
+ action: Action.ViewUserSettings,
+ initialTabId: UserTab.General,
+ });
+ }}
+ >
+ {content}
+
+ );
+}
+
+export function NotificationPusherSettings(): JSX.Element {
+ const EmailPusherTemplate: Omit = useMemo(
+ () => ({
+ kind: "email",
+ app_id: "m.email",
+ app_display_name: _t("Email Notifications"),
+ lang: navigator.language,
+ data: {
+ brand: SdkConfig.get().brand,
+ },
+ }),
+ [],
+ );
+
+ 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();
+ },
+ [EmailPusherTemplate, 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 },
+ )}
+
+
+ );
+}
diff --git a/src/components/views/settings/shared/SettingsBanner.tsx b/src/components/views/settings/shared/SettingsBanner.tsx
new file mode 100644
index 00000000000..85dfb1d89d8
--- /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..48ddf2fb70e
--- /dev/null
+++ b/src/components/views/settings/shared/SettingsIndent.tsx
@@ -0,0 +1,27 @@
+/*
+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, { 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 243346c0473..926ef5e2323 100644
--- a/src/components/views/settings/shared/SettingsSection.tsx
+++ b/src/components/views/settings/shared/SettingsSection.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
+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.
@@ -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 }) => (
-
diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx
index 4e95220df1f..50afdf91c93 100644
--- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019-2021 The Matrix.org Foundation C.I.C.
+Copyright 2019-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.
@@ -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/hooks/useNotificationSettings.tsx b/src/hooks/useNotificationSettings.tsx
index b4174b4924e..0e5f26b88f0 100644
--- a/src/hooks/useNotificationSettings.tsx
+++ b/src/hooks/useNotificationSettings.tsx
@@ -44,6 +44,7 @@ type UseNotificationSettings = {
};
export function useNotificationSettings(cli: MatrixClient): UseNotificationSettings {
+ const run = useLinearisedPromise();
const supportsIntentionalMentions = useMemo(() => cli.supportsIntentionalMentions(), [cli]);
const pushRules = useRef(null);
@@ -61,21 +62,41 @@ export function useNotificationSettings(cli: MatrixClient): UseNotificationSetti
}, [cli, supportsIntentionalMentions]);
useEffect(() => {
- updatePushRules().catch((err) => console.error(err));
- }, [cli, updatePushRules]);
+ run(updatePushRules).catch((err) => console.error(err));
+ }, [cli, run, updatePushRules]);
const reconcile = useCallback(
(model: NotificationSettings) => {
- if (pushRules.current !== null) {
- setModel(model);
- const changes = reconcileNotificationSettings(pushRules.current, model, supportsIntentionalMentions);
- applyChanges(cli, changes)
- .then(updatePushRules)
- .catch((err) => console.error(err));
- }
+ setModel(model);
+ run(async () => {
+ if (pushRules.current !== null) {
+ const changes = reconcileNotificationSettings(
+ pushRules.current,
+ model,
+ supportsIntentionalMentions,
+ );
+ await applyChanges(cli, changes);
+ await updatePushRules();
+ }
+ }).catch((err) => console.error(err));
},
- [cli, updatePushRules, supportsIntentionalMentions],
+ [run, supportsIntentionalMentions, cli, updatePushRules],
);
return { model, hasPendingChanges, reconcile };
}
+
+function useLinearisedPromise(): (fun: () => Promise) => Promise {
+ const lastPromise = useRef | null>(null);
+
+ return useCallback((fun: () => Promise): Promise => {
+ let next: Promise;
+ if (lastPromise.current === null) {
+ next = fun();
+ } else {
+ next = lastPromise.current.then(fun);
+ }
+ lastPromise.current = next;
+ return next;
+ }, []);
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d9f74d40a56..dff93e43b99 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",
+ "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.": "Introducing a simpler way to change your notification settings. Customize your %(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,33 @@
"%(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 Notifications": "Email Notifications",
+ "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 sent by bots": "Messages sent by bots",
+ "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)s": "Notify when someone mentions using @displayname or %(mxid)s",
+ "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/models/notificationsettings/reconcileNotificationSettings.ts b/src/models/notificationsettings/reconcileNotificationSettings.ts
index 7fdd366e633..510e7ca3af1 100644
--- a/src/models/notificationsettings/reconcileNotificationSettings.ts
+++ b/src/models/notificationsettings/reconcileNotificationSettings.ts
@@ -196,6 +196,11 @@ export function reconcileNotificationSettings(
}
}
+ const mentionActions = NotificationUtils.encodeActions({
+ notify: true,
+ sound: model.sound.mentions,
+ highlight: true,
+ });
const contentRules = pushRules.global.content?.filter((rule) => !rule.rule_id.startsWith(".")) ?? [];
const newKeywords = new Set(model.keywords);
for (const rule of contentRules) {
@@ -204,12 +209,27 @@ export function reconcileNotificationSettings(
rule_id: rule.rule_id,
kind: PushRuleKind.ContentSpecific,
});
- } else if (rule.enabled !== model.mentions.keywords) {
- changes.updated.push({
- rule_id: rule.rule_id,
- kind: PushRuleKind.ContentSpecific,
- enabled: model.mentions.keywords,
- });
+ } else {
+ let changed = false;
+ if (rule.enabled !== model.mentions.keywords) {
+ changed = true;
+ } else if (rule.actions !== undefined) {
+ const originalActions = NotificationUtils.decodeActions(rule.actions);
+ const actions = NotificationUtils.decodeActions(mentionActions);
+ if (originalActions === null || actions === null) {
+ changed = true;
+ } else if (!deepCompare(actions, originalActions)) {
+ changed = true;
+ }
+ }
+ if (changed) {
+ changes.updated.push({
+ rule_id: rule.rule_id,
+ kind: PushRuleKind.ContentSpecific,
+ enabled: model.mentions.keywords,
+ actions: mentionActions,
+ });
+ }
}
newKeywords.delete(rule.pattern!);
}
@@ -220,7 +240,7 @@ export function reconcileNotificationSettings(
default: false,
enabled: model.mentions.keywords,
pattern: keyword,
- actions: StandardActions.ACTION_NOTIFY,
+ actions: mentionActions,
});
}
diff --git a/src/models/notificationsettings/toNotificationSettings.ts b/src/models/notificationsettings/toNotificationSettings.ts
index cfb28718c48..9a3f15453d2 100644
--- a/src/models/notificationsettings/toNotificationSettings.ts
+++ b/src/models/notificationsettings/toNotificationSettings.ts
@@ -37,6 +37,22 @@ function shouldNotify(rules: (IPushRule | null | undefined | false)[]): boolean
return false;
}
+function isMuted(rules: (IPushRule | null | undefined | false)[]): boolean {
+ if (rules.length === 0) {
+ return false;
+ }
+ for (const rule of rules) {
+ if (rule === null || rule === undefined || rule === false || !rule.enabled) {
+ continue;
+ }
+ const actions = NotificationUtils.decodeActions(rule.actions);
+ if (actions !== null && !actions.notify && actions.highlight !== true && actions.sound === undefined) {
+ return true;
+ }
+ }
+ return false;
+}
+
function determineSound(rules: (IPushRule | null | undefined | false)[]): string | undefined {
for (const rule of rules) {
if (rule === null || rule === undefined || rule === false || !rule.enabled) {
@@ -74,7 +90,7 @@ export function toNotificationSettings(
people: determineSound(dmRules),
},
activity: {
- bot_notices: shouldNotify([standardRules.get(RuleId.SuppressNotices)]),
+ bot_notices: !isMuted([standardRules.get(RuleId.SuppressNotices)]),
invite: shouldNotify([standardRules.get(RuleId.InviteToSelf)]),
status_event: shouldNotify([standardRules.get(RuleId.MemberEvent), standardRules.get(RuleId.Tombstone)]),
},
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 7b932eb3b99..5f1cbe8165c 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -94,6 +94,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",
}
@@ -229,6 +230,28 @@ 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(
+ "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.",
+ {
+ brand: SdkConfig.get().brand,
+ },
+ )}
+