From 2b02742bc485c2504b71a990518bd1a85dfce1b2 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 15:29:38 +0200 Subject: [PATCH] courrier preview --- components/email/BulkActionsToolbar.tsx | 6 +- components/email/ComposeEmail.tsx | 9 +- components/email/EmailContentDisplay.tsx | 10 + components/email/EmailList.tsx | 12 +- components/email/RichEmailEditor.tsx | 60 ++-- lib/utils/email-content.ts | 14 + lib/utils/email-utils.ts | 373 +++++++++++++++-------- 7 files changed, 335 insertions(+), 149 deletions(-) diff --git a/components/email/BulkActionsToolbar.tsx b/components/email/BulkActionsToolbar.tsx index 04a70e79..da260fe5 100644 --- a/components/email/BulkActionsToolbar.tsx +++ b/components/email/BulkActionsToolbar.tsx @@ -20,7 +20,7 @@ export default function BulkActionsToolbar({ onBulkAction }: BulkActionsToolbarProps) { return ( -
+
{selectedCount} selected @@ -32,13 +32,13 @@ export default function BulkActionsToolbar({ variant="ghost" size="icon" className="h-7 w-7 text-blue-600 hover:text-blue-900 hover:bg-blue-100" - onClick={() => onBulkAction('mark-read')} + onClick={() => onBulkAction('mark-unread')} > -

Mark as read

+

Mark as unread

diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 45d9feb9..b5b367b1 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -170,7 +170,9 @@ export default function ComposeEmail(props: ComposeEmailProps) { } else { console.log('Setting reply content:', { length: content.length, - isHtml: formatted.content.isHtml + isHtml: formatted.content.isHtml, + startsWithHtml: content.trim().startsWith('<'), + contentType: typeof content }); setEmailContent(content); } @@ -198,7 +200,8 @@ export default function ComposeEmail(props: ComposeEmailProps) { if (!content) { console.warn('Forward content is empty, falling back to a basic template'); // Provide a basic template if the content is empty - const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedInfoForEmail(initialEmail); + const { fromStr, dateStr, subject: origSubject, toStr, ccStr } = getFormattedInfoForEmail(initialEmail); + console.log('Creating forward fallback with:', { fromStr, dateStr, origSubject }); const fallbackContent = `
@@ -215,7 +218,7 @@ export default function ComposeEmail(props: ComposeEmailProps) { Subject: - ${subject || ''} + ${origSubject || ''} To: diff --git a/components/email/EmailContentDisplay.tsx b/components/email/EmailContentDisplay.tsx index 5ba2c26b..d047c89e 100644 --- a/components/email/EmailContentDisplay.tsx +++ b/components/email/EmailContentDisplay.tsx @@ -26,10 +26,19 @@ const EmailContentDisplay: React.FC = ({ // Process content if provided const processedContent = useMemo(() => { if (!content) { + console.log('EmailContentDisplay: No content provided'); return { __html: '' }; } try { + console.log('EmailContentDisplay processing:', { + contentType: typeof content, + isNull: content === null, + isString: typeof content === 'string', + isObject: typeof content === 'object', + length: typeof content === 'string' ? content.length : null + }); + let formattedContent: string; // If it's a string, we need to determine if it's HTML or plain text @@ -41,6 +50,7 @@ const EmailContentDisplay: React.FC = ({ formattedContent = formatEmailContent({ content }); } + console.log('EmailContentDisplay processed result length:', formattedContent.length); return { __html: formattedContent }; } catch (error) { console.error('Error processing email content:', error); diff --git a/components/email/EmailList.tsx b/components/email/EmailList.tsx index 7ed0c0cb..6dac4916 100644 --- a/components/email/EmailList.tsx +++ b/components/email/EmailList.tsx @@ -119,12 +119,14 @@ export default function EmailList({ return (
- {/* Only show bulk actions when emails are explicitly selected via checkboxes */} + {/* Sticky toolbar - always visible at the top when emails are selected */} {selectedEmailIds.length > 0 && ( - +
+ +
)} {/* Search header */} diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index e7a1c310..6d1949e6 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -119,9 +119,13 @@ const RichEmailEditor: React.FC = ({ quillRef.current.root.innerHTML = sanitizedContent; // Set the direction for the content - quillRef.current.format('direction', direction); - if (direction === 'rtl') { - quillRef.current.format('align', 'right'); + if (quillRef.current && quillRef.current.format) { + quillRef.current.format('direction', direction); + if (direction === 'rtl') { + quillRef.current.format('align', 'right'); + } + } else { + console.warn('Cannot format content: editor not fully initialized'); } } @@ -227,23 +231,38 @@ const RichEmailEditor: React.FC = ({ const textContent = tempDiv.textContent || tempDiv.innerText || ''; // Create simple HTML with text content - quillRef.current.setText(textContent); + if (quillRef.current) { + quillRef.current.setText(textContent || 'No content available'); + } } else { // SIMPLIFIED: Set content directly to the root element rather than using clipboard - quillRef.current.root.innerHTML = sanitizedContent; - - // Set the direction for the content - quillRef.current.format('direction', direction); - if (direction === 'rtl') { - quillRef.current.format('align', 'right'); + if (quillRef.current && quillRef.current.root) { + // First set the content + quillRef.current.root.innerHTML = sanitizedContent; + + // Then safely apply formatting only if quillRef is valid + try { + if (quillRef.current && quillRef.current.format && quillRef.current.root.innerHTML.trim().length > 0) { + // Set the direction for the content + quillRef.current.format('direction', direction); + if (direction === 'rtl') { + quillRef.current.format('align', 'right'); + } + + // Force update + quillRef.current.update(); + + // Set selection to beginning + quillRef.current.setSelection(0, 0); + } else { + console.warn('Skipping format - either editor not ready or content empty'); + } + } catch (formatError) { + console.error('Error applying formatting:', formatError); + // Continue without formatting if there's an error + } } } - - // Force update - quillRef.current.update(); - - // Set selection to beginning - quillRef.current.setSelection(0, 0); } catch (err) { console.error('Error updating content:', err); // Safer fallback that avoids clipboard API @@ -252,11 +271,16 @@ const RichEmailEditor: React.FC = ({ const tempDiv = document.createElement('div'); tempDiv.innerHTML = initialContent; const textContent = tempDiv.textContent || tempDiv.innerText || ''; - quillRef.current.setText(textContent); + + if (quillRef.current) { + quillRef.current.setText(textContent || 'Error loading content'); + } } catch (e) { console.error('All fallbacks failed:', e); // Last resort - quillRef.current.setText('Error loading content'); + if (quillRef.current) { + quillRef.current.setText('Error loading content'); + } } } } diff --git a/lib/utils/email-content.ts b/lib/utils/email-content.ts index dac643c4..1b4dc304 100644 --- a/lib/utils/email-content.ts +++ b/lib/utils/email-content.ts @@ -162,16 +162,30 @@ export function formatEmailContent(email: any): string { // Extract content from email const { text, html } = extractEmailContent(email); + console.log('formatEmailContent processing:', { + hasHtml: !!html, + htmlLength: html?.length || 0, + hasText: !!text, + textLength: text?.length || 0, + emailType: typeof email === 'string' ? 'string' : 'object' + }); + // If we have HTML content, sanitize and standardize it if (html) { // Process HTML content let processedHtml = processHtmlContent(html, text); + console.log('HTML content processed:', { + processedLength: processedHtml?.length || 0, + isEmpty: !processedHtml || processedHtml.trim().length === 0 + }); + // Apply styling return ``; } // If we only have text content, format it properly else if (text) { + console.log('Using plain text formatting'); return formatPlainTextToHtml(text); } diff --git a/lib/utils/email-utils.ts b/lib/utils/email-utils.ts index c7a2ab8a..63380109 100644 --- a/lib/utils/email-utils.ts +++ b/lib/utils/email-utils.ts @@ -252,162 +252,295 @@ function getFormattedHeaderInfo(email: any): { * Format email for reply */ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail { + console.log('formatReplyEmail called:', { type, emailId: originalEmail?.id }); + if (!originalEmail) { + console.warn('formatReplyEmail: No original email provided'); return { to: '', - cc: '', subject: '', - content: { - text: '', - html: '', - isHtml: false, - direction: 'ltr' as const - } + content: { text: '', html: '', isHtml: false, direction: 'ltr' } }; } - - // Extract recipient addresses - const { to, cc } = getRecipientAddresses(originalEmail, type); - - // Get header information - const { fromStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail); - - // Extract content using the centralized extraction function - const { text, html } = extractEmailContent(originalEmail); - // Create a clearer reply header with separator line - const replyHeader = ` -
- On ${dateStr}, ${fromStr} wrote: -
- `; + // Adapt legacy format if needed + const email = 'content' in originalEmail ? originalEmail : adaptLegacyEmail(originalEmail); - // Use the original HTML content if available, otherwise format the text - const contentHtml = html || (text ? `

${text.replace(/\n/g, '

')}

` : '

No content available

'); + // Format subject with Re: prefix + const subject = email.subject ? + (email.subject.toLowerCase().startsWith('re:') ? email.subject : `Re: ${email.subject}`) : + 'Re: '; - // Wrap the original content in proper styling without losing the HTML structure - const cleanHtml = ` - ${replyHeader} -
- ${contentHtml} -
- `; + // Get recipient addresses + const { to, cc } = getRecipientAddresses(email, type); - // Plain text version - const plainText = ` -On ${dateStr}, ${fromStr} wrote: -------------------------------------------------------------------- -${text.split('\n').join('\n> ')} - `; - - return { - to, - cc, - subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`, - content: { - text: plainText.trim(), - html: cleanHtml, - isHtml: true, - direction: 'ltr' + // Get email content and sanitize it + const originalContent = email.content; + + // Extract text and html content + let htmlContent = ''; + let textContent = ''; + let direction: 'ltr' | 'rtl' = 'ltr'; + + // Handle different content formats + if (typeof originalContent === 'string') { + console.log('formatReplyEmail: content is string, length:', originalContent.length); + // Simple string content + textContent = originalContent; + const isHtml = isHtmlContent(originalContent); + if (isHtml) { + htmlContent = originalContent; + } else { + // If it's plain text, convert to HTML + htmlContent = formatPlainTextToHtml(originalContent); } + } + else if (originalContent) { + console.log('formatReplyEmail: content is object:', { + hasHtml: !!originalContent.html, + htmlLength: originalContent.html?.length || 0, + hasText: !!originalContent.text, + textLength: originalContent.text?.length || 0, + direction: originalContent.direction + }); + + // Standard EmailContent object + htmlContent = originalContent.html || ''; + textContent = originalContent.text || ''; + direction = originalContent.direction || 'ltr' as const; + + // If no HTML but has text, convert text to HTML + if (!htmlContent && textContent) { + htmlContent = formatPlainTextToHtml(textContent); + } + } + + // Get quote header + const { fromStr, dateStr } = getFormattedHeaderInfo(email); + + // Use the from name if available, otherwise use email address + const sender = fromStr; + const date = dateStr; + + // Create the quoted reply content + if (htmlContent) { + // Format HTML reply + console.log('Formatting HTML reply, quoted content length:', htmlContent.length); + htmlContent = ` +
+ On ${date}, ${sender} wrote: +
+
+ ${sanitizeHtml(htmlContent)} +
+ `; + } + + if (textContent) { + // Format plain text reply + const lines = textContent.split(/\r\n|\r|\n/); + textContent = `On ${date}, ${sender} wrote:\n\n${lines.map(line => `> ${line}`).join('\n')}`; + } + + const result = { + to, + cc: cc || undefined, + subject, + content: { + html: htmlContent, + text: textContent, + isHtml: true, + direction, + }, + attachments: email.attachments?.map(att => { + // Create properly typed attachment + if ('name' in att) { + return { + filename: att.filename || att.name || 'attachment', + contentType: att.contentType || 'application/octet-stream', + content: att.content + }; + } + return { + filename: att.filename || 'attachment', + contentType: att.contentType || 'application/octet-stream', + content: att.content + }; + }) }; + + console.log('formatReplyEmail result:', { + to: result.to, + subject: result.subject, + hasHtml: !!result.content.html, + htmlLength: result.content.html?.length || 0, + hasText: !!result.content.text, + textLength: result.content.text?.length || 0 + }); + + return result; } /** * Format email for forwarding */ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail { + console.log('formatForwardedEmail called:', { emailId: originalEmail?.id }); + if (!originalEmail) { + console.warn('formatForwardedEmail: No original email provided'); return { to: '', subject: '', - content: { - text: '', - html: '', - isHtml: false, - direction: 'ltr' as const - } + content: { text: '', html: '', isHtml: false, direction: 'ltr' } }; } + + // Adapt legacy format if needed + const email = 'content' in originalEmail ? originalEmail : adaptLegacyEmail(originalEmail); + + // Format subject with Fwd: prefix + const subject = email.subject ? + (email.subject.toLowerCase().startsWith('fwd:') ? email.subject : `Fwd: ${email.subject}`) : + 'Fwd: '; + + // Get original email info for headers + const { fromStr, toStr, ccStr, dateStr } = getFormattedHeaderInfo(email); + + console.log('Forward header info:', { fromStr, toStr, dateStr, subject }); + + // Original sent date + const date = dateStr; + + // Get email content + const originalContent = email.content; + + // Extract text and html content + let htmlContent = ''; + let textContent = ''; + let direction: 'ltr' | 'rtl' = 'ltr'; + + // Handle different content formats + if (typeof originalContent === 'string') { + console.log('formatForwardedEmail: content is string, length:', originalContent.length); + // Simple string content + textContent = originalContent; + const isHtml = isHtmlContent(originalContent); + if (isHtml) { + htmlContent = originalContent; + } else { + // If it's plain text, convert to HTML + htmlContent = formatPlainTextToHtml(originalContent); + } + } + else if (originalContent) { + console.log('formatForwardedEmail: content is object:', { + hasHtml: !!originalContent.html, + htmlLength: originalContent.html?.length || 0, + hasText: !!originalContent.text, + textLength: originalContent.text?.length || 0, + direction: originalContent.direction + }); + + // Standard EmailContent object + htmlContent = originalContent.html || ''; + textContent = originalContent.text || ''; + direction = originalContent.direction || 'ltr' as const; + + // If no HTML but has text, convert text to HTML + if (!htmlContent && textContent) { + htmlContent = formatPlainTextToHtml(textContent); + } + } - // Get header information - const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail); - - // Extract content using the centralized extraction function - const { text, html } = extractEmailContent(originalEmail); - - // Create a traditional forward format with dashed separator - const forwardHeader = ` -
-
-
---------------------------- Forwarded Message ----------------------------
+ // Create the forwarded email HTML content + if (htmlContent) { + console.log('Formatting HTML forward, original content length:', htmlContent.length); + htmlContent = ` +
+ ---------- Forwarded message ----------
+ + + + + + + + + + + + + + + + + + + ${ccStr ? ` + + + + + ` : ''} + +
From:${fromStr}
Date:${date}
Subject:${email.subject || ''}
To:${toStr}
Cc:${ccStr}
- - - - - - - - - - - - - - - - - - ${ccStr ? ` - - - - ` : ''} -
From:${fromStr}
Date:${dateStr}
Subject:${subject || ''}
To:${toStr}
Cc:${ccStr}
-
-
----------------------------------------------------------------------
+
+ ${sanitizeHtml(htmlContent)}
-
- `; + `; + } - // Use the original HTML content if available, otherwise format the text - const contentHtml = html || (text ? `

${text.replace(/\n/g, '

')}

` : '

No content available

'); - - const cleanHtml = `${forwardHeader}${contentHtml}`; - - // Plain text version - with clearer formatting - const plainText = ` ----------------------------- Forwarded Message ---------------------------- + // Format the plain text version + if (textContent) { + textContent = ` +---------- Forwarded message ---------- From: ${fromStr} -Date: ${dateStr} -Subject: ${subject || ''} +Date: ${date} +Subject: ${email.subject || ''} To: ${toStr} -${ccStr ? `Cc: ${ccStr}` : ''} ----------------------------------------------------------------------- +${ccStr ? `Cc: ${ccStr}\n` : ''} -${text} - `; - - // Check if original has attachments - const attachments = originalEmail.attachments || []; - - return { +${textContent} + `.trim(); + } + + const result = { to: '', - subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`, + subject, content: { - text: plainText.trim(), - html: cleanHtml, + html: htmlContent, + text: textContent, isHtml: true, - direction: 'ltr' + direction, }, - // Only include attachments if they exist - attachments: attachments.length > 0 ? attachments.map(att => ({ - filename: att.filename || 'attachment', - contentType: att.contentType || 'application/octet-stream', - content: att.content - })) : undefined + attachments: email.attachments?.map(att => { + // Create properly typed attachment + if ('name' in att) { + return { + filename: att.filename || att.name || 'attachment', + contentType: att.contentType || 'application/octet-stream', + content: att.content + }; + } + return { + filename: att.filename || 'attachment', + contentType: att.contentType || 'application/octet-stream', + content: att.content + }; + }) }; + + console.log('formatForwardedEmail result:', { + subject: result.subject, + hasHtml: !!result.content.html, + htmlLength: result.content.html?.length || 0, + hasText: !!result.content.text, + textLength: result.content.text?.length || 0 + }); + + return result; } /**