diff --git a/src/app/features/settings/notifications/PushRuleEditor.tsx b/src/app/features/settings/notifications/PushRuleEditor.tsx index 68291526c..d4777f5b4 100644 --- a/src/app/features/settings/notifications/PushRuleEditor.tsx +++ b/src/app/features/settings/notifications/PushRuleEditor.tsx @@ -18,6 +18,7 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { getNotificationModeActions, + getNotificationModeOptionsFromActions, NotificationMode, useNotificationModeActions, } from '../../../hooks/useNotificationMode'; @@ -131,7 +132,13 @@ type RuleModeSwitcherProps = { function RuleModeSwitcher({ kind, pushRule }: RuleModeSwitcherProps) { const mx = useMatrixClient(); - const getModeActions = useNotificationModeActions(); + // Preserve any `highlight`/custom sound tweak already on the rule — otherwise + // switching mode here rebuilds actions from scratch and silently drops them. + const options = useMemo( + () => getNotificationModeOptionsFromActions(pushRule.actions), + [pushRule.actions], + ); + const getModeActions = useNotificationModeActions(options); const handleChange = useCallback( async (mode: NotificationMode) => { diff --git a/src/app/hooks/useNotificationMode.test.ts b/src/app/hooks/useNotificationMode.test.ts new file mode 100644 index 000000000..539b8dd61 --- /dev/null +++ b/src/app/hooks/useNotificationMode.test.ts @@ -0,0 +1,42 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { PushRuleActionName, TweakName } from 'matrix-js-sdk'; +import { getNotificationModeOptionsFromActions } from './useNotificationMode'; + +test('getNotificationModeOptionsFromActions: no tweaks -> no highlight, no sound value', () => { + const options = getNotificationModeOptionsFromActions([PushRuleActionName.Notify]); + assert.deepEqual(options, { soundValue: undefined, highlight: false }); +}); + +test('getNotificationModeOptionsFromActions: picks up highlight: true', () => { + const options = getNotificationModeOptionsFromActions([ + PushRuleActionName.Notify, + { set_tweak: TweakName.Highlight, value: true }, + ]); + assert.equal(options.highlight, true); +}); + +test('getNotificationModeOptionsFromActions: highlight: false is not treated as set', () => { + const options = getNotificationModeOptionsFromActions([ + PushRuleActionName.Notify, + { set_tweak: TweakName.Highlight, value: false }, + ]); + assert.equal(options.highlight, false); +}); + +test('getNotificationModeOptionsFromActions: picks up a custom sound value', () => { + const options = getNotificationModeOptionsFromActions([ + PushRuleActionName.Notify, + { set_tweak: TweakName.Sound, value: 'ping.ogg' }, + ]); + assert.equal(options.soundValue, 'ping.ogg'); +}); + +test('getNotificationModeOptionsFromActions: picks up both tweaks together', () => { + const options = getNotificationModeOptionsFromActions([ + PushRuleActionName.Notify, + { set_tweak: TweakName.Sound, value: 'ping.ogg' }, + { set_tweak: TweakName.Highlight, value: true }, + ]); + assert.deepEqual(options, { soundValue: 'ping.ogg', highlight: true }); +}); diff --git a/src/app/hooks/useNotificationMode.ts b/src/app/hooks/useNotificationMode.ts index de9f039d2..02d11a2bc 100644 --- a/src/app/hooks/useNotificationMode.ts +++ b/src/app/hooks/useNotificationMode.ts @@ -49,6 +49,29 @@ export const getNotificationModeActions = ( return actions; }; +// Derive the options that would reproduce an existing rule's tweaks, so a mode +// switch rebuilds actions on top of them instead of silently dropping a +// `highlight` tweak (or a custom sound) that isn't part of the mode itself. +export const getNotificationModeOptionsFromActions = ( + actions: PushRuleAction[], +): NotificationModeOptions => { + const soundTweak = actions.find( + (action) => typeof action === 'object' && action.set_tweak === TweakName.Sound, + ); + const highlightTweak = actions.find( + (action) => typeof action === 'object' && action.set_tweak === TweakName.Highlight, + ); + + return { + soundValue: + soundTweak && typeof soundTweak === 'object' && typeof soundTweak.value === 'string' + ? soundTweak.value + : undefined, + highlight: + !!highlightTweak && typeof highlightTweak === 'object' && highlightTweak.value !== false, + }; +}; + export type GetNotificationModeCallback = (mode: NotificationMode) => PushRuleAction[]; export const useNotificationModeActions = ( options?: NotificationModeOptions,