fix(notifications): show the loudest mode per group and re-enable disabled rules so a server-disabled rule can be toggled
This commit is contained in:
parent
e66d8cf7bf
commit
d985b289c9
4 changed files with 70 additions and 32 deletions
|
|
@ -6,9 +6,11 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||||
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
|
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { SettingsSection } from '../SettingsSection';
|
import { SettingsSection } from '../SettingsSection';
|
||||||
import { getPushRule, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
|
import { getPushRule, PushRuleData } from '../../../hooks/usePushRule';
|
||||||
import {
|
import {
|
||||||
getNotificationModeActions,
|
getNotificationModeActions,
|
||||||
|
getNotificationModeFromRule,
|
||||||
|
loudestNotificationMode,
|
||||||
NotificationMode,
|
NotificationMode,
|
||||||
useNotificationModeActions,
|
useNotificationModeActions,
|
||||||
} from '../../../hooks/useNotificationMode';
|
} from '../../../hooks/useNotificationMode';
|
||||||
|
|
@ -63,33 +65,42 @@ function AllMessagesModeSwitcher({ pushRules }: { pushRules: IPushRules }) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const getModeActions = useNotificationModeActions();
|
const getModeActions = useNotificationModeActions();
|
||||||
|
|
||||||
// Use the DM rule as the representative selected state — all four are kept in
|
// The displayed mode is the loudest across all four rules, with each rule's
|
||||||
// lockstep so any of them reflects the consolidated mode.
|
// mode factoring in whether the server has it disabled (so a disabled rule
|
||||||
const representative = ALL_MESSAGE_RULES[0];
|
// reads OFF rather than echoing stale loud actions).
|
||||||
const defaultRepresentative = getAllMessageDefaultRule(
|
const selectedMode = useMemo(
|
||||||
representative.ruleId,
|
() =>
|
||||||
representative.encrypted,
|
loudestNotificationMode(
|
||||||
representative.oneToOne
|
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(
|
const handleChange = useCallback(
|
||||||
async (mode: NotificationMode) => {
|
async (mode: NotificationMode) => {
|
||||||
const actions = getModeActions(mode);
|
const actions = getModeActions(mode);
|
||||||
// Fan the choice across ALL four underride rules so encrypted rooms never
|
// 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(
|
await Promise.all(
|
||||||
ALL_MESSAGE_RULES.map(({ ruleId, encrypted, oneToOne }) => {
|
ALL_MESSAGE_RULES.flatMap(({ ruleId, encrypted, oneToOne }) => {
|
||||||
const { kind } =
|
const { kind } =
|
||||||
getPushRule(pushRules, ruleId) ?? getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
|
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]
|
[mx, getModeActions, pushRules]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <NotificationModeSwitcher pushRule={pushRule} onChange={handleChange} />;
|
return <NotificationModeSwitcher selectedMode={selectedMode} onChange={handleChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AllMessagesNotifications() {
|
export function AllMessagesNotifications() {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Box, color, config, Icon, Icons, IconSrc, Spinner, toRem } from 'folds';
|
import { Box, color, config, Icon, Icons, IconSrc, Spinner, toRem } from 'folds';
|
||||||
import { IPushRule } from 'matrix-js-sdk';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NotificationMode, useNotificationActionsMode } from '../../../hooks/useNotificationMode';
|
import { NotificationMode } from '../../../hooks/useNotificationMode';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
|
|
||||||
export const useNotificationModes = (): NotificationMode[] =>
|
export const useNotificationModes = (): NotificationMode[] =>
|
||||||
|
|
@ -33,14 +32,18 @@ const useNotificationModeStr = (): Record<NotificationMode, string> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
type NotificationModeSwitcherProps = {
|
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<void>;
|
onChange: (mode: NotificationMode) => Promise<void>;
|
||||||
};
|
};
|
||||||
export function NotificationModeSwitcher({ pushRule, onChange }: NotificationModeSwitcherProps) {
|
export function NotificationModeSwitcher({
|
||||||
|
selectedMode,
|
||||||
|
onChange,
|
||||||
|
}: NotificationModeSwitcherProps) {
|
||||||
const modes = useNotificationModes();
|
const modes = useNotificationModes();
|
||||||
const modeToIcon = useNotificationModeIcon();
|
const modeToIcon = useNotificationModeIcon();
|
||||||
const modeToStr = useNotificationModeStr();
|
const modeToStr = useNotificationModeStr();
|
||||||
const selectedMode = useNotificationActionsMode(pushRule.actions);
|
|
||||||
const [changeState, change] = useAsyncCallback(onChange);
|
const [changeState, change] = useAsyncCallback(onChange);
|
||||||
const changing = changeState.status === AsyncStatus.Loading;
|
const changing = changeState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,11 @@ import { SettingsSection } from '../SettingsSection';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { getMxIdLocalPart } from '../../../utils/matrix';
|
import { getMxIdLocalPart } from '../../../utils/matrix';
|
||||||
import {
|
import { getPushRule, makePushRuleData, PushRuleData } from '../../../hooks/usePushRule';
|
||||||
getPushRule,
|
|
||||||
makePushRuleData,
|
|
||||||
PushRuleData,
|
|
||||||
usePushRule,
|
|
||||||
} from '../../../hooks/usePushRule';
|
|
||||||
import {
|
import {
|
||||||
getNotificationModeActions,
|
getNotificationModeActions,
|
||||||
|
getNotificationModeFromRule,
|
||||||
|
loudestNotificationMode,
|
||||||
NotificationMode,
|
NotificationMode,
|
||||||
NotificationModeOptions,
|
NotificationModeOptions,
|
||||||
useNotificationModeActions,
|
useNotificationModeActions,
|
||||||
|
|
@ -110,25 +107,38 @@ function MentionGroupSwitcher({ pushRules, group }: MentionGroupSwitcherProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
|
const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
|
||||||
|
|
||||||
// First rule in the group is the representative for the displayed mode; all
|
// Displayed mode is the loudest across the group, each rule factoring in
|
||||||
// rules in the group are written in lockstep.
|
// whether the server has it disabled.
|
||||||
const representative = group[0];
|
const selectedMode = useMemo(
|
||||||
const { pushRule } = usePushRule(pushRules, representative.ruleId) ?? representative.getDefault();
|
() =>
|
||||||
|
loudestNotificationMode(
|
||||||
|
group.map(({ ruleId, getDefault }) => {
|
||||||
|
const { pushRule } = getPushRule(pushRules, ruleId) ?? getDefault();
|
||||||
|
return getNotificationModeFromRule(pushRule);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
[pushRules, group]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
async (mode: NotificationMode) => {
|
async (mode: NotificationMode) => {
|
||||||
const actions = getModeActions(mode);
|
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(
|
await Promise.all(
|
||||||
group.map(({ ruleId, getDefault }) => {
|
group.flatMap(({ ruleId, getDefault }) => {
|
||||||
const { kind } = getPushRule(pushRules, 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]
|
[mx, getModeActions, pushRules, group]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <NotificationModeSwitcher pushRule={pushRule} onChange={handleChange} />;
|
return <NotificationModeSwitcher selectedMode={selectedMode} onChange={handleChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpecialMessagesNotifications() {
|
export function SpecialMessagesNotifications() {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export enum NotificationMode {
|
export enum NotificationMode {
|
||||||
|
|
@ -66,3 +66,17 @@ export const useNotificationActionsMode = (actions: PushRuleAction[]): Notificat
|
||||||
|
|
||||||
return mode;
|
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;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue