diff --git a/src/app/features/settings/notifications/AllMessages.tsx b/src/app/features/settings/notifications/AllMessages.tsx
index da80cc80..f12d3ece 100644
--- a/src/app/features/settings/notifications/AllMessages.tsx
+++ b/src/app/features/settings/notifications/AllMessages.tsx
@@ -6,9 +6,11 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
import { SettingTile } from '../../../components/setting-tile';
import { SettingsSection } from '../SettingsSection';
-import { getPushRule, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
+import { getPushRule, PushRuleData } from '../../../hooks/usePushRule';
import {
getNotificationModeActions,
+ getNotificationModeFromRule,
+ loudestNotificationMode,
NotificationMode,
useNotificationModeActions,
} from '../../../hooks/useNotificationMode';
@@ -63,33 +65,42 @@ function AllMessagesModeSwitcher({ pushRules }: { pushRules: IPushRules }) {
const mx = useMatrixClient();
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
+ // The displayed mode is the loudest across all four rules, with each rule's
+ // mode factoring in whether the server has it disabled (so a disabled rule
+ // reads OFF rather than echoing stale loud actions).
+ const selectedMode = useMemo(
+ () =>
+ loudestNotificationMode(
+ ALL_MESSAGE_RULES.map(({ ruleId, encrypted, oneToOne }) => {
+ const { pushRule } =
+ getPushRule(pushRules, ruleId) ?? getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
+ return getNotificationModeFromRule(pushRule);
+ })
+ ),
+ [pushRules]
);
- const { pushRule } = usePushRule(pushRules, representative.ruleId) ?? defaultRepresentative;
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
// Fan the choice across ALL four underride rules so encrypted rooms never
- // silently fall out of sync.
+ // silently fall out of sync. Also (re-)enable each rule: a server-disabled
+ // rule otherwise ignores the new actions and stays silent.
await Promise.all(
- ALL_MESSAGE_RULES.map(({ ruleId, encrypted, oneToOne }) => {
+ ALL_MESSAGE_RULES.flatMap(({ ruleId, encrypted, oneToOne }) => {
const { kind } =
getPushRule(pushRules, ruleId) ?? getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
- return mx.setPushRuleActions('global', kind, ruleId, actions);
+ return [
+ mx.setPushRuleEnabled('global', kind, ruleId, true),
+ mx.setPushRuleActions('global', kind, ruleId, actions),
+ ];
})
);
},
[mx, getModeActions, pushRules]
);
- return ;
+ return ;
}
export function AllMessagesNotifications() {
diff --git a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
index 3b10509a..69ba081e 100644
--- a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
+++ b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
@@ -1,8 +1,7 @@
import { Box, color, config, Icon, Icons, IconSrc, Spinner, toRem } from 'folds';
-import { IPushRule } from 'matrix-js-sdk';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { NotificationMode, useNotificationActionsMode } from '../../../hooks/useNotificationMode';
+import { NotificationMode } from '../../../hooks/useNotificationMode';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
export const useNotificationModes = (): NotificationMode[] =>
@@ -33,14 +32,18 @@ const useNotificationModeStr = (): Record => {
};
type NotificationModeSwitcherProps = {
- pushRule: IPushRule;
+ // The group's consolidated mode (loudest member, factoring server-disabled
+ // rules). Controlled by the parent — the switcher no longer reads a rule.
+ selectedMode: NotificationMode;
onChange: (mode: NotificationMode) => Promise;
};
-export function NotificationModeSwitcher({ pushRule, onChange }: NotificationModeSwitcherProps) {
+export function NotificationModeSwitcher({
+ selectedMode,
+ 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;
diff --git a/src/app/features/settings/notifications/SpecialMessages.tsx b/src/app/features/settings/notifications/SpecialMessages.tsx
index 631e02d7..dcdea0cc 100644
--- a/src/app/features/settings/notifications/SpecialMessages.tsx
+++ b/src/app/features/settings/notifications/SpecialMessages.tsx
@@ -8,14 +8,11 @@ import { SettingsSection } from '../SettingsSection';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
import { getMxIdLocalPart } from '../../../utils/matrix';
-import {
- getPushRule,
- makePushRuleData,
- PushRuleData,
- usePushRule,
-} from '../../../hooks/usePushRule';
+import { getPushRule, makePushRuleData, PushRuleData } from '../../../hooks/usePushRule';
import {
getNotificationModeActions,
+ getNotificationModeFromRule,
+ loudestNotificationMode,
NotificationMode,
NotificationModeOptions,
useNotificationModeActions,
@@ -110,25 +107,38 @@ 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();
+ // Displayed mode is the loudest across the group, each rule factoring in
+ // whether the server has it disabled.
+ const selectedMode = useMemo(
+ () =>
+ loudestNotificationMode(
+ group.map(({ ruleId, getDefault }) => {
+ const { pushRule } = getPushRule(pushRules, ruleId) ?? getDefault();
+ return getNotificationModeFromRule(pushRule);
+ })
+ ),
+ [pushRules, group]
+ );
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
+ // Write actions AND (re-)enable each rule in lockstep — a server-disabled
+ // mention rule otherwise ignores the new actions and stays silent.
await Promise.all(
- group.map(({ ruleId, getDefault }) => {
+ group.flatMap(({ ruleId, getDefault }) => {
const { kind } = getPushRule(pushRules, ruleId) ?? getDefault();
- return mx.setPushRuleActions('global', kind, ruleId, actions);
+ return [
+ mx.setPushRuleEnabled('global', kind, ruleId, true),
+ mx.setPushRuleActions('global', kind, ruleId, actions),
+ ];
})
);
},
[mx, getModeActions, pushRules, group]
);
- return ;
+ return ;
}
export function SpecialMessagesNotifications() {
diff --git a/src/app/hooks/useNotificationMode.ts b/src/app/hooks/useNotificationMode.ts
index df90f647..b2f9adb6 100644
--- a/src/app/hooks/useNotificationMode.ts
+++ b/src/app/hooks/useNotificationMode.ts
@@ -1,4 +1,4 @@
-import { PushRuleAction, PushRuleActionName, TweakName } from 'matrix-js-sdk';
+import { IPushRule, PushRuleAction, PushRuleActionName, TweakName } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
export enum NotificationMode {
@@ -66,3 +66,17 @@ export const useNotificationActionsMode = (actions: PushRuleAction[]): Notificat
return mode;
};
+
+// A rule the server has disabled is effectively OFF regardless of its stored
+// actions — without this the UI reads loud actions on a disabled rule and
+// claims "On" while nothing actually fires.
+export const getNotificationModeFromRule = (pushRule: IPushRule): NotificationMode =>
+ pushRule.enabled === false ? NotificationMode.OFF : getNotificationMode(pushRule.actions);
+
+// The mode shown for a consolidated group of rules is its loudest member, so a
+// partially-muted group doesn't misreport as fully off (mirrors Element).
+export const loudestNotificationMode = (modes: NotificationMode[]): NotificationMode => {
+ if (modes.includes(NotificationMode.NotifyLoud)) return NotificationMode.NotifyLoud;
+ if (modes.includes(NotificationMode.Notify)) return NotificationMode.Notify;
+ return NotificationMode.OFF;
+};