Initial release: JMAP Webmail Client
A modern, privacy-focused webmail client built with Next.js and the JMAP protocol. Designed for Stalwart Mail Server. Features: - Full email operations (compose, reply, forward, threading) - Real-time push notifications - Dark/light theme support - Mobile responsive design - Keyboard shortcuts - Drag-and-drop organization - i18n (English/French) - Security-first (external content blocked, HTML sanitization)
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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)
|
||||
|
||||
// Advanced
|
||||
debugMode: boolean;
|
||||
|
||||
// Actions
|
||||
updateSetting: <K extends keyof SettingsState>(
|
||||
key: K,
|
||||
value: SettingsState[K]
|
||||
) => void;
|
||||
resetToDefaults: () => void;
|
||||
exportSettings: () => string;
|
||||
importSettings: (json: 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
|
||||
|
||||
// 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,
|
||||
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
||||
sendConfirmation: state.sendConfirmation,
|
||||
defaultReplyMode: state.defaultReplyMode,
|
||||
sessionTimeout: state.sessionTimeout,
|
||||
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;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user