From 4b6a9014420ba636eb5dd6ab77337c42495b0d6b Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 16 Apr 2025 18:48:43 +0200 Subject: [PATCH] Neah version mail design fix 5 --- app/mail/page.tsx | 748 +++++++++++++--------------------------------- 1 file changed, 204 insertions(+), 544 deletions(-) diff --git a/app/mail/page.tsx b/app/mail/page.tsx index 250b390..5f4cd66 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -666,6 +666,7 @@ export default function MailPage() { } }; + // Add these improved handlers const handleEmailCheckbox = (e: React.ChangeEvent, emailId: number) => { e.stopPropagation(); if (e.target.checked) { @@ -675,457 +676,229 @@ export default function MailPage() { } }; - // Update the toggleStarred function - const toggleStarred = async (emailId: number, e: React.MouseEvent) => { - e.stopPropagation(); - - // 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 - ) - ); - } + // Handles marking an individual email as read/unread + const handleMarkAsRead = (emailId: string, isRead: boolean) => { + setEmails(emails.map(email => + email.id.toString() === emailId ? { ...email, read: isRead } : email + )); }; - // Handle reply with MIME encoding - const handleReply = async (type: 'reply' | 'replyAll' | 'forward') => { - const selectedEmailData = getSelectedEmail(); - if (!selectedEmailData) return; - - setShowCompose(true); - const subject = `${type === 'forward' ? 'Fwd: ' : 'Re: '}${selectedEmailData.subject}`; - let to = ''; - 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 + // Handles bulk actions for selected emails + const handleBulkAction = (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => { + selectedEmails.forEach(emailId => { + const email = emails.find(e => e.id.toString() === emailId); + if (email) { + switch (action) { + case 'delete': + setEmails(emails.filter(e => e.id.toString() !== emailId)); + break; + case 'mark-read': + handleMarkAsRead(emailId, true); + break; + case 'mark-unread': + handleMarkAsRead(emailId, false); + break; + case 'archive': + setEmails(emails.map(e => + e.id.toString() === emailId ? { ...e, folder: 'Archive' } : e + )); + break; + } + } }); - - switch (type) { - case 'reply': - to = selectedEmailData.from; - content = `\n\nOn ${emailDate}, ${selectedEmailData.fromName} wrote:\n> ${decodedBody.split('\n').join('\n> ')}`; - break; - - case 'replyAll': - 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(); - - // 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; - - case 'forward': - // Safely handle attachments - const attachments = parsedEmail?.attachments || []; - const attachmentInfo = attachments.length > 0 - ? '\n\n-------- Attachments --------\n' + - 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; - } - - // 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([]); - 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); - } + setSelectedEmails([]); }; const toggleSelectAll = () => { if (selectedEmails.length === emails.length) { setSelectedEmails([]); - setShowBulkActions(false); } else { - const allEmailIds = emails.map(email => email.id.toString()); - setSelectedEmails(allEmailIds); - setShowBulkActions(true); + setSelectedEmails(emails.map(email => email.id.toString())); } }; - const handleFileAttachment = (e: React.ChangeEvent) => { - if (e.target.files) { - 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 ( -
-
Current view: {currentView}
-
Total emails: {emails.length}
-
Categories present: { - [...new Set(emails.map(e => e.folder))].join(', ') - }
-
Flags present: { - [...new Set(emails.flatMap(e => e.flags || []))].join(', ') - }
+ // Update the email list header + const renderEmailListHeader = () => ( +
+
+
+ 0 && selectedEmails.length === emails.length} + onCheckedChange={toggleSelectAll} + className="mt-0.5" + /> +

+ {currentView === 'INBOX' ? 'Inbox' : + currentView === 'starred' ? 'Starred' : + currentView.charAt(0).toUpperCase() + currentView.slice(1).toLowerCase()} +

