feat: enhance identity management with identity refresh functionality and improved modal behavior
This commit is contained in:
@@ -664,13 +664,24 @@ export function EmailComposer({
|
|||||||
finalBody = body + '\n\n-- \n' + currentIdentity.textSignature;
|
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 `<br><br>-- <br>${sanitizeEmailHtml(currentIdentity.htmlSignature)}`;
|
||||||
|
}
|
||||||
|
if (currentIdentity?.textSignature) {
|
||||||
|
return `<br><br>-- <br>${currentIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build HTML body
|
||||||
let finalHtmlBody: string | undefined;
|
let finalHtmlBody: string | undefined;
|
||||||
|
const signatureHtml = buildSignatureHtml();
|
||||||
|
|
||||||
if (replyTo?.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
|
if (replyTo?.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
|
||||||
|
// Reply/forward with original HTML content
|
||||||
const escapedBody = body.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
const escapedBody = body.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
||||||
const signatureHtml = currentIdentity?.textSignature
|
|
||||||
? `<br><br>-- <br>${currentIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`
|
|
||||||
: '';
|
|
||||||
const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : '';
|
const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : '';
|
||||||
const fromAddr = replyTo.from?.[0];
|
const fromAddr = replyTo.from?.[0];
|
||||||
const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
|
const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
|
||||||
@@ -679,6 +690,10 @@ export function EmailComposer({
|
|||||||
: `On ${date}, ${fromStr} wrote:<br>`;
|
: `On ${date}, ${fromStr} wrote:<br>`;
|
||||||
|
|
||||||
finalHtmlBody = `<div>${escapedBody}</div>${signatureHtml}<br><div><div>${quoteHeader}</div><blockquote style="margin:0 0 0 0.8ex;border-left:2px solid #ccc;padding-left:1ex">${replyTo.htmlBody}</blockquote></div>`;
|
finalHtmlBody = `<div>${escapedBody}</div>${signatureHtml}<br><div><div>${quoteHeader}</div><blockquote style="margin:0 0 0 0.8ex;border-left:2px solid #ccc;padding-left:1ex">${replyTo.htmlBody}</blockquote></div>`;
|
||||||
|
} else if (signatureHtml) {
|
||||||
|
// New compose or plain-text reply — include HTML body with signature
|
||||||
|
const escapedBody = body.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
||||||
|
finalHtmlBody = `<div>${escapedBody}</div>${signatureHtml}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
const tNotif = useTranslations('notifications');
|
const tNotif = useTranslations('notifications');
|
||||||
|
|
||||||
const client = useAuthStore((state) => state.client);
|
const client = useAuthStore((state) => state.client);
|
||||||
const { identities, addIdentity, updateIdentityLocal, removeIdentity } = useIdentityStore();
|
const identities = useIdentityStore((state) => state.identities);
|
||||||
const syncIdentities = useSyncIdentities();
|
const syncIdentities = useSyncIdentities();
|
||||||
|
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
@@ -46,6 +46,25 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
|
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
|
// Focus trap with Escape handling
|
||||||
const modalRef = useFocusTrap({
|
const modalRef = useFocusTrap({
|
||||||
isActive: isOpen,
|
isActive: isOpen,
|
||||||
@@ -60,9 +79,10 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
restoreFocus: true,
|
restoreFocus: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on click outside
|
// Close on click outside (but not when ConfirmDialog is open)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (confirmDialogProps.isOpen) return;
|
||||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -72,13 +92,13 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}
|
}
|
||||||
}, [isOpen, onClose, modalRef]);
|
}, [isOpen, onClose, modalRef, confirmDialogProps.isOpen]);
|
||||||
|
|
||||||
const handleCreate = useCallback(async (data: IdentityFormData) => {
|
const handleCreate = useCallback(async (data: IdentityFormData) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newIdentity = await client.createIdentity(
|
await client.createIdentity(
|
||||||
data.name,
|
data.name,
|
||||||
data.email,
|
data.email,
|
||||||
data.replyTo,
|
data.replyTo,
|
||||||
@@ -87,8 +107,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
data.htmlSignature
|
data.htmlSignature
|
||||||
);
|
);
|
||||||
|
|
||||||
addIdentity(newIdentity);
|
await refreshIdentities();
|
||||||
syncIdentities();
|
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
toast.success(tNotif('identity_created'));
|
toast.success(tNotif('identity_created'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -96,7 +115,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
toast.error(tNotif('identity_create_failed', { error: message }));
|
toast.error(tNotif('identity_create_failed', { error: message }));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [client, addIdentity, t, tNotif]);
|
}, [client, refreshIdentities, t, tNotif]);
|
||||||
|
|
||||||
const handleUpdate = useCallback(async (identity: Identity, data: IdentityFormData) => {
|
const handleUpdate = useCallback(async (identity: Identity, data: IdentityFormData) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
@@ -110,8 +129,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
htmlSignature: data.htmlSignature,
|
htmlSignature: data.htmlSignature,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateIdentityLocal(identity.id, data);
|
await refreshIdentities();
|
||||||
syncIdentities();
|
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
toast.success(tNotif('identity_updated'));
|
toast.success(tNotif('identity_updated'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -119,7 +137,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
toast.error(tNotif('identity_update_failed', { error: message }));
|
toast.error(tNotif('identity_update_failed', { error: message }));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [client, updateIdentityLocal, t, tNotif]);
|
}, [client, refreshIdentities, t, tNotif]);
|
||||||
|
|
||||||
const handleDelete = useCallback(async (identity: Identity) => {
|
const handleDelete = useCallback(async (identity: Identity) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
@@ -140,8 +158,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await client.deleteIdentity(identity.id);
|
await client.deleteIdentity(identity.id);
|
||||||
removeIdentity(identity.id);
|
await refreshIdentities();
|
||||||
syncIdentities();
|
|
||||||
toast.success(tNotif('identity_deleted'));
|
toast.success(tNotif('identity_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : t('validation_errors.unknown_error');
|
const message = error instanceof Error ? error.message : t('validation_errors.unknown_error');
|
||||||
@@ -149,7 +166,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
|
|||||||
} finally {
|
} finally {
|
||||||
setDeletingId(null);
|
setDeletingId(null);
|
||||||
}
|
}
|
||||||
}, [client, removeIdentity, t, tNotif, confirmDialog]);
|
}, [client, refreshIdentities, t, tNotif, confirmDialog]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user