From 3186198fad190011f7147762e544d0729bd56767 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:50:35 +0100 Subject: [PATCH] feat: enhance identity management with identity refresh functionality and improved modal behavior --- components/email/email-composer.tsx | 23 ++++++++-- .../identity/identity-manager-modal.tsx | 43 +++++++++++++------ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index cd7e090d..01e1bbbd 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -664,13 +664,24 @@ export function EmailComposer({ finalBody = body + '\n\n-- \n' + currentIdentity.textSignature; } - // Build HTML body when replying/forwarding with original HTML content + // Build HTML signature block (prefer htmlSignature, fall back to escaped textSignature) + const buildSignatureHtml = (): string => { + if (currentIdentity?.htmlSignature) { + return `

--
${sanitizeEmailHtml(currentIdentity.htmlSignature)}`; + } + if (currentIdentity?.textSignature) { + return `

--
${currentIdentity.textSignature.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}`; + } + return ''; + }; + + // Build HTML body let finalHtmlBody: string | undefined; + const signatureHtml = buildSignatureHtml(); + if (replyTo?.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) { + // Reply/forward with original HTML content const escapedBody = body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
'); - const signatureHtml = currentIdentity?.textSignature - ? `

--
${currentIdentity.textSignature.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}` - : ''; const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ''; const fromAddr = replyTo.from?.[0]; const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown'); @@ -679,6 +690,10 @@ export function EmailComposer({ : `On ${date}, ${fromStr} wrote:
`; finalHtmlBody = `
${escapedBody}
${signatureHtml}
${quoteHeader}
${replyTo.htmlBody}
`; + } else if (signatureHtml) { + // New compose or plain-text reply — include HTML body with signature + const escapedBody = body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
'); + finalHtmlBody = `
${escapedBody}
${signatureHtml}`; } try { diff --git a/components/identity/identity-manager-modal.tsx b/components/identity/identity-manager-modal.tsx index 7125f10c..e5479e39 100644 --- a/components/identity/identity-manager-modal.tsx +++ b/components/identity/identity-manager-modal.tsx @@ -38,7 +38,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr const tNotif = useTranslations('notifications'); const client = useAuthStore((state) => state.client); - const { identities, addIdentity, updateIdentityLocal, removeIdentity } = useIdentityStore(); + const identities = useIdentityStore((state) => state.identities); const syncIdentities = useSyncIdentities(); const [editingId, setEditingId] = useState(null); @@ -46,6 +46,25 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr 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 sorted = [...serverIdentities].sort((a, b) => { + const aMatch = a.email === username ? -1 : 0; + const bMatch = b.email === username ? -1 : 0; + return aMatch - bMatch; + }); + 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, @@ -60,9 +79,10 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr restoreFocus: true, }); - // Close on click outside + // 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(); } @@ -72,13 +92,13 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } - }, [isOpen, onClose, modalRef]); + }, [isOpen, onClose, modalRef, confirmDialogProps.isOpen]); const handleCreate = useCallback(async (data: IdentityFormData) => { if (!client) return; try { - const newIdentity = await client.createIdentity( + await client.createIdentity( data.name, data.email, data.replyTo, @@ -87,8 +107,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr data.htmlSignature ); - addIdentity(newIdentity); - syncIdentities(); + await refreshIdentities(); setIsCreating(false); toast.success(tNotif('identity_created')); } catch (error) { @@ -96,7 +115,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr toast.error(tNotif('identity_create_failed', { error: message })); throw error; } - }, [client, addIdentity, t, tNotif]); + }, [client, refreshIdentities, t, tNotif]); const handleUpdate = useCallback(async (identity: Identity, data: IdentityFormData) => { if (!client) return; @@ -110,8 +129,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr htmlSignature: data.htmlSignature, }); - updateIdentityLocal(identity.id, data); - syncIdentities(); + await refreshIdentities(); setEditingId(null); toast.success(tNotif('identity_updated')); } catch (error) { @@ -119,7 +137,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr toast.error(tNotif('identity_update_failed', { error: message })); throw error; } - }, [client, updateIdentityLocal, t, tNotif]); + }, [client, refreshIdentities, t, tNotif]); const handleDelete = useCallback(async (identity: Identity) => { if (!client) return; @@ -140,8 +158,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr try { await client.deleteIdentity(identity.id); - removeIdentity(identity.id); - syncIdentities(); + await refreshIdentities(); toast.success(tNotif('identity_deleted')); } catch (error) { const message = error instanceof Error ? error.message : t('validation_errors.unknown_error'); @@ -149,7 +166,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr } finally { setDeletingId(null); } - }, [client, removeIdentity, t, tNotif, confirmDialog]); + }, [client, refreshIdentities, t, tNotif, confirmDialog]); if (!isOpen) return null;