compose mime

This commit is contained in:
alma 2025-04-25 09:38:06 +02:00
parent b025baa6c1
commit 96d6d50e50

View File

@ -42,21 +42,16 @@ export interface Account {
} }
export interface Email { export interface Email {
id: number; id: string;
accountId: number;
from: string; from: string;
fromName: string; fromName?: string;
to: string; to: string;
subject: string; subject: string;
body: string; content: string;
date: string; date: string;
read: boolean; read: boolean;
starred: boolean; starred: boolean;
folder: string; attachments?: { name: string; url: string }[];
cc?: string;
bcc?: string;
flags?: string[];
raw: string;
} }
interface Attachment { interface Attachment {
@ -113,7 +108,7 @@ function EmailContent({ email }: { email: Email }) {
setIsLoading(true); setIsLoading(true);
try { try {
if (!email.body) { if (!email.content) {
if (mounted) { if (mounted) {
setContent(<div className="text-gray-500">No content available</div>); setContent(<div className="text-gray-500">No content available</div>);
setIsLoading(false); setIsLoading(false);
@ -121,7 +116,7 @@ function EmailContent({ email }: { email: Email }) {
return; return;
} }
const formattedEmail = email.body.trim(); const formattedEmail = email.content.trim();
if (!formattedEmail) { if (!formattedEmail) {
if (mounted) { if (mounted) {
setContent(<div className="text-gray-500">No content available</div>); setContent(<div className="text-gray-500">No content available</div>);
@ -167,7 +162,7 @@ function EmailContent({ email }: { email: Email }) {
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [email?.body]); }, [email?.content]);
if (isLoading) { if (isLoading) {
return ( return (
@ -263,12 +258,12 @@ function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-al
async function loadReplyContent() { async function loadReplyContent() {
try { try {
if (!email.body) { if (!email.content) {
if (mounted) setContent(''); if (mounted) setContent('');
return; return;
} }
const decoded = await decodeEmail(email.body); const decoded = await decodeEmail(email.content);
if (mounted) { if (mounted) {
let formattedContent = ''; let formattedContent = '';
@ -278,7 +273,7 @@ function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-al
<div class="forwarded-message"> <div class="forwarded-message">
<p>---------- Forwarded message ---------</p> <p>---------- Forwarded message ---------</p>
<p>From: ${decoded.from || ''}</p> <p>From: ${decoded.from || ''}</p>
<p>Date: ${formatDate(decoded.date)}</p> <p>Date: ${formatDate(decoded.date ? new Date(decoded.date) : null)}</p>
<p>Subject: ${decoded.subject || ''}</p> <p>Subject: ${decoded.subject || ''}</p>
<p>To: ${decoded.to || ''}</p> <p>To: ${decoded.to || ''}</p>
<br> <br>
@ -288,7 +283,7 @@ function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-al
} else { } else {
formattedContent = ` formattedContent = `
<div class="quoted-message"> <div class="quoted-message">
<p>On ${formatDate(decoded.date)}, ${decoded.from || ''} wrote:</p> <p>On ${formatDate(decoded.date ? new Date(decoded.date) : null)}, ${decoded.from || ''} wrote:</p>
<blockquote> <blockquote>
${decoded.html || `<pre>${decoded.text || ''}</pre>`} ${decoded.html || `<pre>${decoded.text || ''}</pre>`}
</blockquote> </blockquote>
@ -313,7 +308,7 @@ function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-al
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [email.body, type]); }, [email.content, type]);
if (error) { if (error) {
return <div className="text-red-500">{error}</div>; return <div className="text-red-500">{error}</div>;
@ -336,14 +331,14 @@ function EmailPreview({ email }: { email: Email }) {
let mounted = true; let mounted = true;
async function loadPreview() { async function loadPreview() {
if (!email?.body) { if (!email?.content) {
if (mounted) setPreview('No content available'); if (mounted) setPreview('No content available');
return; return;
} }
setIsLoading(true); setIsLoading(true);
try { try {
const decoded = await decodeEmail(email.body); const decoded = await decodeEmail(email.content);
if (mounted) { if (mounted) {
if (decoded.text) { if (decoded.text) {
setPreview(decoded.text.substring(0, 150) + '...'); setPreview(decoded.text.substring(0, 150) + '...');
@ -371,7 +366,7 @@ function EmailPreview({ email }: { email: Email }) {
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [email?.body]); }, [email?.content]);
if (isLoading) { if (isLoading) {
return <span className="text-gray-400">Loading preview...</span>; return <span className="text-gray-400">Loading preview...</span>;
@ -1078,45 +1073,51 @@ export default function CourrierPage() {
}, [availableFolders]); }, [availableFolders]);
// Update the email list item to match header checkbox alignment // Update the email list item to match header checkbox alignment
const renderEmailListItem = (email: Email) => { const renderEmailListItem = (email: Email) => (
return (
<div <div
key={email.id} key={email.id}
className={`flex items-center gap-3 px-4 py-2 hover:bg-gray-50/80 cursor-pointer ${ className={`p-3 hover:bg-gray-50/50 transition-colors cursor-pointer flex items-start gap-3 ${
selectedEmail?.id === email.id ? 'bg-blue-50/50' : '' selectedEmail?.id === email.id ? 'bg-gray-50/80' : ''
} ${!email.read ? 'bg-blue-50/20' : ''}`} }`}
onClick={() => handleEmailSelect(email.id)} onClick={() => handleEmailSelect(email.id)}
> >
<div className="flex-none pt-1">
<Checkbox <Checkbox
checked={selectedEmails.includes(email.id.toString())} checked={selectedEmails.includes(email.id)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => toggleEmailSelection(email.id)}
const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent<HTMLInputElement>;
handleEmailCheckbox(e, email.id);
}}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="mt-0.5" className="mt-0.5"
/> />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2 mb-1">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}> <span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
{email.fromName || email.from} {email.fromName || email.from}
</span> </span>
{!email.read && (
<span className="w-1.5 h-1.5 bg-blue-600 rounded-full flex-shrink-0"></span>
)}
</div> </div>
<span className="text-xs text-gray-500 whitespace-nowrap"> <span className="text-xs text-gray-500 whitespace-nowrap">
{formatDate(new Date(email.date))} {formatDate(new Date(email.date))}
</span> </span>
</div> </div>
<h3 className="text-sm text-gray-900 truncate"> <h3 className={`text-sm truncate mb-0.5 ${!email.read ? 'text-gray-900' : 'text-gray-600'}`}>
{email.subject || '(No subject)'} {email.subject || '(No subject)'}
</h3> </h3>
<div className="text-xs text-gray-500 line-clamp-2"> <EmailPreview email={email} />
{generateEmailPreview(email)}
</div> </div>
<div className="flex-none flex items-center gap-1">
{email.starred && (
<Star className="h-4 w-4 text-yellow-400 fill-yellow-400" />
)}
{email.attachments && email.attachments.length > 0 && (
<Paperclip className="h-4 w-4 text-gray-400" />
)}
</div> </div>
</div> </div>
); );
};
const handleMailboxChange = async (newMailbox: string) => { const handleMailboxChange = async (newMailbox: string) => {
setCurrentView(newMailbox); setCurrentView(newMailbox);
@ -1355,6 +1356,14 @@ export default function CourrierPage() {
</AlertDialog> </AlertDialog>
); );
const toggleEmailSelection = (emailId: string) => {
setSelectedEmails((prev) =>
prev.includes(emailId)
? prev.filter((id) => id !== emailId)
: [...prev, emailId]
);
};
if (error) { if (error) {
return ( return (
<div className="flex h-[calc(100vh-theme(spacing.12))] items-center justify-center bg-gray-100 mt-12"> <div className="flex h-[calc(100vh-theme(spacing.12))] items-center justify-center bg-gray-100 mt-12">