diff --git a/src/app/features/settings/notifications/AllMessages.tsx b/src/app/features/settings/notifications/AllMessages.tsx
index 4502cfff..da80cc80 100644
--- a/src/app/features/settings/notifications/AllMessages.tsx
+++ b/src/app/features/settings/notifications/AllMessages.tsx
@@ -6,7 +6,7 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
import { SettingTile } from '../../../components/setting-tile';
import { SettingsSection } from '../SettingsSection';
-import { PushRuleData, usePushRule } from '../../../hooks/usePushRule';
+import { getPushRule, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
import {
getNotificationModeActions,
NotificationMode,
@@ -14,6 +14,22 @@ import {
} from '../../../hooks/useNotificationMode';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
+// The four global underride rules that govern "all messages" notifications.
+// They are written in lockstep behind one consolidated control so a single
+// "Default for new chats" choice fans out to DM / encrypted DM / rooms /
+// encrypted rooms. NEVER drop a ruleId here — a missing one silently stops
+// notifying (e.g. encrypted rooms) for that surface.
+const ALL_MESSAGE_RULES: {
+ ruleId: RuleId.DM | RuleId.EncryptedDM | RuleId.Message | RuleId.EncryptedMessage;
+ encrypted: boolean;
+ oneToOne: boolean;
+}[] = [
+ { ruleId: RuleId.DM, encrypted: false, oneToOne: true },
+ { ruleId: RuleId.EncryptedDM, encrypted: true, oneToOne: true },
+ { ruleId: RuleId.Message, encrypted: false, oneToOne: false },
+ { ruleId: RuleId.EncryptedMessage, encrypted: true, oneToOne: false },
+];
+
const getAllMessageDefaultRule = (
ruleId: RuleId,
encrypted: boolean,
@@ -43,29 +59,34 @@ const getAllMessageDefaultRule = (
};
};
-type PushRulesProps = {
- ruleId: RuleId.DM | RuleId.EncryptedDM | RuleId.Message | RuleId.EncryptedMessage;
- pushRules: IPushRules;
- encrypted?: boolean;
- oneToOne?: boolean;
-};
-function AllMessagesModeSwitcher({
- ruleId,
- pushRules,
- encrypted = false,
- oneToOne = false,
-}: PushRulesProps) {
+function AllMessagesModeSwitcher({ pushRules }: { pushRules: IPushRules }) {
const mx = useMatrixClient();
- const defaultPushRuleData = getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
- const { kind, pushRule } = usePushRule(pushRules, ruleId) ?? defaultPushRuleData;
const getModeActions = useNotificationModeActions();
+ // Use the DM rule as the representative selected state — all four are kept in
+ // lockstep so any of them reflects the consolidated mode.
+ const representative = ALL_MESSAGE_RULES[0];
+ const defaultRepresentative = getAllMessageDefaultRule(
+ representative.ruleId,
+ representative.encrypted,
+ representative.oneToOne
+ );
+ const { pushRule } = usePushRule(pushRules, representative.ruleId) ?? defaultRepresentative;
+
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
- await mx.setPushRuleActions('global', kind, ruleId, actions);
+ // Fan the choice across ALL four underride rules so encrypted rooms never
+ // silently fall out of sync.
+ await Promise.all(
+ ALL_MESSAGE_RULES.map(({ ruleId, encrypted, oneToOne }) => {
+ const { kind } =
+ getPushRule(pushRules, ruleId) ?? getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
+ return mx.setPushRuleActions('global', kind, ruleId, actions);
+ })
+ );
},
- [mx, getModeActions, kind, ruleId]
+ [mx, getModeActions, pushRules]
);
return ;
@@ -82,33 +103,9 @@ export function AllMessagesNotifications() {
return (
}
- />
-
- }
- />
- }
- />
-
- }
+ title={t('Settings.default_for_new_chats')}
+ description={t('Settings.default_for_new_chats_desc')}
+ after={}
/>
);
diff --git a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
index 5987ff62..3b10509a 100644
--- a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
+++ b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
@@ -1,27 +1,25 @@
-import {
- Box,
- Button,
- config,
- Icon,
- Icons,
- Menu,
- MenuItem,
- PopOut,
- RectCords,
- Spinner,
- Text,
-} from 'folds';
+import { Box, color, config, Icon, Icons, IconSrc, Spinner, toRem } from 'folds';
import { IPushRule } from 'matrix-js-sdk';
-import React, { MouseEventHandler, useMemo, useState } from 'react';
+import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import FocusTrap from 'focus-trap-react';
import { NotificationMode, useNotificationActionsMode } from '../../../hooks/useNotificationMode';
-import { stopPropagation } from '../../../utils/keyboard';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
export const useNotificationModes = (): NotificationMode[] =>
useMemo(() => [NotificationMode.NotifyLoud, NotificationMode.Notify, NotificationMode.OFF], []);
+// Mirror the per-room bell language (getRoomNotificationModeIcon): loud = ring,
+// silent = ping, off = mute.
+const useNotificationModeIcon = (): Record =>
+ useMemo(
+ () => ({
+ [NotificationMode.NotifyLoud]: Icons.BellRing,
+ [NotificationMode.Notify]: Icons.BellPing,
+ [NotificationMode.OFF]: Icons.BellMute,
+ }),
+ []
+ );
+
const useNotificationModeStr = (): Record => {
const { t } = useTranslation();
return useMemo(
@@ -40,81 +38,63 @@ type NotificationModeSwitcherProps = {
};
export function NotificationModeSwitcher({ pushRule, onChange }: NotificationModeSwitcherProps) {
const modes = useNotificationModes();
+ const modeToIcon = useNotificationModeIcon();
const modeToStr = useNotificationModeStr();
const selectedMode = useNotificationActionsMode(pushRule.actions);
const [changeState, change] = useAsyncCallback(onChange);
const changing = changeState.status === AsyncStatus.Loading;
- const [menuCords, setMenuCords] = useState();
-
- const handleMenu: MouseEventHandler = (evt) => {
- setMenuCords(evt.currentTarget.getBoundingClientRect());
- };
-
- const handleSelect = (mode: NotificationMode) => {
- setMenuCords(undefined);
- change(mode);
- };
-
return (
- <>
-
- ) : (
-
- )
- }
- onClick={handleMenu}
- disabled={changing}
- >
- {modeToStr[selectedMode]}
-
- setMenuCords(undefined),
- clickOutsideDeactivates: true,
- isKeyForward: (evt: KeyboardEvent) =>
- evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
- isKeyBackward: (evt: KeyboardEvent) =>
- evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
- escapeDeactivates: stopPropagation,
+
+ {modes.map((mode) => {
+ const selected = mode === selectedMode;
+ return (
+
+ );
+ })}
+ {changing && (
+
+
+
+ )}
+
);
}
diff --git a/src/app/features/settings/notifications/SpecialMessages.tsx b/src/app/features/settings/notifications/SpecialMessages.tsx
index ae2fb3cf..631e02d7 100644
--- a/src/app/features/settings/notifications/SpecialMessages.tsx
+++ b/src/app/features/settings/notifications/SpecialMessages.tsx
@@ -7,9 +7,13 @@ import { SettingTile } from '../../../components/setting-tile';
import { SettingsSection } from '../SettingsSection';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
-import { useUserProfile } from '../../../hooks/useUserProfile';
import { getMxIdLocalPart } from '../../../utils/matrix';
-import { makePushRuleData, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
+import {
+ getPushRule,
+ makePushRuleData,
+ PushRuleData,
+ usePushRule,
+} from '../../../hooks/usePushRule';
import {
getNotificationModeActions,
NotificationMode,
@@ -89,23 +93,39 @@ const DefaultAtRoomNotification = makePushRuleData(
]
);
-type PushRulesProps = {
+type RuleGroupEntry = {
ruleId: RuleId;
- pushRules: IPushRules;
- defaultPushRuleData: PushRuleData;
+ getDefault: () => PushRuleData;
};
-function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRulesProps) {
- const mx = useMatrixClient();
- const { kind, pushRule } = usePushRule(pushRules, ruleId) ?? defaultPushRuleData;
+// One consolidated "Mentions" control fans across the three personal-mention
+// rules; one "@room" control fans across the two room-wide announcement rules.
+// Every ruleId is still written behind the grouped control — a dropped ruleId
+// silently breaks mention notifications.
+type MentionGroupSwitcherProps = {
+ pushRules: IPushRules;
+ group: RuleGroupEntry[];
+};
+function MentionGroupSwitcher({ pushRules, group }: MentionGroupSwitcherProps) {
+ const mx = useMatrixClient();
const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
+ // First rule in the group is the representative for the displayed mode; all
+ // rules in the group are written in lockstep.
+ const representative = group[0];
+ const { pushRule } = usePushRule(pushRules, representative.ruleId) ?? representative.getDefault();
+
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
- await mx.setPushRuleActions('global', kind, ruleId, actions);
+ await Promise.all(
+ group.map(({ ruleId, getDefault }) => {
+ const { kind } = getPushRule(pushRules, ruleId) ?? getDefault();
+ return mx.setPushRuleActions('global', kind, ruleId, actions);
+ })
+ );
},
- [mx, getModeActions, kind, ruleId]
+ [mx, getModeActions, pushRules, group]
);
return ;
@@ -114,68 +134,42 @@ function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRul
export function SpecialMessagesNotifications() {
const { t } = useTranslation();
const userId = useAuthedUserId();
- const { displayName } = useUserProfile(userId);
const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
const pushRules = useMemo(
() => pushRulesEvt?.getContent() ?? { global: {} },
[pushRulesEvt]
);
+ const username = getMxIdLocalPart(userId) ?? userId;
+
+ const mentionGroup = useMemo(
+ () => [
+ { ruleId: RuleId.IsUserMention, getDefault: () => getDefaultIsUserMention(userId) },
+ { ruleId: RuleId.ContainsDisplayName, getDefault: () => DefaultContainsDisplayName },
+ { ruleId: RuleId.ContainsUserName, getDefault: () => getDefaultContainsUsername(username) },
+ ],
+ [userId, username]
+ );
+
+ const roomGroup = useMemo(
+ () => [
+ { ruleId: RuleId.IsRoomMention, getDefault: () => DefaultIsRoomMention },
+ { ruleId: RuleId.AtRoomNotification, getDefault: () => DefaultAtRoomNotification },
+ ],
+ []
+ );
+
return (
- }
+ title={t('Settings.notify_on_mention')}
+ description={t('Settings.notify_on_mention_desc')}
+ after={}
/>
- }
- />
-
- }
- />
-
- }
- />
-
- }
+ title={t('Settings.room_announcements')}
+ description={t('Settings.room_announcements_desc')}
+ after={}
/>
);