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;
|
if (!serverUrl) return;
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (suggestionsRef.current && !suggestionsRef.current.contains(event.target as Node) &&
|
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);
|
setShowSuggestions(false);
|
||||||
}
|
}
|
||||||
if (themeMenuRef.current && !themeMenuRef.current.contains(event.target as Node)) {
|
if (themeMenuRef.current && !themeMenuRef.current.contains(event.target as Node)) {
|
||||||
|
|||||||
@@ -407,7 +407,10 @@ export default function Home() {
|
|||||||
// Handle new email notifications - play sound
|
// Handle new email notifications - play sound
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (newEmailNotification) {
|
if (newEmailNotification) {
|
||||||
playNotificationSound();
|
const { emailNotificationsEnabled, emailNotificationSound, notificationSoundChoice } = useSettingsStore.getState();
|
||||||
|
if (emailNotificationsEnabled && emailNotificationSound) {
|
||||||
|
playNotificationSound(notificationSoundChoice);
|
||||||
|
}
|
||||||
debug.log('New email received:', newEmailNotification.subject);
|
debug.log('New email received:', newEmailNotification.subject);
|
||||||
clearNewEmailNotification();
|
clearNewEmailNotification();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
BookUser,
|
BookUser,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
|
Bell,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { ContactsSettings } from '@/components/settings/contacts-settings';
|
||||||
import { SmimeSettings } from '@/components/settings/smime-settings';
|
import { SmimeSettings } from '@/components/settings/smime-settings';
|
||||||
import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings';
|
import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings';
|
||||||
|
import { NotificationSettings } from '@/components/settings/notification-settings';
|
||||||
import { useAuthStore, redirectToLogin } from '@/stores/auth-store';
|
import { useAuthStore, redirectToLogin } from '@/stores/auth-store';
|
||||||
import { useEmailStore } from '@/stores/email-store';
|
import { useEmailStore } from '@/stores/email-store';
|
||||||
import { useIsDesktop } from '@/hooks/use-media-query';
|
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 { useConfig } from '@/hooks/use-config';
|
||||||
import { cn } from '@/lib/utils';
|
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';
|
type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system';
|
||||||
|
|
||||||
interface TabDef {
|
interface TabDef {
|
||||||
@@ -68,6 +70,7 @@ interface TabDef {
|
|||||||
const tabIcons: Record<Tab, LucideIcon> = {
|
const tabIcons: Record<Tab, LucideIcon> = {
|
||||||
appearance: Palette,
|
appearance: Palette,
|
||||||
email: Mail,
|
email: Mail,
|
||||||
|
notifications: Bell,
|
||||||
account: User,
|
account: User,
|
||||||
security: Shield,
|
security: Shield,
|
||||||
identities: UserPen,
|
identities: UserPen,
|
||||||
@@ -138,6 +141,7 @@ export default function SettingsPage() {
|
|||||||
const tabs: TabDef[] = [
|
const tabs: TabDef[] = [
|
||||||
{ id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'general' },
|
{ id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'general' },
|
||||||
{ id: 'email', label: t('tabs.email'), icon: tabIcons.email, 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' },
|
{ 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 }] : []),
|
...(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' },
|
{ id: 'identities', label: t('tabs.identities'), icon: tabIcons.identities, group: 'account' },
|
||||||
@@ -177,6 +181,7 @@ export default function SettingsPage() {
|
|||||||
<>
|
<>
|
||||||
{activeTab === 'appearance' && <AppearanceSettings />}
|
{activeTab === 'appearance' && <AppearanceSettings />}
|
||||||
{activeTab === 'email' && <EmailSettings />}
|
{activeTab === 'email' && <EmailSettings />}
|
||||||
|
{activeTab === 'notifications' && <NotificationSettings />}
|
||||||
{activeTab === 'account' && <AccountSettings />}
|
{activeTab === 'account' && <AccountSettings />}
|
||||||
{activeTab === 'security' && <AccountSecuritySettings />}
|
{activeTab === 'security' && <AccountSecuritySettings />}
|
||||||
{activeTab === 'identities' && <IdentitySettings />}
|
{activeTab === 'identities' && <IdentitySettings />}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export function CalendarSettings() {
|
|||||||
firstDayOfWeek,
|
firstDayOfWeek,
|
||||||
showTimeInMonthView,
|
showTimeInMonthView,
|
||||||
showWeekNumbers,
|
showWeekNumbers,
|
||||||
calendarNotificationsEnabled,
|
|
||||||
calendarNotificationSound,
|
|
||||||
calendarInvitationParsingEnabled,
|
|
||||||
enableCalendarTasks,
|
enableCalendarTasks,
|
||||||
showTasksOnCalendar,
|
showTasksOnCalendar,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
@@ -103,37 +100,6 @@ export function CalendarSettings() {
|
|||||||
</SettingItem>
|
</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>
|
</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() {
|
export function useCalendarAlerts() {
|
||||||
const { isAuthenticated, client } = useAuthStore();
|
const { isAuthenticated, client } = useAuthStore();
|
||||||
const { events, calendars, supportsCalendar } = useCalendarStore();
|
const { events, calendars, supportsCalendar } = useCalendarStore();
|
||||||
const { calendarNotificationsEnabled, calendarNotificationSound, enableCalendarTasks } = useSettingsStore();
|
const { calendarNotificationsEnabled, calendarNotificationSound, enableCalendarTasks, notificationSoundChoice } = useSettingsStore();
|
||||||
const { tasks: storeTasks } = useTaskStore();
|
const { tasks: storeTasks } = useTaskStore();
|
||||||
const { acknowledgedAlerts, acknowledgeAlert, cleanupStaleAlerts } = useCalendarNotificationStore();
|
const { acknowledgedAlerts, acknowledgeAlert, cleanupStaleAlerts } = useCalendarNotificationStore();
|
||||||
const addToast = useToastStore((s) => s.addToast);
|
const addToast = useToastStore((s) => s.addToast);
|
||||||
@@ -47,7 +47,7 @@ export function useCalendarAlerts() {
|
|||||||
acknowledgeAlert(key, alert.fireTimeMs);
|
acknowledgeAlert(key, alert.fireTimeMs);
|
||||||
|
|
||||||
if (calendarNotificationSound) {
|
if (calendarNotificationSound) {
|
||||||
playNotificationSound();
|
playNotificationSound(notificationSoundChoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
const diffMs = new Date(alert.event.utcStart || alert.event.start).getTime() - now;
|
const diffMs = new Date(alert.event.utcStart || alert.event.start).getTime() - now;
|
||||||
@@ -83,7 +83,7 @@ export function useCalendarAlerts() {
|
|||||||
acknowledgeAlert(key, taskAlert.fireTimeMs);
|
acknowledgeAlert(key, taskAlert.fireTimeMs);
|
||||||
|
|
||||||
if (calendarNotificationSound) {
|
if (calendarNotificationSound) {
|
||||||
playNotificationSound();
|
playNotificationSound(notificationSoundChoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskMsg = taskAlert.calendarName
|
const taskMsg = taskAlert.calendarName
|
||||||
@@ -105,7 +105,7 @@ export function useCalendarAlerts() {
|
|||||||
// Silently ignore alert evaluation errors
|
// Silently ignore alert evaluation errors
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
calendarNotificationsEnabled, calendarNotificationSound,
|
calendarNotificationsEnabled, calendarNotificationSound, notificationSoundChoice,
|
||||||
isAuthenticated, events, calendars, acknowledgedAlerts,
|
isAuthenticated, events, calendars, acknowledgedAlerts,
|
||||||
acknowledgeAlert, addToast, t, locale,
|
acknowledgeAlert, addToast, t, locale,
|
||||||
]);
|
]);
|
||||||
|
|||||||
+114
-3
@@ -3479,6 +3479,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
private pollingInterval: NodeJS.Timeout | null = null;
|
private pollingInterval: NodeJS.Timeout | null = null;
|
||||||
private pollingStates: { [key: string]: string } = {};
|
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> = {
|
private static readonly STATE_TYPE_MAP: Record<string, string> = {
|
||||||
'Mailbox/get': 'Mailbox',
|
'Mailbox/get': 'Mailbox',
|
||||||
@@ -3488,13 +3490,114 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
'SieveScript/get': 'SieveScript',
|
'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 {
|
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.fetchCurrentStates();
|
||||||
this.pollingInterval = setInterval(() => {
|
this.pollingInterval = setInterval(() => {
|
||||||
this.checkForStateChanges();
|
this.checkForStateChanges();
|
||||||
}, 15_000);
|
}, JMAPClient.POLLING_INTERVAL);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildStatePollingRequest(): { using: string[]; methodCalls: JMAPMethodCall[] } {
|
private buildStatePollingRequest(): { using: string[]; methodCalls: JMAPMethodCall[] } {
|
||||||
@@ -3588,6 +3691,14 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
clearInterval(this.pollingInterval);
|
clearInterval(this.pollingInterval);
|
||||||
this.pollingInterval = null;
|
this.pollingInterval = null;
|
||||||
}
|
}
|
||||||
|
if (this.sseAbortController) {
|
||||||
|
this.sseAbortController.abort();
|
||||||
|
this.sseAbortController = null;
|
||||||
|
}
|
||||||
|
if (this.sseReconnectTimeout) {
|
||||||
|
clearTimeout(this.sseReconnectTimeout);
|
||||||
|
this.sseReconnectTimeout = null;
|
||||||
|
}
|
||||||
if (this.eventSource) {
|
if (this.eventSource) {
|
||||||
this.eventSource.close();
|
this.eventSource.close();
|
||||||
this.eventSource = null;
|
this.eventSource = null;
|
||||||
|
|||||||
+44
-14
@@ -1,21 +1,51 @@
|
|||||||
import { debug } from '@/lib/debug';
|
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 {
|
try {
|
||||||
const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
|
const choice = sound ?? 'default';
|
||||||
const oscillator = audioContext.createOscillator();
|
const entry = NOTIFICATION_SOUNDS.find((s) => s.id === choice);
|
||||||
const gainNode = audioContext.createGain();
|
|
||||||
|
|
||||||
oscillator.connect(gainNode);
|
if (entry?.file) {
|
||||||
gainNode.connect(audioContext.destination);
|
playFile(entry.file);
|
||||||
|
} else {
|
||||||
oscillator.frequency.value = 800;
|
playBeep();
|
||||||
oscillator.type = 'sine';
|
}
|
||||||
gainNode.gain.value = 0.1;
|
|
||||||
|
|
||||||
oscillator.start();
|
|
||||||
oscillator.stop(audioContext.currentTime + 0.15);
|
|
||||||
oscillator.onended = () => audioContext.close();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug.log('Could not play notification sound:', e);
|
debug.log('Could not play notification sound:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "Verschlüsselung",
|
"encryption": "Verschlüsselung",
|
||||||
"files": "Dateien",
|
"files": "Dateien",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "Sidebar-Apps"
|
"sidebar_apps": "Sidebar-Apps",
|
||||||
|
"notifications": "Benachrichtigungen"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Allgemein",
|
"general": "Allgemein",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Schlüsselwort bei bestehenden E-Mails aktualisieren…",
|
"migrating": "Schlüsselwort bei bestehenden E-Mails aktualisieren…",
|
||||||
"migration_error": "Schlüsselwort konnte bei bestehenden E-Mails nicht aktualisiert werden"
|
"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": {
|
"language_region": {
|
||||||
"title": "Sprache & Region",
|
"title": "Sprache & Region",
|
||||||
"description": "Konfigurieren Sie Sprach- und Regionaleinstellungen",
|
"description": "Konfigurieren Sie Sprach- und Regionaleinstellungen",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"files": "Files",
|
"files": "Files",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"encryption": "Encryption",
|
"encryption": "Encryption",
|
||||||
"sidebar_apps": "Sidebar Apps"
|
"sidebar_apps": "Sidebar Apps",
|
||||||
|
"notifications": "Notifications"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Updating keyword on existing emails…",
|
"migrating": "Updating keyword on existing emails…",
|
||||||
"migration_error": "Failed to update 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": {
|
"language_region": {
|
||||||
"title": "Language & Region",
|
"title": "Language & Region",
|
||||||
"description": "Configure language and regional preferences",
|
"description": "Configure language and regional preferences",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "Cifrado",
|
"encryption": "Cifrado",
|
||||||
"files": "Archivos",
|
"files": "Archivos",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "Apps de barra lateral"
|
"sidebar_apps": "Apps de barra lateral",
|
||||||
|
"notifications": "Notificaciones"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Actualizando etiqueta en correos existentes…",
|
"migrating": "Actualizando etiqueta en correos existentes…",
|
||||||
"migration_error": "Error al actualizar la 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": {
|
"language_region": {
|
||||||
"title": "Idioma y Región",
|
"title": "Idioma y Región",
|
||||||
"description": "Configure las preferencias de idioma y región",
|
"description": "Configure las preferencias de idioma y región",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "Chiffrement",
|
"encryption": "Chiffrement",
|
||||||
"files": "Fichiers",
|
"files": "Fichiers",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "Apps de la barre latérale"
|
"sidebar_apps": "Apps de la barre latérale",
|
||||||
|
"notifications": "Notifications"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Général",
|
"general": "Général",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Mise à jour du mot-clé sur les e-mails existants…",
|
"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"
|
"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": {
|
"language_region": {
|
||||||
"title": "Langue et région",
|
"title": "Langue et région",
|
||||||
"description": "Configurez vos préférences linguistiques et régionales",
|
"description": "Configurez vos préférences linguistiques et régionales",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "Cifratura",
|
"encryption": "Cifratura",
|
||||||
"files": "File",
|
"files": "File",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "App nella barra laterale"
|
"sidebar_apps": "App nella barra laterale",
|
||||||
|
"notifications": "Notifiche"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Aggiornamento parola chiave sulle email esistenti…",
|
"migrating": "Aggiornamento parola chiave sulle email esistenti…",
|
||||||
"migration_error": "Impossibile aggiornare la 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": {
|
"language_region": {
|
||||||
"title": "Lingua e regione",
|
"title": "Lingua e regione",
|
||||||
"description": "Configura le preferenze di lingua e regionali",
|
"description": "Configura le preferenze di lingua e regionali",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "暗号化",
|
"encryption": "暗号化",
|
||||||
"files": "ファイル",
|
"files": "ファイル",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "サイドバーアプリ"
|
"sidebar_apps": "サイドバーアプリ",
|
||||||
|
"notifications": "通知"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "一般",
|
"general": "一般",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "既存のメールでキーワードを更新中…",
|
"migrating": "既存のメールでキーワードを更新中…",
|
||||||
"migration_error": "既存のメールでのキーワード更新に失敗しました"
|
"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": {
|
"language_region": {
|
||||||
"title": "言語と地域",
|
"title": "言語と地域",
|
||||||
"description": "言語と地域の設定を構成",
|
"description": "言語と地域の設定を構成",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "Versleuteling",
|
"encryption": "Versleuteling",
|
||||||
"files": "Bestanden",
|
"files": "Bestanden",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "Zijbalk-apps"
|
"sidebar_apps": "Zijbalk-apps",
|
||||||
|
"notifications": "Meldingen"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Trefwoord bijwerken op bestaande e-mails…",
|
"migrating": "Trefwoord bijwerken op bestaande e-mails…",
|
||||||
"migration_error": "Kan trefwoord niet 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": {
|
"language_region": {
|
||||||
"title": "Taal & Regio",
|
"title": "Taal & Regio",
|
||||||
"description": "Configureer taal- en regiovoorkeuren",
|
"description": "Configureer taal- en regiovoorkeuren",
|
||||||
|
|||||||
+36
-1
@@ -624,7 +624,8 @@
|
|||||||
"encryption": "Criptografia",
|
"encryption": "Criptografia",
|
||||||
"files": "Arquivos",
|
"files": "Arquivos",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"sidebar_apps": "Apps da barra lateral"
|
"sidebar_apps": "Apps da barra lateral",
|
||||||
|
"notifications": "Notificações"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
@@ -696,6 +697,40 @@
|
|||||||
"migrating": "Atualizando etiqueta nos e-mails existentes…",
|
"migrating": "Atualizando etiqueta nos e-mails existentes…",
|
||||||
"migration_error": "Falha ao atualizar 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": {
|
"language_region": {
|
||||||
"title": "Idioma e Região",
|
"title": "Idioma e Região",
|
||||||
"description": "Configure preferências de 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 { persist } from 'zustand/middleware';
|
||||||
import { useThemeStore } from './theme-store';
|
import { useThemeStore } from './theme-store';
|
||||||
import { useLocaleStore } from './locale-store';
|
import { useLocaleStore } from './locale-store';
|
||||||
|
import type { NotificationSoundChoice } from '@/lib/notification-sound';
|
||||||
|
|
||||||
// Use console directly to avoid circular dependency with lib/debug.ts
|
// Use console directly to avoid circular dependency with lib/debug.ts
|
||||||
// (debug.ts imports useSettingsStore for debugMode check)
|
// (debug.ts imports useSettingsStore for debugMode check)
|
||||||
@@ -130,6 +131,11 @@ interface SettingsState {
|
|||||||
enableCalendarTasks: boolean;
|
enableCalendarTasks: boolean;
|
||||||
showTasksOnCalendar: boolean;
|
showTasksOnCalendar: boolean;
|
||||||
|
|
||||||
|
// Email Notifications
|
||||||
|
emailNotificationsEnabled: boolean;
|
||||||
|
emailNotificationSound: boolean;
|
||||||
|
notificationSoundChoice: NotificationSoundChoice;
|
||||||
|
|
||||||
// Calendar Notifications
|
// Calendar Notifications
|
||||||
calendarNotificationsEnabled: boolean;
|
calendarNotificationsEnabled: boolean;
|
||||||
calendarNotificationSound: boolean;
|
calendarNotificationSound: boolean;
|
||||||
@@ -238,6 +244,11 @@ const DEFAULT_SETTINGS = {
|
|||||||
enableCalendarTasks: false,
|
enableCalendarTasks: false,
|
||||||
showTasksOnCalendar: true,
|
showTasksOnCalendar: true,
|
||||||
|
|
||||||
|
// Email Notifications
|
||||||
|
emailNotificationsEnabled: true,
|
||||||
|
emailNotificationSound: true,
|
||||||
|
notificationSoundChoice: 'default' as NotificationSoundChoice,
|
||||||
|
|
||||||
// Calendar Notifications
|
// Calendar Notifications
|
||||||
calendarNotificationsEnabled: true,
|
calendarNotificationsEnabled: true,
|
||||||
calendarNotificationSound: true,
|
calendarNotificationSound: true,
|
||||||
@@ -319,6 +330,9 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
sendConfirmation: state.sendConfirmation,
|
sendConfirmation: state.sendConfirmation,
|
||||||
defaultReplyMode: state.defaultReplyMode,
|
defaultReplyMode: state.defaultReplyMode,
|
||||||
sessionTimeout: state.sessionTimeout,
|
sessionTimeout: state.sessionTimeout,
|
||||||
|
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
||||||
|
emailNotificationSound: state.emailNotificationSound,
|
||||||
|
notificationSoundChoice: state.notificationSoundChoice,
|
||||||
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
||||||
calendarNotificationSound: state.calendarNotificationSound,
|
calendarNotificationSound: state.calendarNotificationSound,
|
||||||
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user