diff --git a/src/renderer/src/components/settings/NotificationsPane.test.tsx b/src/renderer/src/components/settings/NotificationsPane.test.tsx index a085ad3f3..08d43709a 100644 --- a/src/renderer/src/components/settings/NotificationsPane.test.tsx +++ b/src/renderer/src/components/settings/NotificationsPane.test.tsx @@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderToStaticMarkup } from 'react-dom/server' import type { GlobalSettings, NotificationDispatchRequest } from '../../../../shared/types' import { getNotificationSoundOptions } from '@/components/notification-sound-options' -import { NotificationsPane, sendNotificationSettingsTestNotification } from './NotificationsPane' +import { + createNotificationVolumeDraftState, + NotificationsPane, + resolveNotificationVolumeDraftState, + sendNotificationSettingsTestNotification +} from './NotificationsPane' const { toastError, toastMessage, toastSuccess } = vi.hoisted(() => ({ toastError: vi.fn(), @@ -54,6 +59,17 @@ describe('NotificationsPane', () => { ) }) + it('resets the volume draft only when the persisted volume changes', () => { + const state = createNotificationVolumeDraftState(50) + state.draft = 75 + + expect(resolveNotificationVolumeDraftState(state, 50)).toBe(state) + expect(resolveNotificationVolumeDraftState(state, 25)).toEqual({ + sourceVolume: 25, + draft: 25 + }) + }) + it('uses native main-process delivery even when renderer permission is stale denied on macOS', async () => { const notifications = { getPermissionStatus: vi.fn(async () => ({ diff --git a/src/renderer/src/components/settings/NotificationsPane.tsx b/src/renderer/src/components/settings/NotificationsPane.tsx index 1e9330074..f19831314 100644 --- a/src/renderer/src/components/settings/NotificationsPane.tsx +++ b/src/renderer/src/components/settings/NotificationsPane.tsx @@ -42,6 +42,29 @@ type SystemNotificationSettingsCopy = { failureDescription: string } +type NotificationVolumeDraftState = { + sourceVolume: number + draft: number +} + +export function createNotificationVolumeDraftState( + sourceVolume: number +): NotificationVolumeDraftState { + return { + sourceVolume, + draft: sourceVolume + } +} + +export function resolveNotificationVolumeDraftState( + state: NotificationVolumeDraftState, + sourceVolume: number +): NotificationVolumeDraftState { + return state.sourceVolume === sourceVolume + ? state + : createNotificationVolumeDraftState(sourceVolume) +} + function getSystemNotificationSettingsCopy( platform: NodeJS.Platform ): SystemNotificationSettingsCopy | null { @@ -161,15 +184,32 @@ export function NotificationsPane({ }) } - // Why: keep dragging local and persist only on Radix's commit event. That - // avoids IPC on every tick without a debounce timer that can race settings updates. - const [volumeDraft, setVolumeDraft] = useState(notificationSettings.customSoundVolume) - useEffect(() => { notificationSettingsRef.current = notificationSettings - setVolumeDraft(notificationSettings.customSoundVolume) }, [notificationSettings]) + // Why: keep dragging local and persist only on Radix's commit event. That + // avoids IPC on every tick without a debounce timer that can race settings updates. + const [volumeDraftState, setVolumeDraftState] = useState(() => + createNotificationVolumeDraftState(notificationSettings.customSoundVolume) + ) + const resolvedVolumeDraftState = resolveNotificationVolumeDraftState( + volumeDraftState, + notificationSettings.customSoundVolume + ) + if (resolvedVolumeDraftState !== volumeDraftState) { + // Why: external settings writes should update the slider before paint, but + // unrelated notification toggles should not restart an in-progress drag. + setVolumeDraftState(resolvedVolumeDraftState) + } + const volumeDraft = resolvedVolumeDraftState.draft + const setVolumeDraft = (value: number): void => { + setVolumeDraftState((current) => ({ + ...resolveNotificationVolumeDraftState(current, notificationSettings.customSoundVolume), + draft: value + })) + } + const handleVolumeCommit = (value: number): void => { if (notificationSettingsRef.current.customSoundVolume !== value) { void updateNotificationSettings({ customSoundVolume: value })