From 55e9cba5fdbd6a6ffbb63dc68996c42ca40749f5 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:42:53 -0500 Subject: [PATCH] Add reset notification settings action Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com> --- _locales/en/messages.json | 16 +++++ ts/components/Preferences.dom.stories.tsx | 1 + ts/components/Preferences.dom.tsx | 45 ++++++++++++ ts/models/conversations.preload.ts | 21 +++++- ts/state/ducks/conversations.preload.ts | 9 +++ ts/state/smart/Preferences.preload.tsx | 85 ++++++++++++++++++++--- 6 files changed, 165 insertions(+), 12 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c22496b9cb..3b1d15057f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -8232,6 +8232,22 @@ "messageformat": "App badge", "description": "Preferences > Notifications > App Badge Section > Title" }, + "icu:Preferences__Notifications__Reset__title": { + "messageformat": "Reset notification settings to default, including custom settings for your chats.", + "description": "Preferences > Notifications > Reset Section > Text of the row that restores the default notification settings" + }, + "icu:Preferences__Notifications__Reset__button": { + "messageformat": "Reset", + "description": "Preferences > Notifications > Reset Section > Label of the button that restores the default notification settings" + }, + "icu:Preferences__Notifications__Reset__modal--body": { + "messageformat": "Reset all notification settings to default for all chats?", + "description": "Preferences > Notifications > Reset Section > Confirmation Modal > Body. The modal has no visible title, so this is the only text in it." + }, + "icu:Preferences__Notifications__Reset__modal--confirm": { + "messageformat": "Reset", + "description": "Preferences > Notifications > Reset Section > Confirmation Modal > Label of the button that confirms restoring the default notification settings" + }, "icu:PlaintextExport--PreferencesRow--Header": { "messageformat": "Export chat history", "description": "Shown in the Preferences/Chats page, header for a row" diff --git a/ts/components/Preferences.dom.stories.tsx b/ts/components/Preferences.dom.stories.tsx index aa1689b5b0..9255738175 100644 --- a/ts/components/Preferences.dom.stories.tsx +++ b/ts/components/Preferences.dom.stories.tsx @@ -632,6 +632,7 @@ export default { onReactionNotificationsChange: action('onReactionNotificationsChange'), onReadReceiptsChange: action('onReadReceiptsChange'), onRelayCallsChange: action('onRelayCallsChange'), + onResetNotificationSettings: action('onResetNotificationSettings'), onSealedSenderIndicatorsChange: action('onSealedSenderIndicatorsChange'), onSelectedCameraChange: action('onSelectedCameraChange'), onSelectedMicrophoneChange: action('onSelectedMicrophoneChange'), diff --git a/ts/components/Preferences.dom.tsx b/ts/components/Preferences.dom.tsx index 0351af71dc..7bd4087c1c 100644 --- a/ts/components/Preferences.dom.tsx +++ b/ts/components/Preferences.dom.tsx @@ -100,6 +100,7 @@ import type { VisibleRemoteMegaphoneType } from '../types/Megaphone.std.ts'; import { TitlebarDragArea } from './TitlebarDragArea.dom.tsx'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.ts'; import { Emoji } from '../axo/emoji.std.ts'; +import { AxoAlertDialog } from '../axo/AxoAlertDialog.dom.tsx'; import { AxoConfirmDialog } from '../axo/AxoConfirmDialog.dom.tsx'; import moment from 'moment'; import { AxoItem } from '../axo/items/AxoItem.dom.tsx'; @@ -364,6 +365,7 @@ type PropsFunctionType = { onReactionNotificationsChange: CheckboxChangeHandlerType; onReadReceiptsChange: CheckboxChangeHandlerType; onRelayCallsChange: CheckboxChangeHandlerType; + onResetNotificationSettings: () => unknown; onSealedSenderIndicatorsChange: CheckboxChangeHandlerType; onSelectedCameraChange: SelectChangeHandlerType; onSelectedMicrophoneChange: SelectChangeHandlerType; @@ -561,6 +563,7 @@ export function Preferences({ onReactionNotificationsChange, onReadReceiptsChange, onRelayCallsChange, + onResetNotificationSettings, onSealedSenderIndicatorsChange, onSelectedCameraChange, onSelectedMicrophoneChange, @@ -658,6 +661,8 @@ export function Preferences({ const [confirmStoriesOff, setConfirmStoriesOff] = useState(false); const [confirmContentProtection, setConfirmContentProtection] = useState(false); + const [confirmResetNotifications, setConfirmResetNotifications] = + useState(false); const [showSyncFailed, setShowSyncFailed] = useState(false); const [nowSyncing, setNowSyncing] = useState(false); const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] = @@ -1608,6 +1613,46 @@ export function Preferences({ } /> + + + setConfirmResetNotifications(true)} + > + {i18n('icu:Preferences__Notifications__Reset__button')} + + } + /> + + + + {i18n('icu:Preferences__Notifications__Reset__title')} + + + + {i18n('icu:Preferences__Notifications__Reset__modal--body')} + + + + + + {i18n( + 'icu:Preferences__Notifications__Reset__modal--confirm' + )} + + + + + ); content = ( diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index 78d3666ef3..12dc633332 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -32,7 +32,10 @@ import { } from '../util/migrations.preload.ts'; import { drop } from '../util/drop.std.ts'; import { isShallowEqual } from '../util/isShallowEqual.std.ts'; -import type { NotifyWhileMutedKey } from '../util/notifyWhileMuted.std.ts'; +import type { + NotifyWhileMutedFields, + NotifyWhileMutedKey, +} from '../util/notifyWhileMuted.std.ts'; import { NOTIFY_WHILE_MUTED_FIELDS } from '../util/notifyWhileMuted.std.ts'; import { getInitials } from '../util/getInitials.std.ts'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.std.ts'; @@ -6125,6 +6128,22 @@ export class ConversationModel { this.captureChange(attributeName); } + resetNotifyWhileMuted(): void { + const attributeNames = Object.values(NOTIFY_WHILE_MUTED_FIELDS); + if (attributeNames.every(name => this.get(name) === undefined)) { + return; + } + + this.set({ + notifyForCallsIfMuted: undefined, + notifyForMentionsIfMuted: undefined, + notifyForRepliesIfMuted: undefined, + } satisfies Record); + + drop(DataWriter.updateConversation(this.attributes)); + this.captureChange('resetNotifyWhileMuted'); + } + acknowledgeGroupMemberNameCollisions( groupNameCollisions: ReadonlyDeep ): void { diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index de3bc1da27..a36fec0ba3 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -1234,6 +1234,7 @@ export const actions = { repairOldestMessage, replaceAvatar, resetAllChatColors, + resetAllNotifyWhileMuted, copyMessageText, retryDeleteForEveryone, retryMessageSend, @@ -2403,6 +2404,14 @@ function resetAllChatColors(): ThunkAction< }; } +function resetAllNotifyWhileMuted(): NoopActionType { + for (const conversation of window.ConversationController.getAll()) { + conversation.resetNotifyWhileMuted(); + } + + return noopAction('resetAllNotifyWhileMuted'); +} + function kickOffAttachmentDownload( options: Readonly<{ messageId: string }> ): ThunkAction { diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index f3f29b629f..7a5a633e8c 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -129,6 +129,20 @@ import { pinReminderService } from '../../services/pinReminder.preload.ts'; const DEFAULT_NOTIFICATION_SETTING = 'message'; +// Defaults for the settings that "Reset notification settings" restores +const NOTIFICATION_SETTING_DEFAULTS = { + 'notification-setting': DEFAULT_NOTIFICATION_SETTING, + 'call-system-notification': true, + 'reaction-notification': true, + 'notification-draw-attention': false, + 'audio-notification': false, + audioMessage: false, + 'badge-count-muted-conversations': false, + notifyForCallsIfMuted: undefined, + notifyForMentionsIfMuted: undefined, + notifyForRepliesIfMuted: undefined, +} as const satisfies Partial; + function renderUpdateDialog( props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }> ): JSX.Element { @@ -228,8 +242,11 @@ export function SmartPreferences(): JSX.Element | null { setGlobalDefaultConversationColor, toggleNavTabsCollapse, } = useItemsActions(); - const { removeCustomColorOnConversations, resetAllChatColors } = - useConversationsActions(); + const { + removeCustomColorOnConversations, + resetAllChatColors, + resetAllNotifyWhileMuted: resetAllPerChatNotifyWhileMutedSettings, + } = useConversationsActions(); const { startUpdate } = useUpdatesActions(); const { changeLocation } = useNavActions(); const { showToast, openFileInFolder } = useToastActions(); @@ -720,7 +737,7 @@ export function SmartPreferences(): JSX.Element | null { const [hasAudioNotifications, onAudioNotificationsChange] = createItemsAccess( 'audio-notification', - false + NOTIFICATION_SETTING_DEFAULTS['audio-notification'] ); const [hasAutoConvertEmoji, onAutoConvertEmojiChange] = createItemsAccess( 'autoConvertEmoji', @@ -738,16 +755,20 @@ export function SmartPreferences(): JSX.Element | null { ); const [hasCallNotifications, onCallNotificationsChange] = createItemsAccess( 'call-system-notification', - true + NOTIFICATION_SETTING_DEFAULTS['call-system-notification'] ); const [hasIncomingCallNotifications, onIncomingCallNotificationsChange] = createItemsAccess('incoming-call-notification', true); const [hasCallRingtoneNotification, onCallRingtoneNotificationChange] = createItemsAccess('call-ringtone-notification', true); const [hasCountMutedConversations, onCountMutedConversationsChange] = - createItemsAccess('badge-count-muted-conversations', false, () => { - window.Whisper.events.emit('updateUnreadCount'); - }); + createItemsAccess( + 'badge-count-muted-conversations', + NOTIFICATION_SETTING_DEFAULTS['badge-count-muted-conversations'], + () => { + window.Whisper.events.emit('updateUnreadCount'); + } + ); const [hasHideMenuBar, onHideMenuBarChange] = createItemsAccess( 'hide-menu-bar', false, @@ -758,16 +779,22 @@ export function SmartPreferences(): JSX.Element | null { ); const [hasMessageAudio, onMessageAudioChange] = createItemsAccess( 'audioMessage', - false + NOTIFICATION_SETTING_DEFAULTS.audioMessage ); const [hasNotificationAttention, onNotificationAttentionChange] = - createItemsAccess('notification-draw-attention', false); + createItemsAccess( + 'notification-draw-attention', + NOTIFICATION_SETTING_DEFAULTS['notification-draw-attention'] + ); const [hasReactionNotifications, onReactionNotificationsChange] = - createItemsAccess('reaction-notification', true); + createItemsAccess( + 'reaction-notification', + NOTIFICATION_SETTING_DEFAULTS['reaction-notification'] + ); const [notificationContent, onNotificationContentChange] = createItemsAccess( 'notification-setting', - 'message' + NOTIFICATION_SETTING_DEFAULTS['notification-setting'] ); const hasNotifications = notificationContent !== 'off'; const onNotificationsChange = (value: boolean) => { @@ -788,6 +815,41 @@ export function SmartPreferences(): JSX.Element | null { account.captureChange(itemKey); }; + const onResetNotificationSettings = () => { + // Reset global settings + onNotificationContentChange( + NOTIFICATION_SETTING_DEFAULTS['notification-setting'] + ); + onCallNotificationsChange( + NOTIFICATION_SETTING_DEFAULTS['call-system-notification'] + ); + onReactionNotificationsChange( + NOTIFICATION_SETTING_DEFAULTS['reaction-notification'] + ); + onNotificationAttentionChange( + NOTIFICATION_SETTING_DEFAULTS['notification-draw-attention'] + ); + onAudioNotificationsChange( + NOTIFICATION_SETTING_DEFAULTS['audio-notification'] + ); + onMessageAudioChange(NOTIFICATION_SETTING_DEFAULTS.audioMessage); + onCountMutedConversationsChange( + NOTIFICATION_SETTING_DEFAULTS['badge-count-muted-conversations'] + ); + + const account = window.ConversationController.getOurConversationOrThrow(); + for (const itemKey of Object.values(NOTIFY_WHILE_MUTED_FIELDS)) { + if (itemStorage.get(itemKey) === NOTIFICATION_SETTING_DEFAULTS[itemKey]) { + continue; + } + drop(itemStorage.put(itemKey, NOTIFICATION_SETTING_DEFAULTS[itemKey])); + account.captureChange(itemKey); + } + + // Reset per-chat settings + resetAllPerChatNotifyWhileMutedSettings(); + }; + const [hasPinReminders, onPinRemindersChange] = createItemsAccess( 'pinReminders', true, @@ -1082,6 +1144,7 @@ export function SmartPreferences(): JSX.Element | null { onReactionNotificationsChange={onReactionNotificationsChange} onReadReceiptsChange={onReadReceiptsChange} onRelayCallsChange={onRelayCallsChange} + onResetNotificationSettings={onResetNotificationSettings} onSealedSenderIndicatorsChange={onSealedSenderIndicatorsChange} onSelectedCameraChange={onSelectedCameraChange} onSelectedMicrophoneChange={onSelectedMicrophoneChange}