'use client'; import { useEffect, useState, useCallback } from 'react'; import { useTranslations } from 'next-intl'; import { X, Mail, Pencil, Trash2, Plus, AlertTriangle, Star } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { IdentityForm } from './identity-form'; import { useIdentityStore } from '@/stores/identity-store'; import { useAuthStore } from '@/stores/auth-store'; import { useAccountStore } from '@/stores/account-store'; import { useSettingsStore } from '@/stores/settings-store'; function useSyncIdentities() { const syncIdentities = useAuthStore((state) => state.syncIdentities); return syncIdentities; } function useRefreshIdentities() { return useAuthStore((state) => state.refreshIdentities); } import type { Identity, EmailAddress } from '@/lib/jmap/types'; import { toast } from '@/stores/toast-store'; import { useFocusTrap } from '@/hooks/use-focus-trap'; import { useConfirmDialog } from '@/hooks/use-confirm-dialog'; function emailMatchesUsername(email: string, username: string): boolean { if (email === username) return true; if (!username.includes('@') && email.split('@')[0] === username) return true; return false; } interface IdentityFormData { name: string; email: string; replyTo?: EmailAddress[] | null; bcc?: EmailAddress[] | null; textSignature?: string | null; htmlSignature?: string | null; } interface IdentityManagerModalProps { isOpen: boolean; onClose: () => void; } export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalProps) { const t = useTranslations('identities'); const tNotif = useTranslations('notifications'); const client = useAuthStore((state) => state.client); const identities = useIdentityStore((state) => state.identities); const _preferredPrimaryId = useIdentityStore((state) => state.preferredPrimaryId); const setPreferredPrimary = useIdentityStore((state) => state.setPreferredPrimary); const syncIdentities = useSyncIdentities(); const refreshIdentitiesFromServer = useRefreshIdentities(); // Refresh identities from server whenever the modal is opened useEffect(() => { if (isOpen) { refreshIdentitiesFromServer(); } }, [isOpen, refreshIdentitiesFromServer]); const [editingId, setEditingId] = useState(null); const [isCreating, setIsCreating] = useState(false); const [deletingId, setDeletingId] = useState(null); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); // Re-fetch all identities from server and update stores const refreshIdentities = useCallback(async () => { if (!client) return; try { const serverIdentities = await client.getIdentities(); const username = useAuthStore.getState().username; const preferredPrimaryId = useIdentityStore.getState().preferredPrimaryId; const sorted = [...serverIdentities].sort((a, b) => { const aMatch = emailMatchesUsername(a.email, username || ''); const bMatch = emailMatchesUsername(b.email, username || ''); if (aMatch && !bMatch) return -1; if (!aMatch && bMatch) return 1; if (aMatch && bMatch) { if (!a.mayDelete && b.mayDelete) return -1; if (a.mayDelete && !b.mayDelete) return 1; } return 0; }); // Move preferred primary to front if set if (preferredPrimaryId) { const idx = sorted.findIndex((id) => id.id === preferredPrimaryId); if (idx > 0) { const [preferred] = sorted.splice(idx, 1); sorted.unshift(preferred); } } useIdentityStore.getState().setIdentities(sorted); syncIdentities(); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to refresh identities'; toast.error(message); } }, [client, syncIdentities]); // Focus trap with Escape handling const modalRef = useFocusTrap({ isActive: isOpen, onEscape: () => { if (isCreating || editingId) { setIsCreating(false); setEditingId(null); } else { onClose(); } }, restoreFocus: true, }); // Close on click outside (but not when ConfirmDialog is open) useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (confirmDialogProps.isOpen) return; if (modalRef.current && !modalRef.current.contains(e.target as Node)) { onClose(); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isOpen, onClose, modalRef, confirmDialogProps.isOpen]); const handleCreate = useCallback(async (data: IdentityFormData) => { if (!client) return; try { await client.createIdentity( data.name, data.email, data.replyTo, data.bcc, data.textSignature, data.htmlSignature ); await refreshIdentities(); setIsCreating(false); toast.success(tNotif('identity_created')); } catch (error) { const message = error instanceof Error ? error.message : t('validation_errors.unknown_error'); toast.error(tNotif('identity_create_failed', { error: message })); throw error; } }, [client, refreshIdentities, t, tNotif]); const handleUpdate = useCallback(async (identity: Identity, data: IdentityFormData) => { if (!client) return; try { await client.updateIdentity(identity.id, { name: data.name, replyTo: data.replyTo, bcc: data.bcc, textSignature: data.textSignature, htmlSignature: data.htmlSignature, }); await refreshIdentities(); setEditingId(null); toast.success(tNotif('identity_updated')); } catch (error) { const message = error instanceof Error ? error.message : t('validation_errors.unknown_error'); toast.error(tNotif('identity_update_failed', { error: message })); throw error; } }, [client, refreshIdentities, t, tNotif]); const handleDelete = useCallback(async (identity: Identity) => { if (!client) return; if (!identity.mayDelete) { toast.error(t('cannot_delete')); return; } const confirmed = await confirmDialog({ title: t('delete_confirm_title'), message: t('delete_confirm'), confirmText: t('delete_button'), variant: "destructive", }); if (!confirmed) return; setDeletingId(identity.id); try { await client.deleteIdentity(identity.id); await refreshIdentities(); toast.success(tNotif('identity_deleted')); } catch (error) { const message = error instanceof Error ? error.message : t('validation_errors.unknown_error'); toast.error(tNotif('identity_delete_failed', { error: message })); } finally { setDeletingId(null); } }, [client, refreshIdentities, t, tNotif, confirmDialog]); const handleSetPrimary = useCallback((identity: Identity) => { setPreferredPrimary(identity.id); // Persist the choice per account in the synced settings store so it // survives clearing site data, follows the user across devices, and shows // up in exported settings (issue #507). JMAP identity ids are account- // scoped, so the default is keyed by the active account. const activeAccountId = useAccountStore.getState().activeAccountId; if (activeAccountId) { const current = useSettingsStore.getState().preferredIdentityIds; useSettingsStore.getState().updateSetting('preferredIdentityIds', { ...current, [activeAccountId]: identity.id, }); } // Re-sort: move the preferred identity to the front const reordered = [identity, ...identities.filter((id) => id.id !== identity.id)]; useIdentityStore.getState().setIdentities(reordered); syncIdentities(); toast.success(tNotif('identity_set_primary')); }, [identities, setPreferredPrimary, syncIdentities, tNotif]); if (!isOpen) return null; return (
{/* Header */}

{t('modal_title')}

{/* Content */}
{/* Create New Form */} {isCreating && (

{t('create_new')}

setIsCreating(false)} />
)} {/* Create Button */} {!isCreating && !editingId && ( )} {/* Identities List */}
{identities.map((identity) => (
{editingId === identity.id ? (

{t('edit_identity')}

handleUpdate(identity, data)} onCancel={() => setEditingId(null)} />
) : (

{identity.name}

{identities[0]?.id === identity.id && ( {t('primary_identity')} )}

{identity.email}

{/* Additional Info */}
{identity.replyTo && identity.replyTo.length > 0 && (

{t('display.reply_to')} {identity.replyTo.map((a) => a.email).join(', ')}

)} {identity.bcc && identity.bcc.length > 0 && (

{t('display.bcc')} {identity.bcc.map((a) => a.email).join(', ')}

)} {identity.textSignature && (

{t('display.signature')} {identity.textSignature}

)}
{/* Actions */}
{identities[0]?.id !== identity.id && identities.length > 1 && ( )}
{!identity.mayDelete && (

{t('cannot_delete')}

)}
)}
))} {identities.length === 0 && !isCreating && (

{t('no_identities')}

)}
); }