diff --git a/TODO.md b/TODO.md index 8171715..edfd9ec 100644 --- a/TODO.md +++ b/TODO.md @@ -118,7 +118,13 @@ - [x] Add smooth email loading transitions with cross-fade effect - [x] Implement functional quick reply with auto-expand and direct send - [ ] Implement error boundaries -- [ ] Create settings page +- [x] Create settings page +- [x] Integrate mark-as-read delay setting in email viewer +- [ ] Integrate delete action setting (trash vs permanent) +- [ ] Integrate show preview toggle in email list +- [ ] Integrate external content policy with image loading +- [ ] Integrate debug mode with console logging +- [ ] Integrate animations toggle throughout components - [ ] Add keyboard shortcuts (j/k navigation, etc.) - [ ] Implement drag-and-drop for emails - [ ] Add context menus @@ -205,4 +211,7 @@ - [x] Fixed light mode not applying properly (replaced hardcoded colors with CSS variables) - [x] Fixed all hardcoded colors in email viewer for proper theme support - [x] Fixed sidebar footer menu extending page height when scrolling (overflow containment) +- [x] Fixed dark mode folder selection not visible (improved accent color contrast) +- [x] Fixed inbox not selected by default on login (auto-select primary account inbox) +- [x] Fixed email store not cleared on logout (proper state reset) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index d45e451..2c5fdcd 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { useRouter, useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { Sidebar } from "@/components/layout/sidebar"; @@ -9,6 +9,7 @@ import { EmailViewer } from "@/components/email/email-viewer"; import { EmailComposer } from "@/components/email/email-composer"; import { useEmailStore } from "@/stores/email-store"; import { useAuthStore } from "@/stores/auth-store"; +import { useSettingsStore } from "@/stores/settings-store"; export default function Home() { const router = useRouter(); @@ -18,6 +19,7 @@ export default function Home() { const [composerMode, setComposerMode] = useState<'compose' | 'reply' | 'replyAll' | 'forward'>('compose'); const [dataLoaded, setDataLoaded] = useState(false); const [initialCheckDone, setInitialCheckDone] = useState(false); + const markAsReadTimeoutRef = useRef(null); const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading } = useAuthStore(); const { emails, @@ -80,9 +82,10 @@ export default function Home() { }); }, [checkAuth]); - // Redirect to login if not authenticated + // Redirect to login if not authenticated and reset data loaded flag useEffect(() => { if (initialCheckDone && !isAuthenticated && !authLoading) { + setDataLoaded(false); // Reset so data is reloaded on next login router.push(`/${params.locale}/login`); } }, [initialCheckDone, isAuthenticated, authLoading, router, params.locale]); @@ -92,21 +95,19 @@ export default function Home() { if (isAuthenticated && client && !dataLoaded) { const loadData = async () => { try { - // First fetch mailboxes and quota + // First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes) await Promise.all([ fetchMailboxes(client), fetchQuota(client) ]); - // Get the actual inbox mailbox ID + // Get the selected mailbox (should be inbox by default) const state = useEmailStore.getState(); - const inboxMailbox = state.mailboxes.find(m => m.role === 'inbox'); + const selectedMailboxId = state.selectedMailbox; - if (inboxMailbox) { - // Update selected mailbox to actual inbox ID - state.selectMailbox(inboxMailbox.id); - // Now fetch emails with correct mailbox ID - await fetchEmails(client, inboxMailbox.id); + // Fetch emails for the selected mailbox + if (selectedMailboxId) { + await fetchEmails(client, selectedMailboxId); } else { await fetchEmails(client); } @@ -120,6 +121,52 @@ export default function Home() { } }, [isAuthenticated, client, dataLoaded, fetchMailboxes, fetchEmails, fetchQuota]); + // Handle mark-as-read with delay based on settings + useEffect(() => { + // Clear any existing timeout when email changes + if (markAsReadTimeoutRef.current) { + console.log('[Mark as Read] Clearing previous timeout'); + clearTimeout(markAsReadTimeoutRef.current); + markAsReadTimeoutRef.current = null; + } + + // Only set timeout if there's a selected email, it's unread, and we have a client + if (!selectedEmail || !client || selectedEmail.keywords?.$seen) { + return; + } + + // Get current setting value + const markAsReadDelay = useSettingsStore.getState().markAsReadDelay; + console.log('[Mark as Read] Delay setting:', markAsReadDelay, 'ms for email:', selectedEmail.id); + + if (markAsReadDelay === -1) { + // Never mark as read automatically + console.log('[Mark as Read] Never mode - email will stay unread'); + } else if (markAsReadDelay === 0) { + // Mark as read instantly + console.log('[Mark as Read] Instant mode - marking as read now'); + markAsRead(client, selectedEmail.id, true); + } else { + // Mark as read after delay + console.log('[Mark as Read] Delayed mode - will mark as read in', markAsReadDelay, 'ms'); + markAsReadTimeoutRef.current = setTimeout(() => { + console.log('[Mark as Read] Timeout fired - marking as read now'); + markAsRead(client, selectedEmail.id, true); + markAsReadTimeoutRef.current = null; + }, markAsReadDelay); + } + + // Cleanup on unmount or when dependencies change + return () => { + if (markAsReadTimeoutRef.current) { + console.log('[Mark as Read] Cleanup - clearing timeout'); + clearTimeout(markAsReadTimeoutRef.current); + markAsReadTimeoutRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedEmail?.id]); + const handleEmailSend = async (data: { to: string[]; cc: string[]; @@ -332,11 +379,7 @@ export default function Home() { const fullEmail = await client.getEmail(email.id, accountId); if (fullEmail) { selectEmail(fullEmail); - - // Automatically mark as read after opening (if unread) - if (!fullEmail.keywords?.$seen) { - await markAsRead(client, fullEmail.id, true); - } + // Mark-as-read logic is now handled by useEffect } } catch (error) { console.error('Failed to fetch email content:', error); diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx new file mode 100644 index 0000000..c089f8a --- /dev/null +++ b/app/[locale]/settings/page.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { useTranslations } from 'next-intl'; +import { ArrowLeft, Settings as SettingsIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { AppearanceSettings } from '@/components/settings/appearance-settings'; +import { EmailSettings } from '@/components/settings/email-settings'; +import { AccountSettings } from '@/components/settings/account-settings'; +import { AdvancedSettings } from '@/components/settings/advanced-settings'; +import { cn } from '@/lib/utils'; + +type Tab = 'appearance' | 'email' | 'account' | 'advanced'; + +export default function SettingsPage() { + const router = useRouter(); + const params = useParams(); + const t = useTranslations('settings'); + const [activeTab, setActiveTab] = useState('appearance'); + + const tabs: { id: Tab; label: string }[] = [ + { id: 'appearance', label: t('tabs.appearance') }, + { id: 'email', label: t('tabs.email') }, + { id: 'account', label: t('tabs.account') }, + { id: 'advanced', label: t('tabs.advanced') }, + ]; + + return ( +
+ {/* Settings Sidebar */} +
+ {/* Header */} +
+ +
+ + {/* Tabs */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+
+ + {/* Settings Content */} +
+
+ {/* Page Header */} +
+
+ +

{t('title')}

+
+
+ + {/* Active Tab Content */} +
+ {activeTab === 'appearance' && } + {activeTab === 'email' && } + {activeTab === 'account' && } + {activeTab === 'advanced' && } +
+
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index ebd90d6..855196a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -16,6 +16,11 @@ --color-muted-foreground: #64748b; --color-accent: #dbeafe; --color-accent-foreground: #1e40af; + + /* Settings variables */ + --font-size-base: 16px; + --list-item-height: 48px; + --transition-duration: 0.2s; } .dark { @@ -30,8 +35,8 @@ --color-secondary-foreground: #fafafa; --color-muted: #262626; --color-muted-foreground: #a3a3a3; - --color-accent: #262626; - --color-accent-foreground: #fafafa; + --color-accent: #1e3a8a; + --color-accent-foreground: #dbeafe; } @theme inline { @@ -58,6 +63,7 @@ body { background-color: var(--color-background); color: var(--color-foreground); font-family: system-ui, -apple-system, sans-serif; + font-size: var(--font-size-base); font-feature-settings: "rlig" 1, "calt" 1; } diff --git a/components/email/email-list-item.tsx b/components/email/email-list-item.tsx index cc53322..f6277df 100644 --- a/components/email/email-list-item.tsx +++ b/components/email/email-list-item.tsx @@ -67,8 +67,12 @@ export function EmailListItem({ email, selected, onClick }: EmailListItemProps) isChecked && "ring-2 ring-primary/20 bg-accent/40" )} onClick={onClick} + style={{ minHeight: 'var(--list-item-height)' }} > -
+
{/* Checkbox with smooth animation */} )}
-
- )} - - {/* Settings Submenu View */} - {menuView === 'settings' && ( -
- {/* Back Button */} - - - {/* Theme */} -
-
Theme
-
- - - -
-
- - {/* Language */} -
-
Language
-
- {locales.map((locale) => ( - - ))} -
-
-
- )} + {/* Menu Toggle Button */}
+ + + {/* Import Settings */} + + <> + + + + + + {/* Reset Settings */} + + + + + ); +} diff --git a/components/settings/appearance-settings.tsx b/components/settings/appearance-settings.tsx new file mode 100644 index 0000000..21dec06 --- /dev/null +++ b/components/settings/appearance-settings.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { useThemeStore } from '@/stores/theme-store'; +import { useSettingsStore } from '@/stores/settings-store'; +import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section'; + +export function AppearanceSettings() { + const t = useTranslations('settings.appearance'); + const { theme, setTheme } = useThemeStore(); + const { fontSize, listDensity, animationsEnabled, updateSetting } = useSettingsStore(); + + return ( + + {/* Theme */} + + setTheme(value as 'light' | 'dark' | 'system')} + options={[ + { value: 'light', label: t('theme.light') }, + { value: 'dark', label: t('theme.dark') }, + { value: 'system', label: t('theme.system') }, + ]} + /> + + + {/* Font Size */} + + updateSetting('fontSize', value as 'small' | 'medium' | 'large')} + options={[ + { value: 'small', label: t('font_size.small') }, + { value: 'medium', label: t('font_size.medium') }, + { value: 'large', label: t('font_size.large') }, + ]} + /> + + + {/* List Density */} + + + updateSetting('listDensity', value as 'compact' | 'regular' | 'comfortable') + } + options={[ + { value: 'compact', label: t('list_density.compact') }, + { value: 'regular', label: t('list_density.regular') }, + { value: 'comfortable', label: t('list_density.comfortable') }, + ]} + /> + + + {/* Animations */} + + updateSetting('animationsEnabled', checked)} + /> + + + ); +} diff --git a/components/settings/email-settings.tsx b/components/settings/email-settings.tsx new file mode 100644 index 0000000..8402ad0 --- /dev/null +++ b/components/settings/email-settings.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { useSettingsStore } from '@/stores/settings-store'; +import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section'; + +export function EmailSettings() { + const t = useTranslations('settings.email_behavior'); + const { + markAsReadDelay, + deleteAction, + showPreview, + emailsPerPage, + externalContentPolicy, + updateSetting, + } = useSettingsStore(); + + return ( + + {/* Mark as Read */} + + updateSetting('deleteAction', value as 'trash' | 'permanent')} + options={[ + { value: 'trash', label: t('delete_action.trash') }, + { value: 'permanent', label: t('delete_action.permanent') }, + ]} + /> + + + {/* Show Preview */} + + updateSetting('showPreview', checked)} /> + + + {/* Emails Per Page */} + + + updateSetting('externalContentPolicy', value as 'ask' | 'block' | 'allow') + } + options={[ + { value: 'ask', label: t('external_content.ask') }, + { value: 'block', label: t('external_content.block') }, + { value: 'allow', label: t('external_content.allow') }, + ]} + /> + + + ); +} diff --git a/components/settings/settings-section.tsx b/components/settings/settings-section.tsx new file mode 100644 index 0000000..640d0b9 --- /dev/null +++ b/components/settings/settings-section.tsx @@ -0,0 +1,123 @@ +import { ReactNode } from 'react'; + +interface SettingsSectionProps { + title: string; + description?: string; + children: ReactNode; +} + +export function SettingsSection({ title, description, children }: SettingsSectionProps) { + return ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+
{children}
+
+ ); +} + +interface SettingItemProps { + label: string; + description?: string; + children: ReactNode; +} + +export function SettingItem({ label, description, children }: SettingItemProps) { + return ( +
+
+ + {description && ( +

{description}

+ )} +
+
{children}
+
+ ); +} + +interface ToggleSwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; +} + +export function ToggleSwitch({ checked, onChange, disabled }: ToggleSwitchProps) { + return ( + + ); +} + +interface RadioGroupProps { + value: string; + onChange: (value: string) => void; + options: { value: string; label: string }[]; +} + +export function RadioGroup({ value, onChange, options }: RadioGroupProps) { + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +} + +interface SelectProps { + value: string; + onChange: (value: string) => void; + options: { value: string; label: string }[]; +} + +export function Select({ value, onChange, options }: SelectProps) { + return ( + + ); +} diff --git a/locales/en/common.json b/locales/en/common.json index 21de95b..d4398b6 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -199,5 +199,210 @@ "title": "Language", "english": "English", "french": "Français" + }, + "settings": { + "title": "Settings", + "back_to_mail": "Back to Mail", + "save_success": "Settings saved successfully", + "import_success": "Settings imported successfully", + "import_error": "Failed to import settings", + "reset_confirm": "Are you sure you want to reset all settings to defaults?", + "tabs": { + "appearance": "Appearance", + "language": "Language & Region", + "email": "Email Behavior", + "composer": "Composer", + "privacy": "Privacy & Security", + "account": "Account", + "advanced": "Advanced" + }, + "appearance": { + "title": "Appearance", + "description": "Customize the look and feel of your webmail", + "theme": { + "label": "Theme", + "description": "Choose your preferred color scheme", + "light": "Light", + "dark": "Dark", + "system": "System" + }, + "font_size": { + "label": "Font Size", + "description": "Adjust text size for better readability", + "small": "Small", + "medium": "Medium", + "large": "Large" + }, + "list_density": { + "label": "List Density", + "description": "Control spacing in email lists", + "compact": "Compact", + "regular": "Regular", + "comfortable": "Comfortable" + }, + "animations": { + "label": "Enable Animations", + "description": "Show smooth transitions and effects" + } + }, + "language_region": { + "title": "Language & Region", + "description": "Configure language and regional preferences", + "language": { + "label": "Language", + "description": "Choose your preferred language", + "english": "English", + "french": "Français" + }, + "date_format": { + "label": "Date Format", + "description": "How dates should be displayed", + "regional": "Regional", + "iso": "ISO 8601", + "custom": "Custom" + }, + "time_format": { + "label": "Time Format", + "description": "Choose between 12-hour or 24-hour clock", + "12h": "12-hour", + "24h": "24-hour" + }, + "first_day": { + "label": "First Day of Week", + "description": "Start week on Sunday or Monday", + "sunday": "Sunday", + "monday": "Monday" + } + }, + "email_behavior": { + "title": "Email Behavior", + "description": "Configure how emails are handled", + "mark_read": { + "label": "Mark as Read", + "description": "When to mark emails as read when opened", + "instant": "Instantly", + "delay_3s": "After 3 seconds", + "delay_5s": "After 5 seconds", + "never": "Never" + }, + "delete_action": { + "label": "Delete Action", + "description": "What happens when you delete an email", + "trash": "Move to Trash", + "permanent": "Delete Permanently" + }, + "show_preview": { + "label": "Show Preview Text", + "description": "Display email preview in the list" + }, + "emails_per_page": { + "label": "Emails Per Page", + "description": "Number of emails to load at once", + "25": "25 emails", + "50": "50 emails", + "100": "100 emails" + }, + "external_content": { + "label": "External Content", + "description": "How to handle images and external content", + "ask": "Always ask", + "block": "Always block", + "allow": "Always allow" + } + }, + "composer": { + "title": "Composer", + "description": "Configure email composition settings", + "autosave": { + "label": "Auto-save Interval", + "description": "How often to save drafts automatically", + "30s": "Every 30 seconds", + "1m": "Every minute", + "2m": "Every 2 minutes", + "5m": "Every 5 minutes" + }, + "send_confirmation": { + "label": "Send Confirmation", + "description": "Ask for confirmation before sending emails" + }, + "default_reply": { + "label": "Default Reply Mode", + "description": "Default action when clicking reply", + "reply": "Reply", + "reply_all": "Reply All" + } + }, + "privacy": { + "title": "Privacy & Security", + "description": "Manage your privacy and security settings", + "external_images": { + "label": "Block External Images", + "description": "Prevent tracking through external images" + }, + "session_timeout": { + "label": "Session Timeout", + "description": "Automatically log out after inactivity", + "never": "Never", + "30m": "30 minutes", + "1h": "1 hour", + "4h": "4 hours" + }, + "clear_cache": { + "label": "Clear Cache", + "description": "Remove cached data and temporary files", + "button": "Clear Cache", + "confirm": "Are you sure you want to clear the cache?", + "success": "Cache cleared successfully" + } + }, + "account": { + "title": "Account", + "description": "View your account information", + "email": { + "label": "Email Address", + "value": "{{email}}" + }, + "server": { + "label": "JMAP Server", + "value": "{{server}}" + }, + "storage": { + "label": "Storage Usage", + "used": "{{used}} of {{total}} used", + "percentage": "{{percent}}% used" + }, + "last_sync": { + "label": "Last Sync", + "value": "{{time}}" + } + }, + "advanced": { + "title": "Advanced", + "description": "Advanced options and developer settings", + "debug_mode": { + "label": "Debug Mode", + "description": "Enable detailed logging for troubleshooting" + }, + "keyboard_shortcuts": { + "label": "Keyboard Shortcuts", + "description": "View available keyboard shortcuts", + "button": "View Shortcuts" + }, + "reset_settings": { + "label": "Reset Settings", + "description": "Restore all settings to default values", + "button": "Reset to Defaults" + }, + "export_settings": { + "label": "Export Settings", + "description": "Download your settings as JSON", + "button": "Export" + }, + "import_settings": { + "label": "Import Settings", + "description": "Upload settings from JSON file", + "button": "Import" + } + } } } \ No newline at end of file diff --git a/locales/fr/common.json b/locales/fr/common.json index 9016a10..5749bbd 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -199,5 +199,210 @@ "title": "Langue", "english": "English", "french": "Français" + }, + "settings": { + "title": "Paramètres", + "back_to_mail": "Retour aux emails", + "save_success": "Paramètres enregistrés avec succès", + "import_success": "Paramètres importés avec succès", + "import_error": "Échec de l'importation des paramètres", + "reset_confirm": "Êtes-vous sûr de vouloir réinitialiser tous les paramètres ?", + "tabs": { + "appearance": "Apparence", + "language": "Langue et région", + "email": "Comportement email", + "composer": "Compositeur", + "privacy": "Confidentialité et sécurité", + "account": "Compte", + "advanced": "Avancé" + }, + "appearance": { + "title": "Apparence", + "description": "Personnalisez l'apparence de votre webmail", + "theme": { + "label": "Thème", + "description": "Choisissez votre schéma de couleurs préféré", + "light": "Clair", + "dark": "Sombre", + "system": "Système" + }, + "font_size": { + "label": "Taille de police", + "description": "Ajustez la taille du texte pour une meilleure lisibilité", + "small": "Petite", + "medium": "Moyenne", + "large": "Grande" + }, + "list_density": { + "label": "Densité de la liste", + "description": "Contrôlez l'espacement dans les listes d'emails", + "compact": "Compacte", + "regular": "Normale", + "comfortable": "Confortable" + }, + "animations": { + "label": "Activer les animations", + "description": "Afficher les transitions et effets fluides" + } + }, + "language_region": { + "title": "Langue et région", + "description": "Configurez vos préférences linguistiques et régionales", + "language": { + "label": "Langue", + "description": "Choisissez votre langue préférée", + "english": "English", + "french": "Français" + }, + "date_format": { + "label": "Format de date", + "description": "Comment les dates doivent être affichées", + "regional": "Régional", + "iso": "ISO 8601", + "custom": "Personnalisé" + }, + "time_format": { + "label": "Format d'heure", + "description": "Choisissez entre 12 heures ou 24 heures", + "12h": "12 heures", + "24h": "24 heures" + }, + "first_day": { + "label": "Premier jour de la semaine", + "description": "Commencer la semaine le dimanche ou le lundi", + "sunday": "Dimanche", + "monday": "Lundi" + } + }, + "email_behavior": { + "title": "Comportement email", + "description": "Configurez la gestion des emails", + "mark_read": { + "label": "Marquer comme lu", + "description": "Quand marquer les emails comme lus à l'ouverture", + "instant": "Instantanément", + "delay_3s": "Après 3 secondes", + "delay_5s": "Après 5 secondes", + "never": "Jamais" + }, + "delete_action": { + "label": "Action de suppression", + "description": "Que se passe-t-il quand vous supprimez un email", + "trash": "Déplacer vers la corbeille", + "permanent": "Supprimer définitivement" + }, + "show_preview": { + "label": "Afficher l'aperçu", + "description": "Afficher l'aperçu de l'email dans la liste" + }, + "emails_per_page": { + "label": "Emails par page", + "description": "Nombre d'emails à charger à la fois", + "25": "25 emails", + "50": "50 emails", + "100": "100 emails" + }, + "external_content": { + "label": "Contenu externe", + "description": "Comment gérer les images et le contenu externe", + "ask": "Toujours demander", + "block": "Toujours bloquer", + "allow": "Toujours autoriser" + } + }, + "composer": { + "title": "Compositeur", + "description": "Configurez les paramètres de composition d'email", + "autosave": { + "label": "Intervalle de sauvegarde automatique", + "description": "Fréquence de sauvegarde automatique des brouillons", + "30s": "Toutes les 30 secondes", + "1m": "Toutes les minutes", + "2m": "Toutes les 2 minutes", + "5m": "Toutes les 5 minutes" + }, + "send_confirmation": { + "label": "Confirmation d'envoi", + "description": "Demander une confirmation avant d'envoyer les emails" + }, + "default_reply": { + "label": "Mode de réponse par défaut", + "description": "Action par défaut lors du clic sur répondre", + "reply": "Répondre", + "reply_all": "Répondre à tous" + } + }, + "privacy": { + "title": "Confidentialité et sécurité", + "description": "Gérez vos paramètres de confidentialité et sécurité", + "external_images": { + "label": "Bloquer les images externes", + "description": "Empêcher le suivi par les images externes" + }, + "session_timeout": { + "label": "Délai d'expiration de session", + "description": "Déconnexion automatique après inactivité", + "never": "Jamais", + "30m": "30 minutes", + "1h": "1 heure", + "4h": "4 heures" + }, + "clear_cache": { + "label": "Vider le cache", + "description": "Supprimer les données en cache et fichiers temporaires", + "button": "Vider le cache", + "confirm": "Êtes-vous sûr de vouloir vider le cache ?", + "success": "Cache vidé avec succès" + } + }, + "account": { + "title": "Compte", + "description": "Consultez les informations de votre compte", + "email": { + "label": "Adresse email", + "value": "{{email}}" + }, + "server": { + "label": "Serveur JMAP", + "value": "{{server}}" + }, + "storage": { + "label": "Utilisation du stockage", + "used": "{{used}} sur {{total}} utilisés", + "percentage": "{{percent}}% utilisé" + }, + "last_sync": { + "label": "Dernière synchronisation", + "value": "{{time}}" + } + }, + "advanced": { + "title": "Avancé", + "description": "Options avancées et paramètres développeur", + "debug_mode": { + "label": "Mode débogage", + "description": "Activer la journalisation détaillée pour le dépannage" + }, + "keyboard_shortcuts": { + "label": "Raccourcis clavier", + "description": "Voir les raccourcis clavier disponibles", + "button": "Voir les raccourcis" + }, + "reset_settings": { + "label": "Réinitialiser les paramètres", + "description": "Restaurer tous les paramètres par défaut", + "button": "Réinitialiser" + }, + "export_settings": { + "label": "Exporter les paramètres", + "description": "Télécharger vos paramètres au format JSON", + "button": "Exporter" + }, + "import_settings": { + "label": "Importer les paramètres", + "description": "Charger les paramètres depuis un fichier JSON", + "button": "Importer" + } + } } } \ No newline at end of file diff --git a/stores/auth-store.ts b/stores/auth-store.ts index 2630340..85c0b20 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { JMAPClient } from '@/lib/jmap/client'; +import { useEmailStore } from './email-store'; interface AuthState { isAuthenticated: boolean; @@ -91,6 +92,18 @@ export const useAuthStore = create()( // Clear persisted storage localStorage.removeItem('auth-storage'); + + // Clear email store state + useEmailStore.setState({ + emails: [], + mailboxes: [], + selectedEmail: null, + selectedMailbox: "", + isLoading: false, + error: null, + searchQuery: "", + quota: null, + }); }, checkAuth: async () => { diff --git a/stores/email-store.ts b/stores/email-store.ts index f48fb23..b12589b 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { Email, Mailbox } from "@/lib/jmap/types"; import { JMAPClient } from "@/lib/jmap/client"; +import { useSettingsStore } from "@/stores/settings-store"; interface EmailStore { emails: Email[]; @@ -105,7 +106,20 @@ export const useEmailStore = create((set, get) => ({ set({ isLoading: true, error: null }); try { const mailboxes = await client.getAllMailboxes(); - set({ mailboxes, isLoading: false }); + + // Auto-select inbox if no mailbox is currently selected + const currentSelectedMailbox = get().selectedMailbox; + if (!currentSelectedMailbox) { + // Find inbox from PRIMARY account (not shared accounts) + const inboxMailbox = mailboxes.find(m => m.role === 'inbox' && !m.isShared); + if (inboxMailbox) { + set({ mailboxes, selectedMailbox: inboxMailbox.id, isLoading: false }); + } else { + set({ mailboxes, isLoading: false }); + } + } else { + set({ mailboxes, isLoading: false }); + } } catch (error) { set({ error: error instanceof Error ? error.message : "Failed to fetch mailboxes", @@ -127,7 +141,10 @@ export const useEmailStore = create((set, get) => ({ // Use originalId for JMAP queries (shared mailboxes use namespaced IDs in the store) const jmapMailboxId = mailbox?.originalId || targetMailboxId; - const result = await client.getEmails(jmapMailboxId, accountId, 50, 0); + // Get emails per page from settings + const emailsPerPage = useSettingsStore.getState().emailsPerPage; + + const result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, 0); set({ emails: result.emails, hasMoreEmails: result.hasMore, @@ -162,7 +179,10 @@ export const useEmailStore = create((set, get) => ({ // Use originalId for JMAP queries (shared mailboxes use namespaced IDs in the store) const jmapMailboxId = mailbox?.originalId || selectedMailbox; - const result = await client.getEmails(jmapMailboxId, accountId, 50, emails.length); + // Get emails per page from settings + const emailsPerPage = useSettingsStore.getState().emailsPerPage; + + const result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, emails.length); set({ emails: [...emails, ...result.emails], diff --git a/stores/settings-store.ts b/stores/settings-store.ts new file mode 100644 index 0000000..ba16793 --- /dev/null +++ b/stores/settings-store.ts @@ -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: ( + 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()( + 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); +}