Neah version mail design fix 5

This commit is contained in:
alma 2025-04-16 18:48:43 +02:00
parent b0dc6f9aed
commit 4b6a901442

View File

@ -666,6 +666,7 @@ export default function MailPage() {
} }
}; };
// Add these improved handlers
const handleEmailCheckbox = (e: React.ChangeEvent<HTMLInputElement>, emailId: number) => { const handleEmailCheckbox = (e: React.ChangeEvent<HTMLInputElement>, emailId: number) => {
e.stopPropagation(); e.stopPropagation();
if (e.target.checked) { if (e.target.checked) {
@ -675,389 +676,54 @@ export default function MailPage() {
} }
}; };
// Update the toggleStarred function // Handles marking an individual email as read/unread
const toggleStarred = async (emailId: number, e: React.MouseEvent) => { const handleMarkAsRead = (emailId: string, isRead: boolean) => {
e.stopPropagation(); setEmails(emails.map(email =>
email.id.toString() === emailId ? { ...email, read: isRead } : email
// Update the email in state ));
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, starred: !email.starred }
: email
)
);
try {
const response = await fetch('/api/mail/toggle-star', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emailId })
});
if (!response.ok) {
throw new Error('Failed to toggle star');
}
} catch (error) {
console.error('Error toggling star:', error);
// Revert the change if the server request fails
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, starred: !email.starred }
: email
)
);
}
}; };
// Handle reply with MIME encoding // Handles bulk actions for selected emails
const handleReply = async (type: 'reply' | 'replyAll' | 'forward') => { const handleBulkAction = (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => {
const selectedEmailData = getSelectedEmail(); selectedEmails.forEach(emailId => {
if (!selectedEmailData) return; const email = emails.find(e => e.id.toString() === emailId);
if (email) {
setShowCompose(true); switch (action) {
const subject = `${type === 'forward' ? 'Fwd: ' : 'Re: '}${selectedEmailData.subject}`; case 'delete':
let to = ''; setEmails(emails.filter(e => e.id.toString() !== emailId));
let cc = '';
let content = '';
// Parse the original email content using MIME decoder
const parsedEmail = parseFullEmail(selectedEmailData.body);
const decodedBody = parsedEmail?.text || parsedEmail?.html || selectedEmailData.body;
// Format the date properly
const emailDate = new Date(selectedEmailData.date).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
switch (type) {
case 'reply':
to = selectedEmailData.from;
content = `\n\nOn ${emailDate}, ${selectedEmailData.fromName} wrote:\n> ${decodedBody.split('\n').join('\n> ')}`;
break; break;
case 'mark-read':
case 'replyAll': handleMarkAsRead(emailId, true);
to = selectedEmailData.from;
// Get our email address from the selected account
const ourEmail = accounts.find(acc => acc.id === selectedEmailData.accountId)?.email || '';
// Handle CC addresses
const ccList = new Set<string>();
// Add original TO recipients (except ourselves and the person we're replying to)
selectedEmailData.to.split(',')
.map(addr => addr.trim())
.filter(addr => addr !== ourEmail && addr !== selectedEmailData.from)
.forEach(addr => ccList.add(addr));
// Add original CC recipients (if any)
if (selectedEmailData.cc) {
selectedEmailData.cc.split(',')
.map(addr => addr.trim())
.filter(addr => addr !== ourEmail && addr !== selectedEmailData.from)
.forEach(addr => ccList.add(addr));
}
// Convert Set to string
cc = Array.from(ccList).join(', ');
// If we have CC recipients, show the CC field
if (cc) {
setShowCc(true);
}
content = `\n\nOn ${emailDate}, ${selectedEmailData.fromName} wrote:\n> ${decodedBody.split('\n').join('\n> ')}`;
break; break;
case 'mark-unread':
case 'forward': handleMarkAsRead(emailId, false);
// Safely handle attachments break;
const attachments = parsedEmail?.attachments || []; case 'archive':
const attachmentInfo = attachments.length > 0 setEmails(emails.map(e =>
? '\n\n-------- Attachments --------\n' + e.id.toString() === emailId ? { ...e, folder: 'Archive' } : e
attachments.map(att => att.filename).join('\n') ));
: '';
content = `\n\n---------- Forwarded message ----------\n` +
`From: ${selectedEmailData.fromName} <${selectedEmailData.from}>\n` +
`Date: ${emailDate}\n` +
`Subject: ${selectedEmailData.subject}\n` +
`To: ${selectedEmailData.to}\n` +
(selectedEmailData.cc ? `Cc: ${selectedEmailData.cc}\n` : '') +
`\n\n${decodedBody}${attachmentInfo}`;
// Handle attachments if present
if (attachments.length > 0) {
handleForwardedAttachments(attachments);
}
break; break;
} }
// Set the form state
setComposeSubject(subject);
setComposeTo(to);
setComposeCc(cc);
setComposeBody(content);
};
// Handle forwarded attachments
const handleForwardedAttachments = (attachments: Array<{
filename: string;
contentType: string;
encoding: string;
content: string;
}>) => {
try {
// Store attachment information in state
setAttachments(attachments.map(att => ({
name: att.filename,
type: att.contentType,
content: att.content,
encoding: att.encoding
})));
} catch (error) {
console.error('Error handling attachments:', error);
} }
};
// Modified send function to handle MIME encoding
const handleSend = async () => {
try {
// Create multipart message
const boundary = `----=_Part_${Date.now()}`;
let mimeContent = [
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
'Content-Type: text/plain; charset=utf-8',
'Content-Transfer-Encoding: 7bit',
'',
composeBody,
''
];
// Add attachments if any
if (attachments && attachments.length > 0) {
for (const attachment of attachments) {
mimeContent = mimeContent.concat([
`--${boundary}`,
`Content-Type: ${attachment.type}`,
`Content-Transfer-Encoding: ${attachment.encoding}`,
`Content-Disposition: attachment; filename="${attachment.name}"`,
'',
attachment.content,
''
]);
}
}
// Close the multipart message
mimeContent.push(`--${boundary}--`);
// Send the email with MIME content
await fetch('/api/mail/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: composeTo,
cc: composeCc,
bcc: composeBcc,
subject: composeSubject,
body: mimeContent.join('\r\n'),
attachments: attachments
})
}); });
// Clear the form
setShowCompose(false);
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setShowCc(false);
setShowBcc(false);
setAttachments([]);
} catch (error) {
console.error('Error sending email:', error);
}
};
const handleBulkDelete = () => {
setDeleteType('emails');
setShowDeleteConfirm(true);
};
const handleDeleteConfirm = async () => {
try {
if (selectedEmails.length > 0) {
await fetch('/api/mail/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emailIds: selectedEmails })
});
setEmails(emails.filter(email => !selectedEmails.includes(email.id.toString())));
setSelectedEmails([]); setSelectedEmails([]);
setShowBulkActions(false);
}
} catch (error) {
console.error('Error deleting emails:', error);
}
setShowDeleteConfirm(false);
};
const handleAccountAction = (accountId: number, action: 'edit' | 'delete') => {
setShowAccountActions(null);
if (action === 'delete') {
setDeleteType('account');
setItemToDelete(accountId);
setShowDeleteConfirm(true);
}
}; };
const toggleSelectAll = () => { const toggleSelectAll = () => {
if (selectedEmails.length === emails.length) { if (selectedEmails.length === emails.length) {
setSelectedEmails([]); setSelectedEmails([]);
setShowBulkActions(false);
} else { } else {
const allEmailIds = emails.map(email => email.id.toString()); setSelectedEmails(emails.map(email => email.id.toString()));
setSelectedEmails(allEmailIds);
setShowBulkActions(true);
} }
}; };
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => { // Update the email list header
if (e.target.files) { const renderEmailListHeader = () => (
setAttachments(Array.from(e.target.files).map(file => ({
name: file.name,
type: file.type,
content: URL.createObjectURL(file),
encoding: 'base64'
})));
}
};
// Add debug logging to help track the filtering
useEffect(() => {
console.log('Current view:', currentView);
console.log('Total emails:', emails.length);
console.log('Filter criteria:', {
starred: emails.filter(e => e.starred).length,
sent: emails.filter(e => e.folder === 'Sent').length,
trash: emails.filter(e => e.folder === 'Trash').length,
spam: emails.filter(e => e.folder === 'Spam').length,
drafts: emails.filter(e => e.folder === 'Drafts').length,
archives: emails.filter(e => e.folder === 'Archives' || e.folder === 'Archive').length
});
}, [currentView, emails]);
// Add a function to move to trash
const moveToTrash = async (emailId: number) => {
// Update the email in state
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, read: true, starred: false, folder: 'Trash' }
: email
)
);
try {
const response = await fetch('/api/mail/move-to-trash', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emailId })
});
if (!response.ok) {
throw new Error('Failed to move to trash');
}
} catch (error) {
console.error('Error moving to trash:', error);
// Revert the change if the server request fails
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, read: false, starred: true, folder: 'starred' }
: email
)
);
}
};
// Add this debug component to help us see what's happening
const DebugInfo = () => {
if (process.env.NODE_ENV !== 'development') return null;
return (
<div className="fixed bottom-4 right-4 bg-black/80 text-white p-4 rounded-lg text-xs">
<div>Current view: {currentView}</div>
<div>Total emails: {emails.length}</div>
<div>Categories present: {
[...new Set(emails.map(e => e.folder))].join(', ')
}</div>
<div>Flags present: {
[...new Set(emails.flatMap(e => e.flags || []))].join(', ')
}</div>
</div>
);
};
// Update sidebar items when available folders change
useEffect(() => {
if (availableFolders.length > 0) {
const newItems = [
...initialSidebarItems,
...availableFolders
.filter(folder => !['INBOX'].includes(folder)) // Exclude folders already in initial items
.map(folder => ({
view: folder as MailFolder,
label: folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(),
icon: getFolderIcon(folder),
folder: folder
}))
];
setSidebarItems(newItems);
}
}, [availableFolders]);
// Sort emails by date (most recent first)
const sortedEmails = useMemo(() => {
return [...emails].sort((a, b) => {
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
}, [emails]);
// Add infinite scroll handler
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const target = e.currentTarget;
if (
target.scrollHeight - target.scrollTop === target.clientHeight &&
!isLoadingMore &&
hasMore
) {
setPage(prev => prev + 1);
loadEmails(true);
}
}, [isLoadingMore, hasMore]);
// Render the email list using sorted emails
const renderEmailList = () => (
<div className="w-[320px] bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col">
{/* Email list header */}
<div className="p-4 border-b border-gray-100"> <div className="p-4 border-b border-gray-100">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Checkbox <Checkbox
checked={selectedEmails.length > 0 && selectedEmails.length === emails.length} checked={emails.length > 0 && selectedEmails.length === emails.length}
onCheckedChange={toggleSelectAll} onCheckedChange={toggleSelectAll}
className="mt-0.5" className="mt-0.5"
/> />
@ -1075,15 +741,10 @@ export default function MailPage() {
size="sm" size="sm"
className="text-gray-600 hover:text-gray-900" className="text-gray-600 hover:text-gray-900"
onClick={() => { onClick={() => {
// Mark as read/unread const someUnread = emails.some(email =>
const isUnread = emails.some(email =>
selectedEmails.includes(email.id.toString()) && !email.read selectedEmails.includes(email.id.toString()) && !email.read
); );
setEmails(emails.map(email => handleBulkAction(someUnread ? 'mark-read' : 'mark-unread');
selectedEmails.includes(email.id.toString())
? { ...email, read: !isUnread }
: email
));
}} }}
> >
<EyeOff className="h-4 w-4 mr-1" /> <EyeOff className="h-4 w-4 mr-1" />
@ -1096,24 +757,16 @@ export default function MailPage() {
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-gray-600 hover:text-gray-900" className="text-gray-600 hover:text-gray-900"
onClick={() => { onClick={() => handleBulkAction('archive')}
// Move to folder (Archive)
setEmails(emails.map(email =>
selectedEmails.includes(email.id.toString())
? { ...email, folder: 'Archive' }
: email
));
setSelectedEmails([]);
}}
> >
<Archive className="h-4 w-4 mr-1" /> <Archive className="h-4 w-4 mr-1" />
Move to Archive
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-red-600 hover:text-red-700" className="text-red-600 hover:text-red-700"
onClick={handleBulkDelete} onClick={() => handleBulkAction('delete')}
> >
<Trash2 className="h-4 w-4 mr-1" /> <Trash2 className="h-4 w-4 mr-1" />
Delete Delete
@ -1126,24 +779,10 @@ export default function MailPage() {
</div> </div>
</div> </div>
</div> </div>
);
{/* Email list with scroll handler */} // Update the email list item checkbox
<div const renderEmailListItem = (email: Email) => (
className="flex-1 overflow-y-auto"
onScroll={handleScroll}
>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : emails.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
<Mail className="h-8 w-8 text-gray-400 mb-2" />
<p className="text-gray-500 text-sm">No emails in this folder</p>
</div>
) : (
<div className="divide-y divide-gray-100">
{sortedEmails.map((email) => (
<div <div
key={email.id} key={email.id}
className={`flex items-start gap-3 p-2 hover:bg-gray-50/80 cursor-pointer ${ className={`flex items-start gap-3 p-2 hover:bg-gray-50/80 cursor-pointer ${
@ -1153,14 +792,11 @@ export default function MailPage() {
> >
<Checkbox <Checkbox
checked={selectedEmails.includes(email.id.toString())} checked={selectedEmails.includes(email.id.toString())}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
if (checked) { const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent<HTMLInputElement>;
setSelectedEmails([...selectedEmails, email.id.toString()]); handleEmailCheckbox(e, email.id);
} else {
setSelectedEmails(selectedEmails.filter(id => id !== email.id.toString()));
}
}} }}
onClick={(e) => e.stopPropagation()}
className="mt-1" className="mt-1"
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@ -1256,7 +892,31 @@ export default function MailPage() {
</div> </div>
</div> </div>
</div> </div>
))} );
// Render the email list using sorted emails
const renderEmailList = () => (
<div className="w-[320px] bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col">
{/* Email list header */}
{renderEmailListHeader()}
{/* Email list with scroll handler */}
<div
className="flex-1 overflow-y-auto"
onScroll={handleScroll}
>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : emails.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
<Mail className="h-8 w-8 text-gray-400 mb-2" />
<p className="text-gray-500 text-sm">No emails in this folder</p>
</div>
) : (
<div className="divide-y divide-gray-100">
{emails.map((email) => renderEmailListItem(email))}
{isLoadingMore && ( {isLoadingMore && (
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-blue-500"></div> <div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-blue-500"></div>