From 8eff9fdfab1ab669492f40df717720d66c533617 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:55:21 +0100 Subject: [PATCH] feat: add notification settings with sound picker and preview --- app/[locale]/login/page.tsx | 2 +- app/[locale]/page.tsx | 5 +- app/[locale]/settings/page.tsx | 7 +- components/settings/calendar-settings.tsx | 34 ----- components/settings/notification-settings.tsx | 115 +++++++++++++++++ hooks/use-calendar-alerts.ts | 8 +- lib/jmap/client.ts | 117 +++++++++++++++++- lib/notification-sound.ts | 58 ++++++--- locales/de/common.json | 37 +++++- locales/en/common.json | 37 +++++- locales/es/common.json | 37 +++++- locales/fr/common.json | 37 +++++- locales/it/common.json | 37 +++++- locales/ja/common.json | 37 +++++- locales/nl/common.json | 37 +++++- locales/pt/common.json | 37 +++++- public/notification/cheerful-527.mp3 | Bin 0 -> 26748 bytes public/notification/involved-notification.mp3 | Bin 0 -> 24658 bytes .../notification-tone-swift-gesture.mp3 | Bin 0 -> 19225 bytes public/notification/relax-message-tone.mp3 | Bin 0 -> 64782 bytes stores/settings-store.ts | 14 +++ 21 files changed, 590 insertions(+), 66 deletions(-) create mode 100644 components/settings/notification-settings.tsx create mode 100644 public/notification/cheerful-527.mp3 create mode 100644 public/notification/involved-notification.mp3 create mode 100644 public/notification/notification-tone-swift-gesture.mp3 create mode 100644 public/notification/relax-message-tone.mp3 diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx index 53123ee7..72ecbc71 100644 --- a/app/[locale]/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -148,7 +148,7 @@ export default function LoginPage() { if (!serverUrl) return; const handleClickOutside = (event: MouseEvent) => { if (suggestionsRef.current && !suggestionsRef.current.contains(event.target as Node) && - inputRef.current && !inputRef.current.contains(event.target as Node)) { + inputRef.current && !inputRef.current.contains(event.target as Node)) { setShowSuggestions(false); } if (themeMenuRef.current && !themeMenuRef.current.contains(event.target as Node)) { diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 0f5852ff..5f4ca016 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -407,7 +407,10 @@ export default function Home() { // Handle new email notifications - play sound useEffect(() => { if (newEmailNotification) { - playNotificationSound(); + const { emailNotificationsEnabled, emailNotificationSound, notificationSoundChoice } = useSettingsStore.getState(); + if (emailNotificationsEnabled && emailNotificationSound) { + playNotificationSound(notificationSoundChoice); + } debug.log('New email received:', newEmailNotification.subject); clearNewEmailNotification(); } diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx index 8ccd3179..d9d81a80 100644 --- a/app/[locale]/settings/page.tsx +++ b/app/[locale]/settings/page.tsx @@ -24,6 +24,7 @@ import { BookUser, KeyRound, PanelLeftClose, + Bell, type LucideIcon, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -44,6 +45,7 @@ import { FilesSettingsComponent } from '@/components/settings/files-settings'; import { ContactsSettings } from '@/components/settings/contacts-settings'; import { SmimeSettings } from '@/components/settings/smime-settings'; import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings'; +import { NotificationSettings } from '@/components/settings/notification-settings'; import { useAuthStore, redirectToLogin } from '@/stores/auth-store'; import { useEmailStore } from '@/stores/email-store'; import { useIsDesktop } from '@/hooks/use-media-query'; @@ -55,7 +57,7 @@ import { ResizeHandle } from '@/components/layout/resize-handle'; import { useConfig } from '@/hooks/use-config'; import { cn } from '@/lib/utils'; -type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'encryption' | 'vacation' | 'calendar' | 'contacts' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'sidebar_apps' | 'advanced'; +type Tab = 'appearance' | 'email' | 'notifications' | 'account' | 'security' | 'identities' | 'encryption' | 'vacation' | 'calendar' | 'contacts' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'sidebar_apps' | 'advanced'; type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system'; interface TabDef { @@ -68,6 +70,7 @@ interface TabDef { const tabIcons: Record = { appearance: Palette, email: Mail, + notifications: Bell, account: User, security: Shield, identities: UserPen, @@ -138,6 +141,7 @@ export default function SettingsPage() { const tabs: TabDef[] = [ { id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'general' }, { id: 'email', label: t('tabs.email'), icon: tabIcons.email, group: 'general' }, + { id: 'notifications', label: t('tabs.notifications'), icon: tabIcons.notifications, group: 'general' }, { id: 'account', label: t('tabs.account'), icon: tabIcons.account, group: 'account' }, ...(stalwartFeaturesEnabled ? [{ id: 'security' as Tab, label: t('tabs.security'), icon: tabIcons.security, group: 'account' as TabGroup }] : []), { id: 'identities', label: t('tabs.identities'), icon: tabIcons.identities, group: 'account' }, @@ -177,6 +181,7 @@ export default function SettingsPage() { <> {activeTab === 'appearance' && } {activeTab === 'email' && } + {activeTab === 'notifications' && } {activeTab === 'account' && } {activeTab === 'security' && } {activeTab === 'identities' && } diff --git a/components/settings/calendar-settings.tsx b/components/settings/calendar-settings.tsx index 96b6882b..9d714565 100644 --- a/components/settings/calendar-settings.tsx +++ b/components/settings/calendar-settings.tsx @@ -16,9 +16,6 @@ export function CalendarSettings() { firstDayOfWeek, showTimeInMonthView, showWeekNumbers, - calendarNotificationsEnabled, - calendarNotificationSound, - calendarInvitationParsingEnabled, enableCalendarTasks, showTasksOnCalendar, updateSetting, @@ -103,37 +100,6 @@ export function CalendarSettings() { )} - - updateSetting('calendarNotificationsEnabled', checked)} - /> - - - - updateSetting('calendarNotificationSound', checked)} - disabled={!calendarNotificationsEnabled} - /> - - - - updateSetting('calendarInvitationParsingEnabled', checked)} - /> - - ); } diff --git a/components/settings/notification-settings.tsx b/components/settings/notification-settings.tsx new file mode 100644 index 00000000..3198e390 --- /dev/null +++ b/components/settings/notification-settings.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { useSettingsStore } from '@/stores/settings-store'; +import { SettingsSection, SettingItem, ToggleSwitch, Select } from './settings-section'; +import { playNotificationSound, NOTIFICATION_SOUNDS } from '@/lib/notification-sound'; +import type { NotificationSoundChoice } from '@/lib/notification-sound'; +import { Button } from '@/components/ui/button'; +import { Volume2 } from 'lucide-react'; + +export function NotificationSettings() { + const t = useTranslations('settings.notifications'); + const { + emailNotificationsEnabled, + emailNotificationSound, + notificationSoundChoice, + calendarNotificationsEnabled, + calendarNotificationSound, + calendarInvitationParsingEnabled, + updateSetting, + } = useSettingsStore(); + + const soundOptions = NOTIFICATION_SOUNDS.map((s) => ({ + value: s.id, + label: t(`sounds.${s.id}`), + })); + + return ( +
+ + +
+ +