+
+
+ {selectedEmails.length > 0 && ( + <> + + + + + )} + + {emails.length} emails + +
- ); - }; +
+ ); - // 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]); + // Update the email list item checkbox + const renderEmailListItem = (email: Email) => ( +
handleEmailSelect(email.id)} + > + { + const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent; + handleEmailCheckbox(e, email.id); + }} + onClick={(e) => e.stopPropagation()} + className="mt-1" + /> +
+
+
+ + {currentView === 'Sent' ? email.to : ( + (() => { + // Check if email is in format "name " + const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/); + if (fromMatch) { + // If we have both name and email, return just the name + return fromMatch[1].trim(); + } + // If it's just an email address, return the full email + return email.from; + })() + )} + +
+
+ + {formatDate(email.date)} + + +
+
+

+ {email.subject || '(No subject)'} +

+
+ {(() => { + // Get clean preview of the actual message content + let preview = ''; + try { + const parsed = parseFullEmail(email.body); + + // Try to get content from parsed email + preview = (parsed.text || parsed.html || '') + .replace(/]*>[\s\S]*?<\/style>/gi, '') + .replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/<[^>]+>/g, '') + .replace(/ |‌|»|«|>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); - // 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]); + // If no preview from parsed content, try direct body + if (!preview) { + preview = email.body + .replace(/<[^>]+>/g, '') + .replace(/ |‌|»|«|>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } - // Add infinite scroll handler - const handleScroll = useCallback((e: React.UIEvent) => { - const target = e.currentTarget; - if ( - target.scrollHeight - target.scrollTop === target.clientHeight && - !isLoadingMore && - hasMore - ) { - setPage(prev => prev + 1); - loadEmails(true); - } - }, [isLoadingMore, hasMore]); + // Remove email artifacts and clean up + preview = preview + .replace(/^>+/gm, '') + .replace(/Content-Type:[^\n]+/g, '') + .replace(/Content-Transfer-Encoding:[^\n]+/g, '') + .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') + .replace(/boundary=[^\n]+/g, '') + .replace(/charset=[^\n]+/g, '') + .replace(/[\r\n]+/g, ' ') + .trim(); + + // Take first 100 characters + preview = preview.substring(0, 100); + + // Try to end at a complete word + if (preview.length === 100) { + const lastSpace = preview.lastIndexOf(' '); + if (lastSpace > 80) { + preview = preview.substring(0, lastSpace); + } + preview += '...'; + } + + } catch (e) { + console.error('Error generating preview:', e); + preview = ''; + } + + return preview || 'No preview available'; + })()} +
+
+
+ ); // Render the email list using sorted emails const renderEmailList = () => (
{/* Email list header */} -
-
-
- 0 && selectedEmails.length === emails.length} - onCheckedChange={toggleSelectAll} - className="mt-0.5" - /> -

- {currentView === 'INBOX' ? 'Inbox' : - currentView === 'starred' ? 'Starred' : - currentView.charAt(0).toUpperCase() + currentView.slice(1).toLowerCase()} -

-
-
- {selectedEmails.length > 0 && ( - <> - - - - - )} - - {emails.length} emails - -
-
-
+ {renderEmailListHeader()} {/* Email list with scroll handler */}
) : (
- {sortedEmails.map((email) => ( -
handleEmailSelect(email.id)} - > - e.stopPropagation()} - onCheckedChange={(checked) => { - if (checked) { - setSelectedEmails([...selectedEmails, email.id.toString()]); - } else { - setSelectedEmails(selectedEmails.filter(id => id !== email.id.toString())); - } - }} - className="mt-1" - /> -
-
-
- - {currentView === 'Sent' ? email.to : ( - (() => { - // Check if email is in format "name " - const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/); - if (fromMatch) { - // If we have both name and email, return just the name - return fromMatch[1].trim(); - } - // If it's just an email address, return the full email - return email.from; - })() - )} - -
-
- - {formatDate(email.date)} - - -
-
-

- {email.subject || '(No subject)'} -

-
- {(() => { - // Get clean preview of the actual message content - let preview = ''; - try { - const parsed = parseFullEmail(email.body); - - // Try to get content from parsed email - preview = (parsed.text || parsed.html || '') - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/<[^>]+>/g, '') - .replace(/ |‌|»|«|>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - - // If no preview from parsed content, try direct body - if (!preview) { - preview = email.body - .replace(/<[^>]+>/g, '') - .replace(/ |‌|»|«|>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - } - - // Remove email artifacts and clean up - preview = preview - .replace(/^>+/gm, '') - .replace(/Content-Type:[^\n]+/g, '') - .replace(/Content-Transfer-Encoding:[^\n]+/g, '') - .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') - .replace(/boundary=[^\n]+/g, '') - .replace(/charset=[^\n]+/g, '') - .replace(/[\r\n]+/g, ' ') - .trim(); - - // Take first 100 characters - preview = preview.substring(0, 100); - - // Try to end at a complete word - if (preview.length === 100) { - const lastSpace = preview.lastIndexOf(' '); - if (lastSpace > 80) { - preview = preview.substring(0, lastSpace); - } - preview += '...'; - } - - } catch (e) { - console.error('Error generating preview:', e); - preview = ''; - } - - return preview || 'No preview available'; - })()} -
-
-
- ))} + {emails.map((email) => renderEmailListItem(email))} {isLoadingMore && (