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 ( - <> - - 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={} /> );