Add client-side calendar event notification system that evaluates JMAP CalendarEventAlert triggers and displays toast notifications when alert times are reached. Includes configurable notification sound, acknowledged alert persistence, and proactive event fetching for background alerts. Also mounts ToastContainer globally to fix silent toast failures.
253 lines
7.1 KiB
TypeScript
253 lines
7.1 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export type FontSize = 'small' | 'medium' | 'large';
|
|
export type ListDensity = 'compact' | 'regular' | 'comfortable';
|
|
export type DeleteAction = 'trash' | 'permanent';
|
|
export type ReplyMode = 'reply' | 'replyAll';
|
|
export type DateFormat = 'regional' | 'iso' | 'custom';
|
|
export type TimeFormat = '12h' | '24h';
|
|
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
|
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
|
|
|
interface SettingsState {
|
|
// Appearance
|
|
fontSize: FontSize;
|
|
listDensity: ListDensity;
|
|
animationsEnabled: boolean;
|
|
|
|
// Language & Region
|
|
dateFormat: DateFormat;
|
|
timeFormat: TimeFormat;
|
|
firstDayOfWeek: FirstDayOfWeek;
|
|
|
|
// Email Behavior
|
|
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
|
|
deleteAction: DeleteAction;
|
|
showPreview: boolean;
|
|
emailsPerPage: number;
|
|
externalContentPolicy: ExternalContentPolicy;
|
|
|
|
// Composer
|
|
autoSaveDraftInterval: number; // milliseconds
|
|
sendConfirmation: boolean;
|
|
defaultReplyMode: ReplyMode;
|
|
|
|
// Privacy & Security
|
|
sessionTimeout: number; // minutes (0 = never)
|
|
trustedSenders: string[]; // Email addresses that can load external content
|
|
|
|
// Calendar Notifications
|
|
calendarNotificationsEnabled: boolean;
|
|
calendarNotificationSound: boolean;
|
|
|
|
// Advanced
|
|
debugMode: boolean;
|
|
|
|
// Actions
|
|
updateSetting: <K extends keyof SettingsState>(
|
|
key: K,
|
|
value: SettingsState[K]
|
|
) => void;
|
|
resetToDefaults: () => void;
|
|
exportSettings: () => string;
|
|
importSettings: (json: string) => boolean;
|
|
|
|
// Trusted senders
|
|
addTrustedSender: (email: string) => void;
|
|
removeTrustedSender: (email: string) => void;
|
|
isSenderTrusted: (email: string) => boolean;
|
|
}
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
// Appearance
|
|
fontSize: 'medium' as FontSize,
|
|
listDensity: 'regular' as ListDensity,
|
|
animationsEnabled: true,
|
|
|
|
// Language & Region
|
|
dateFormat: 'regional' as DateFormat,
|
|
timeFormat: '24h' as TimeFormat,
|
|
firstDayOfWeek: 1 as FirstDayOfWeek, // Monday
|
|
|
|
// Email Behavior
|
|
markAsReadDelay: 0, // Instant
|
|
deleteAction: 'trash' as DeleteAction,
|
|
showPreview: true,
|
|
emailsPerPage: 50,
|
|
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
|
|
|
// Composer
|
|
autoSaveDraftInterval: 60000, // 1 minute
|
|
sendConfirmation: false,
|
|
defaultReplyMode: 'reply' as ReplyMode,
|
|
|
|
// Privacy & Security
|
|
sessionTimeout: 0, // Never
|
|
trustedSenders: [] as string[],
|
|
|
|
// Calendar Notifications
|
|
calendarNotificationsEnabled: true,
|
|
calendarNotificationSound: true,
|
|
|
|
// Advanced
|
|
debugMode: false,
|
|
};
|
|
|
|
export const useSettingsStore = create<SettingsState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...DEFAULT_SETTINGS,
|
|
|
|
updateSetting: (key, value) => {
|
|
set({ [key]: value });
|
|
|
|
// Apply font size to document root
|
|
if (key === 'fontSize') {
|
|
applyFontSize(value as FontSize);
|
|
}
|
|
|
|
// Apply list density to document root
|
|
if (key === 'listDensity') {
|
|
applyListDensity(value as ListDensity);
|
|
}
|
|
|
|
// Apply animations to document root
|
|
if (key === 'animationsEnabled') {
|
|
applyAnimations(value as boolean);
|
|
}
|
|
},
|
|
|
|
resetToDefaults: () => {
|
|
set(DEFAULT_SETTINGS);
|
|
applyFontSize(DEFAULT_SETTINGS.fontSize);
|
|
applyListDensity(DEFAULT_SETTINGS.listDensity);
|
|
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
|
|
},
|
|
|
|
exportSettings: () => {
|
|
const state = get();
|
|
const settings = {
|
|
fontSize: state.fontSize,
|
|
listDensity: state.listDensity,
|
|
animationsEnabled: state.animationsEnabled,
|
|
dateFormat: state.dateFormat,
|
|
timeFormat: state.timeFormat,
|
|
firstDayOfWeek: state.firstDayOfWeek,
|
|
markAsReadDelay: state.markAsReadDelay,
|
|
deleteAction: state.deleteAction,
|
|
showPreview: state.showPreview,
|
|
emailsPerPage: state.emailsPerPage,
|
|
externalContentPolicy: state.externalContentPolicy,
|
|
trustedSenders: state.trustedSenders,
|
|
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
|
sendConfirmation: state.sendConfirmation,
|
|
defaultReplyMode: state.defaultReplyMode,
|
|
sessionTimeout: state.sessionTimeout,
|
|
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
|
calendarNotificationSound: state.calendarNotificationSound,
|
|
debugMode: state.debugMode,
|
|
};
|
|
return JSON.stringify(settings, null, 2);
|
|
},
|
|
|
|
importSettings: (json: string) => {
|
|
try {
|
|
const settings = JSON.parse(json);
|
|
|
|
// Validate settings
|
|
if (typeof settings !== 'object' || settings === null) {
|
|
return false;
|
|
}
|
|
|
|
// Apply settings
|
|
Object.keys(settings).forEach((key) => {
|
|
if (key in DEFAULT_SETTINGS) {
|
|
set({ [key]: settings[key] });
|
|
}
|
|
});
|
|
|
|
// Apply visual settings
|
|
applyFontSize(get().fontSize);
|
|
applyListDensity(get().listDensity);
|
|
applyAnimations(get().animationsEnabled);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to import settings:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// Trusted senders methods
|
|
addTrustedSender: (email: string) => {
|
|
const normalizedEmail = email.toLowerCase().trim();
|
|
const current = get().trustedSenders;
|
|
if (!current.includes(normalizedEmail)) {
|
|
set({ trustedSenders: [...current, normalizedEmail] });
|
|
}
|
|
},
|
|
|
|
removeTrustedSender: (email: string) => {
|
|
const normalizedEmail = email.toLowerCase().trim();
|
|
set({
|
|
trustedSenders: get().trustedSenders.filter(e => e !== normalizedEmail)
|
|
});
|
|
},
|
|
|
|
isSenderTrusted: (email: string) => {
|
|
const normalizedEmail = email.toLowerCase().trim();
|
|
return get().trustedSenders.includes(normalizedEmail);
|
|
},
|
|
}),
|
|
{
|
|
name: 'settings-storage',
|
|
version: 1,
|
|
}
|
|
)
|
|
);
|
|
|
|
// Helper functions to apply settings to DOM
|
|
function applyFontSize(size: FontSize) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const sizeMap = {
|
|
small: '14px',
|
|
medium: '16px',
|
|
large: '18px',
|
|
};
|
|
root.style.setProperty('--font-size-base', sizeMap[size]);
|
|
}
|
|
|
|
function applyListDensity(density: ListDensity) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const densityMap = {
|
|
compact: '32px',
|
|
regular: '48px',
|
|
comfortable: '64px',
|
|
};
|
|
root.style.setProperty('--list-item-height', densityMap[density]);
|
|
}
|
|
|
|
function applyAnimations(enabled: boolean) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
if (enabled) {
|
|
root.style.removeProperty('--transition-duration');
|
|
} else {
|
|
root.style.setProperty('--transition-duration', '0s');
|
|
}
|
|
}
|
|
|
|
// Initialize settings on load
|
|
if (typeof window !== 'undefined') {
|
|
const store = useSettingsStore.getState();
|
|
applyFontSize(store.fontSize);
|
|
applyListDensity(store.listDensity);
|
|
applyAnimations(store.animationsEnabled);
|
|
}
|