feat: add notification settings with sound picker and preview
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<Tab, LucideIcon> = {
|
||||
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' && <AppearanceSettings />}
|
||||
{activeTab === 'email' && <EmailSettings />}
|
||||
{activeTab === 'notifications' && <NotificationSettings />}
|
||||
{activeTab === 'account' && <AccountSettings />}
|
||||
{activeTab === 'security' && <AccountSecuritySettings />}
|
||||
{activeTab === 'identities' && <IdentitySettings />}
|
||||
|
||||
@@ -16,9 +16,6 @@ export function CalendarSettings() {
|
||||
firstDayOfWeek,
|
||||
showTimeInMonthView,
|
||||
showWeekNumbers,
|
||||
calendarNotificationsEnabled,
|
||||
calendarNotificationSound,
|
||||
calendarInvitationParsingEnabled,
|
||||
enableCalendarTasks,
|
||||
showTasksOnCalendar,
|
||||
updateSetting,
|
||||
@@ -103,37 +100,6 @@ export function CalendarSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem
|
||||
label={t('notifications_enabled')}
|
||||
description={t('notifications_enabled_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationsEnabled}
|
||||
onChange={(checked) => updateSetting('calendarNotificationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('notification_sound')}
|
||||
description={t('notification_sound_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationSound}
|
||||
onChange={(checked) => updateSetting('calendarNotificationSound', checked)}
|
||||
disabled={!calendarNotificationsEnabled}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('invitation_parsing')}
|
||||
description={t('invitation_parsing_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarInvitationParsingEnabled}
|
||||
onChange={(checked) => updateSetting('calendarInvitationParsingEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-8">
|
||||
<SettingsSection title={t('sound_selection.title')} description={t('sound_selection.description')}>
|
||||
<SettingItem
|
||||
label={t('sound_selection.choose')}
|
||||
description={t('sound_selection.choose_desc')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => playNotificationSound(notificationSoundChoice)}
|
||||
title={t('test_sound')}
|
||||
>
|
||||
<Volume2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Select
|
||||
value={notificationSoundChoice}
|
||||
onChange={(value) => {
|
||||
const choice = value as NotificationSoundChoice;
|
||||
updateSetting('notificationSoundChoice', choice);
|
||||
playNotificationSound(choice);
|
||||
}}
|
||||
options={soundOptions}
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('email.title')} description={t('email.description')}>
|
||||
<SettingItem
|
||||
label={t('email.enabled')}
|
||||
description={t('email.enabled_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={emailNotificationsEnabled}
|
||||
onChange={(checked) => updateSetting('emailNotificationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('email.sound')}
|
||||
description={t('email.sound_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={emailNotificationSound}
|
||||
onChange={(checked) => updateSetting('emailNotificationSound', checked)}
|
||||
disabled={!emailNotificationsEnabled}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('calendar.title')} description={t('calendar.description')}>
|
||||
<SettingItem
|
||||
label={t('calendar.enabled')}
|
||||
description={t('calendar.enabled_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationsEnabled}
|
||||
onChange={(checked) => updateSetting('calendarNotificationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('calendar.sound')}
|
||||
description={t('calendar.sound_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationSound}
|
||||
onChange={(checked) => updateSetting('calendarNotificationSound', checked)}
|
||||
disabled={!calendarNotificationsEnabled}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('calendar.invitation_parsing')}
|
||||
description={t('calendar.invitation_parsing_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarInvitationParsingEnabled}
|
||||
onChange={(checked) => updateSetting('calendarInvitationParsingEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const PROACTIVE_THROTTLE_MS = CHECK_INTERVAL_MS * 5;
|
||||
export function useCalendarAlerts() {
|
||||
const { isAuthenticated, client } = useAuthStore();
|
||||
const { events, calendars, supportsCalendar } = useCalendarStore();
|
||||
const { calendarNotificationsEnabled, calendarNotificationSound, enableCalendarTasks } = useSettingsStore();
|
||||
const { calendarNotificationsEnabled, calendarNotificationSound, enableCalendarTasks, notificationSoundChoice } = useSettingsStore();
|
||||
const { tasks: storeTasks } = useTaskStore();
|
||||
const { acknowledgedAlerts, acknowledgeAlert, cleanupStaleAlerts } = useCalendarNotificationStore();
|
||||
const addToast = useToastStore((s) => s.addToast);
|
||||
@@ -47,7 +47,7 @@ export function useCalendarAlerts() {
|
||||
acknowledgeAlert(key, alert.fireTimeMs);
|
||||
|
||||
if (calendarNotificationSound) {
|
||||
playNotificationSound();
|
||||
playNotificationSound(notificationSoundChoice);
|
||||
}
|
||||
|
||||
const diffMs = new Date(alert.event.utcStart || alert.event.start).getTime() - now;
|
||||
@@ -83,7 +83,7 @@ export function useCalendarAlerts() {
|
||||
acknowledgeAlert(key, taskAlert.fireTimeMs);
|
||||
|
||||
if (calendarNotificationSound) {
|
||||
playNotificationSound();
|
||||
playNotificationSound(notificationSoundChoice);
|
||||
}
|
||||
|
||||
const taskMsg = taskAlert.calendarName
|
||||
@@ -105,7 +105,7 @@ export function useCalendarAlerts() {
|
||||
// Silently ignore alert evaluation errors
|
||||
}
|
||||
}, [
|
||||
calendarNotificationsEnabled, calendarNotificationSound,
|
||||
calendarNotificationsEnabled, calendarNotificationSound, notificationSoundChoice,
|
||||
isAuthenticated, events, calendars, acknowledgedAlerts,
|
||||
acknowledgeAlert, addToast, t, locale,
|
||||
]);
|
||||
|
||||
+114
-3
@@ -3479,6 +3479,8 @@ export class JMAPClient implements IJMAPClient {
|
||||
|
||||
private pollingInterval: NodeJS.Timeout | null = null;
|
||||
private pollingStates: { [key: string]: string } = {};
|
||||
private sseAbortController: AbortController | null = null;
|
||||
private sseReconnectTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
private static readonly STATE_TYPE_MAP: Record<string, string> = {
|
||||
'Mailbox/get': 'Mailbox',
|
||||
@@ -3488,13 +3490,114 @@ export class JMAPClient implements IJMAPClient {
|
||||
'SieveScript/get': 'SieveScript',
|
||||
};
|
||||
|
||||
// Polling-based push since EventSource cannot send Authorization headers
|
||||
private static readonly POLLING_INTERVAL = 3_000;
|
||||
private static readonly SSE_RECONNECT_DELAY = 3_000;
|
||||
|
||||
setupPushNotifications(): boolean {
|
||||
const eventSourceUrl = this.getEventSourceUrl();
|
||||
if (eventSourceUrl) {
|
||||
this.connectSSE(eventSourceUrl);
|
||||
} else {
|
||||
this.startPollingFallback();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private connectSSE(templateUrl: string): void {
|
||||
const url = templateUrl
|
||||
.replace('{types}', '*')
|
||||
.replace('{closeafter}', 'no')
|
||||
.replace('{ping}', '30');
|
||||
|
||||
this.sseAbortController = new AbortController();
|
||||
|
||||
fetch(url, {
|
||||
headers: { 'Authorization': this.authHeader, 'Accept': 'text/event-stream' },
|
||||
signal: this.sseAbortController.signal,
|
||||
}).then(response => {
|
||||
if (!response.ok || !response.body) {
|
||||
this.fallbackToPolling();
|
||||
return;
|
||||
}
|
||||
this.readSSEStream(response.body);
|
||||
}).catch(() => {
|
||||
this.fallbackToPolling();
|
||||
});
|
||||
}
|
||||
|
||||
private async readSSEStream(body: ReadableStream<Uint8Array>): Promise<void> {
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const parts = buffer.split('\n\n');
|
||||
buffer = parts.pop() || '';
|
||||
|
||||
for (const part of parts) {
|
||||
this.processSSEEvent(part);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') return;
|
||||
}
|
||||
|
||||
// Stream ended — reconnect unless we were intentionally closed
|
||||
if (this.sseAbortController && !this.sseAbortController.signal.aborted) {
|
||||
this.scheduleSSEReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private processSSEEvent(raw: string): void {
|
||||
let eventType = 'message';
|
||||
let dataLines: string[] = [];
|
||||
|
||||
for (const line of raw.split('\n')) {
|
||||
if (line.startsWith('event:')) {
|
||||
eventType = line.slice(6).trim();
|
||||
} else if (line.startsWith('data:')) {
|
||||
dataLines.push(line.slice(5).trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (eventType === 'state' && dataLines.length > 0) {
|
||||
try {
|
||||
const change = JSON.parse(dataLines.join('\n')) as StateChange;
|
||||
this.stateChangeCallback?.(change);
|
||||
} catch {
|
||||
// Malformed SSE data — ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleSSEReconnect(): void {
|
||||
const eventSourceUrl = this.getEventSourceUrl();
|
||||
if (!eventSourceUrl) {
|
||||
this.fallbackToPolling();
|
||||
return;
|
||||
}
|
||||
this.sseReconnectTimeout = setTimeout(() => {
|
||||
this.connectSSE(eventSourceUrl);
|
||||
}, JMAPClient.SSE_RECONNECT_DELAY);
|
||||
}
|
||||
|
||||
private fallbackToPolling(): void {
|
||||
this.sseAbortController = null;
|
||||
if (!this.pollingInterval) {
|
||||
this.startPollingFallback();
|
||||
}
|
||||
}
|
||||
|
||||
private startPollingFallback(): void {
|
||||
this.fetchCurrentStates();
|
||||
this.pollingInterval = setInterval(() => {
|
||||
this.checkForStateChanges();
|
||||
}, 15_000);
|
||||
return true;
|
||||
}, JMAPClient.POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
private buildStatePollingRequest(): { using: string[]; methodCalls: JMAPMethodCall[] } {
|
||||
@@ -3588,6 +3691,14 @@ export class JMAPClient implements IJMAPClient {
|
||||
clearInterval(this.pollingInterval);
|
||||
this.pollingInterval = null;
|
||||
}
|
||||
if (this.sseAbortController) {
|
||||
this.sseAbortController.abort();
|
||||
this.sseAbortController = null;
|
||||
}
|
||||
if (this.sseReconnectTimeout) {
|
||||
clearTimeout(this.sseReconnectTimeout);
|
||||
this.sseReconnectTimeout = null;
|
||||
}
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
|
||||
+44
-14
@@ -1,21 +1,51 @@
|
||||
import { debug } from '@/lib/debug';
|
||||
|
||||
export function playNotificationSound() {
|
||||
export type NotificationSoundChoice = 'default' | 'cheerful' | 'involved' | 'swift' | 'relax';
|
||||
|
||||
export const NOTIFICATION_SOUNDS: { id: NotificationSoundChoice; file?: string }[] = [
|
||||
{ id: 'default' },
|
||||
{ id: 'cheerful', file: '/notification/cheerful-527.mp3' },
|
||||
{ id: 'involved', file: '/notification/involved-notification.mp3' },
|
||||
{ id: 'swift', file: '/notification/notification-tone-swift-gesture.mp3' },
|
||||
{ id: 'relax', file: '/notification/relax-message-tone.mp3' },
|
||||
];
|
||||
|
||||
function playBeep() {
|
||||
const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'sine';
|
||||
gainNode.gain.value = 0.1;
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioContext.currentTime + 0.15);
|
||||
oscillator.onended = () => audioContext.close();
|
||||
}
|
||||
|
||||
function playFile(file: string) {
|
||||
const audio = new Audio(file);
|
||||
audio.volume = 0.3;
|
||||
audio.play().catch((e) => {
|
||||
debug.log('Could not play audio file, falling back to beep:', e);
|
||||
playBeep();
|
||||
});
|
||||
}
|
||||
|
||||
export function playNotificationSound(sound?: NotificationSoundChoice) {
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
const choice = sound ?? 'default';
|
||||
const entry = NOTIFICATION_SOUNDS.find((s) => s.id === choice);
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'sine';
|
||||
gainNode.gain.value = 0.1;
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioContext.currentTime + 0.15);
|
||||
oscillator.onended = () => audioContext.close();
|
||||
if (entry?.file) {
|
||||
playFile(entry.file);
|
||||
} else {
|
||||
playBeep();
|
||||
}
|
||||
} catch (e) {
|
||||
debug.log('Could not play notification sound:', e);
|
||||
}
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "Verschlüsselung",
|
||||
"files": "Dateien",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "Sidebar-Apps"
|
||||
"sidebar_apps": "Sidebar-Apps",
|
||||
"notifications": "Benachrichtigungen"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Allgemein",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Schlüsselwort bei bestehenden E-Mails aktualisieren…",
|
||||
"migration_error": "Schlüsselwort konnte bei bestehenden E-Mails nicht aktualisiert werden"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Benachrichtigungston testen",
|
||||
"sounds": {
|
||||
"default": "Standard (Piepton)",
|
||||
"cheerful": "Fröhlich",
|
||||
"involved": "Aufwendig",
|
||||
"swift": "Schnelle Geste",
|
||||
"relax": "Entspannt"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Benachrichtigungston",
|
||||
"description": "Wählen Sie den Ton für Benachrichtigungen",
|
||||
"choose": "Ton",
|
||||
"choose_desc": "Wählen Sie einen Benachrichtigungston und klicken Sie auf das Lautsprechersymbol zur Vorschau"
|
||||
},
|
||||
"email": {
|
||||
"title": "E-Mail-Benachrichtigungen",
|
||||
"description": "Benachrichtigungen für eingehende E-Mails konfigurieren",
|
||||
"enabled": "E-Mail-Benachrichtigungen",
|
||||
"enabled_desc": "Benachrichtigungen anzeigen, wenn neue E-Mails eintreffen",
|
||||
"sound": "Benachrichtigungston",
|
||||
"sound_desc": "Einen Ton abspielen, wenn neue E-Mails eintreffen"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Kalender-Benachrichtigungen",
|
||||
"description": "Benachrichtigungen für Kalendertermine konfigurieren",
|
||||
"enabled": "Terminbenachrichtigungen",
|
||||
"enabled_desc": "Erinnerungen für bevorstehende Kalendertermine anzeigen",
|
||||
"sound": "Benachrichtigungston",
|
||||
"sound_desc": "Einen Ton für Kalendererinnerungen abspielen",
|
||||
"invitation_parsing": "E-Mail-Einladungen erkennen",
|
||||
"invitation_parsing_desc": "Kalendereinladungen in E-Mail-Anhängen erkennen und Kalenderaktionen anzeigen"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Sprache & Region",
|
||||
"description": "Konfigurieren Sie Sprach- und Regionaleinstellungen",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"files": "Files",
|
||||
"contacts": "Contacts",
|
||||
"encryption": "Encryption",
|
||||
"sidebar_apps": "Sidebar Apps"
|
||||
"sidebar_apps": "Sidebar Apps",
|
||||
"notifications": "Notifications"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "General",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Updating keyword on existing emails…",
|
||||
"migration_error": "Failed to update keyword on existing emails"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Test notification sound",
|
||||
"sounds": {
|
||||
"default": "Default (Beep)",
|
||||
"cheerful": "Cheerful",
|
||||
"involved": "Involved",
|
||||
"swift": "Swift Gesture",
|
||||
"relax": "Relax"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Notification Sound",
|
||||
"description": "Choose which sound to play for notifications",
|
||||
"choose": "Sound",
|
||||
"choose_desc": "Select a notification tone and click the speaker icon to preview it"
|
||||
},
|
||||
"email": {
|
||||
"title": "Email Notifications",
|
||||
"description": "Configure notifications for incoming emails",
|
||||
"enabled": "Email notifications",
|
||||
"enabled_desc": "Show notifications when new emails arrive",
|
||||
"sound": "Notification sound",
|
||||
"sound_desc": "Play an audio alert when new emails arrive"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Calendar Notifications",
|
||||
"description": "Configure notifications for calendar events",
|
||||
"enabled": "Event notifications",
|
||||
"enabled_desc": "Show alerts for upcoming calendar events",
|
||||
"sound": "Notification sound",
|
||||
"sound_desc": "Play an audio alert for calendar reminders",
|
||||
"invitation_parsing": "Parse email invitations",
|
||||
"invitation_parsing_desc": "Detect calendar invitations in email attachments and show calendar actions"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Language & Region",
|
||||
"description": "Configure language and regional preferences",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "Cifrado",
|
||||
"files": "Archivos",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "Apps de barra lateral"
|
||||
"sidebar_apps": "Apps de barra lateral",
|
||||
"notifications": "Notificaciones"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "General",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Actualizando etiqueta en correos existentes…",
|
||||
"migration_error": "Error al actualizar la etiqueta en correos existentes"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Probar sonido de notificación",
|
||||
"sounds": {
|
||||
"default": "Predeterminado (Pitido)",
|
||||
"cheerful": "Alegre",
|
||||
"involved": "Elaborado",
|
||||
"swift": "Gesto rápido",
|
||||
"relax": "Relajado"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Sonido de notificación",
|
||||
"description": "Elige qué sonido reproducir para las notificaciones",
|
||||
"choose": "Sonido",
|
||||
"choose_desc": "Selecciona un tono de notificación y haz clic en el icono del altavoz para previsualizarlo"
|
||||
},
|
||||
"email": {
|
||||
"title": "Notificaciones de correo",
|
||||
"description": "Configurar notificaciones para correos entrantes",
|
||||
"enabled": "Notificaciones de correo",
|
||||
"enabled_desc": "Mostrar notificaciones cuando lleguen nuevos correos",
|
||||
"sound": "Sonido de notificación",
|
||||
"sound_desc": "Reproducir un sonido cuando lleguen nuevos correos"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Notificaciones de calendario",
|
||||
"description": "Configurar notificaciones para eventos del calendario",
|
||||
"enabled": "Notificaciones de eventos",
|
||||
"enabled_desc": "Mostrar alertas para próximos eventos del calendario",
|
||||
"sound": "Sonido de notificación",
|
||||
"sound_desc": "Reproducir un sonido para recordatorios del calendario",
|
||||
"invitation_parsing": "Analizar invitaciones por correo",
|
||||
"invitation_parsing_desc": "Detectar invitaciones de calendario en archivos adjuntos y mostrar acciones de calendario"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Idioma y Región",
|
||||
"description": "Configure las preferencias de idioma y región",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "Chiffrement",
|
||||
"files": "Fichiers",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "Apps de la barre latérale"
|
||||
"sidebar_apps": "Apps de la barre latérale",
|
||||
"notifications": "Notifications"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Général",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Mise à jour du mot-clé sur les e-mails existants…",
|
||||
"migration_error": "Échec de la mise à jour du mot-clé sur les e-mails existants"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Tester le son de notification",
|
||||
"sounds": {
|
||||
"default": "Par défaut (Bip)",
|
||||
"cheerful": "Joyeux",
|
||||
"involved": "Élaboré",
|
||||
"swift": "Geste rapide",
|
||||
"relax": "Détente"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Son de notification",
|
||||
"description": "Choisissez le son à jouer pour les notifications",
|
||||
"choose": "Son",
|
||||
"choose_desc": "Sélectionnez une sonnerie et cliquez sur l'icône du haut-parleur pour l'écouter"
|
||||
},
|
||||
"email": {
|
||||
"title": "Notifications par e-mail",
|
||||
"description": "Configurer les notifications pour les e-mails entrants",
|
||||
"enabled": "Notifications par e-mail",
|
||||
"enabled_desc": "Afficher des notifications à l'arrivée de nouveaux e-mails",
|
||||
"sound": "Son de notification",
|
||||
"sound_desc": "Jouer un son à l'arrivée de nouveaux e-mails"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Notifications de calendrier",
|
||||
"description": "Configurer les notifications pour les événements du calendrier",
|
||||
"enabled": "Notifications d'événements",
|
||||
"enabled_desc": "Afficher des alertes pour les événements à venir",
|
||||
"sound": "Son de notification",
|
||||
"sound_desc": "Jouer un son pour les rappels de calendrier",
|
||||
"invitation_parsing": "Analyser les invitations par e-mail",
|
||||
"invitation_parsing_desc": "Détecter les invitations de calendrier dans les pièces jointes et afficher les actions de calendrier"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Langue et région",
|
||||
"description": "Configurez vos préférences linguistiques et régionales",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "Cifratura",
|
||||
"files": "File",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "App nella barra laterale"
|
||||
"sidebar_apps": "App nella barra laterale",
|
||||
"notifications": "Notifiche"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Generale",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Aggiornamento parola chiave sulle email esistenti…",
|
||||
"migration_error": "Impossibile aggiornare la parola chiave sulle email esistenti"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Testa il suono di notifica",
|
||||
"sounds": {
|
||||
"default": "Predefinito (Bip)",
|
||||
"cheerful": "Allegro",
|
||||
"involved": "Elaborato",
|
||||
"swift": "Gesto veloce",
|
||||
"relax": "Rilassante"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Suono di notifica",
|
||||
"description": "Scegli quale suono riprodurre per le notifiche",
|
||||
"choose": "Suono",
|
||||
"choose_desc": "Seleziona un tono di notifica e clicca sull'icona dell'altoparlante per l'anteprima"
|
||||
},
|
||||
"email": {
|
||||
"title": "Notifiche e-mail",
|
||||
"description": "Configura le notifiche per le e-mail in arrivo",
|
||||
"enabled": "Notifiche e-mail",
|
||||
"enabled_desc": "Mostra notifiche all'arrivo di nuove e-mail",
|
||||
"sound": "Suono di notifica",
|
||||
"sound_desc": "Riproduci un suono all'arrivo di nuove e-mail"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Notifiche calendario",
|
||||
"description": "Configura le notifiche per gli eventi del calendario",
|
||||
"enabled": "Notifiche eventi",
|
||||
"enabled_desc": "Mostra avvisi per i prossimi eventi del calendario",
|
||||
"sound": "Suono di notifica",
|
||||
"sound_desc": "Riproduci un suono per i promemoria del calendario",
|
||||
"invitation_parsing": "Analizza inviti via e-mail",
|
||||
"invitation_parsing_desc": "Rileva inviti calendario negli allegati e mostra azioni calendario"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Lingua e regione",
|
||||
"description": "Configura le preferenze di lingua e regionali",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "暗号化",
|
||||
"files": "ファイル",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "サイドバーアプリ"
|
||||
"sidebar_apps": "サイドバーアプリ",
|
||||
"notifications": "通知"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "一般",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "既存のメールでキーワードを更新中…",
|
||||
"migration_error": "既存のメールでのキーワード更新に失敗しました"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "通知音をテスト",
|
||||
"sounds": {
|
||||
"default": "デフォルト(ビープ)",
|
||||
"cheerful": "チアフル",
|
||||
"involved": "インボルブド",
|
||||
"swift": "スウィフトジェスチャー",
|
||||
"relax": "リラックス"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "通知音",
|
||||
"description": "通知に使用する音を選択",
|
||||
"choose": "サウンド",
|
||||
"choose_desc": "通知音を選択し、スピーカーアイコンをクリックしてプレビュー"
|
||||
},
|
||||
"email": {
|
||||
"title": "メール通知",
|
||||
"description": "受信メールの通知を設定",
|
||||
"enabled": "メール通知",
|
||||
"enabled_desc": "新しいメールが届いたときに通知を表示",
|
||||
"sound": "通知音",
|
||||
"sound_desc": "新しいメールが届いたときに音を鳴らす"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "カレンダー通知",
|
||||
"description": "カレンダーイベントの通知を設定",
|
||||
"enabled": "イベント通知",
|
||||
"enabled_desc": "今後のカレンダーイベントのアラートを表示",
|
||||
"sound": "通知音",
|
||||
"sound_desc": "カレンダーリマインダーの音を鳴らす",
|
||||
"invitation_parsing": "メール招待を解析",
|
||||
"invitation_parsing_desc": "メール添付ファイルのカレンダー招待を検出し、カレンダーアクションを表示"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "言語と地域",
|
||||
"description": "言語と地域の設定を構成",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "Versleuteling",
|
||||
"files": "Bestanden",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "Zijbalk-apps"
|
||||
"sidebar_apps": "Zijbalk-apps",
|
||||
"notifications": "Meldingen"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Algemeen",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Trefwoord bijwerken op bestaande e-mails…",
|
||||
"migration_error": "Kan trefwoord niet bijwerken op bestaande e-mails"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Meldingsgeluid testen",
|
||||
"sounds": {
|
||||
"default": "Standaard (Pieptoon)",
|
||||
"cheerful": "Vrolijk",
|
||||
"involved": "Uitgebreid",
|
||||
"swift": "Snel gebaar",
|
||||
"relax": "Ontspannen"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Meldingsgeluid",
|
||||
"description": "Kies welk geluid wordt afgespeeld voor meldingen",
|
||||
"choose": "Geluid",
|
||||
"choose_desc": "Selecteer een meldingstoon en klik op het luidsprekerpictogram voor een voorbeeld"
|
||||
},
|
||||
"email": {
|
||||
"title": "E-mailmeldingen",
|
||||
"description": "Meldingen voor inkomende e-mails configureren",
|
||||
"enabled": "E-mailmeldingen",
|
||||
"enabled_desc": "Meldingen tonen wanneer nieuwe e-mails binnenkomen",
|
||||
"sound": "Meldingsgeluid",
|
||||
"sound_desc": "Een geluid afspelen wanneer nieuwe e-mails binnenkomen"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Agendameldingen",
|
||||
"description": "Meldingen voor agenda-evenementen configureren",
|
||||
"enabled": "Evenementmeldingen",
|
||||
"enabled_desc": "Waarschuwingen tonen voor aankomende agenda-evenementen",
|
||||
"sound": "Meldingsgeluid",
|
||||
"sound_desc": "Een geluid afspelen voor agendaherinneringen",
|
||||
"invitation_parsing": "E-mailuitnodigingen herkennen",
|
||||
"invitation_parsing_desc": "Agenda-uitnodigingen in e-mailbijlagen detecteren en agendaacties tonen"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Taal & Regio",
|
||||
"description": "Configureer taal- en regiovoorkeuren",
|
||||
|
||||
+36
-1
@@ -624,7 +624,8 @@
|
||||
"encryption": "Criptografia",
|
||||
"files": "Arquivos",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "Apps da barra lateral"
|
||||
"sidebar_apps": "Apps da barra lateral",
|
||||
"notifications": "Notificações"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Geral",
|
||||
@@ -696,6 +697,40 @@
|
||||
"migrating": "Atualizando etiqueta nos e-mails existentes…",
|
||||
"migration_error": "Falha ao atualizar etiqueta nos e-mails existentes"
|
||||
},
|
||||
"notifications": {
|
||||
"test_sound": "Testar som de notificação",
|
||||
"sounds": {
|
||||
"default": "Padrão (Bipe)",
|
||||
"cheerful": "Alegre",
|
||||
"involved": "Elaborado",
|
||||
"swift": "Gesto rápido",
|
||||
"relax": "Relaxante"
|
||||
},
|
||||
"sound_selection": {
|
||||
"title": "Som de notificação",
|
||||
"description": "Escolha qual som reproduzir para notificações",
|
||||
"choose": "Som",
|
||||
"choose_desc": "Selecione um toque de notificação e clique no ícone do alto-falante para pré-visualizar"
|
||||
},
|
||||
"email": {
|
||||
"title": "Notificações de e-mail",
|
||||
"description": "Configurar notificações para e-mails recebidos",
|
||||
"enabled": "Notificações de e-mail",
|
||||
"enabled_desc": "Mostrar notificações quando novos e-mails chegarem",
|
||||
"sound": "Som de notificação",
|
||||
"sound_desc": "Reproduzir um som quando novos e-mails chegarem"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Notificações de calendário",
|
||||
"description": "Configurar notificações para eventos do calendário",
|
||||
"enabled": "Notificações de eventos",
|
||||
"enabled_desc": "Mostrar alertas para próximos eventos do calendário",
|
||||
"sound": "Som de notificação",
|
||||
"sound_desc": "Reproduzir um som para lembretes do calendário",
|
||||
"invitation_parsing": "Analisar convites por e-mail",
|
||||
"invitation_parsing_desc": "Detectar convites de calendário em anexos de e-mail e mostrar ações de calendário"
|
||||
}
|
||||
},
|
||||
"language_region": {
|
||||
"title": "Idioma e Região",
|
||||
"description": "Configure preferências de idioma e região",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { useThemeStore } from './theme-store';
|
||||
import { useLocaleStore } from './locale-store';
|
||||
import type { NotificationSoundChoice } from '@/lib/notification-sound';
|
||||
|
||||
// Use console directly to avoid circular dependency with lib/debug.ts
|
||||
// (debug.ts imports useSettingsStore for debugMode check)
|
||||
@@ -130,6 +131,11 @@ interface SettingsState {
|
||||
enableCalendarTasks: boolean;
|
||||
showTasksOnCalendar: boolean;
|
||||
|
||||
// Email Notifications
|
||||
emailNotificationsEnabled: boolean;
|
||||
emailNotificationSound: boolean;
|
||||
notificationSoundChoice: NotificationSoundChoice;
|
||||
|
||||
// Calendar Notifications
|
||||
calendarNotificationsEnabled: boolean;
|
||||
calendarNotificationSound: boolean;
|
||||
@@ -238,6 +244,11 @@ const DEFAULT_SETTINGS = {
|
||||
enableCalendarTasks: false,
|
||||
showTasksOnCalendar: true,
|
||||
|
||||
// Email Notifications
|
||||
emailNotificationsEnabled: true,
|
||||
emailNotificationSound: true,
|
||||
notificationSoundChoice: 'default' as NotificationSoundChoice,
|
||||
|
||||
// Calendar Notifications
|
||||
calendarNotificationsEnabled: true,
|
||||
calendarNotificationSound: true,
|
||||
@@ -319,6 +330,9 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
sendConfirmation: state.sendConfirmation,
|
||||
defaultReplyMode: state.defaultReplyMode,
|
||||
sessionTimeout: state.sessionTimeout,
|
||||
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
||||
emailNotificationSound: state.emailNotificationSound,
|
||||
notificationSoundChoice: state.notificationSoundChoice,
|
||||
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
||||
calendarNotificationSound: state.calendarNotificationSound,
|
||||
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
||||
|
||||
Reference in New Issue
Block a user