diff --git a/components/email/ComposeEmailAdapter.tsx b/components/email/ComposeEmailAdapter.tsx new file mode 100644 index 00000000..eeec2aad --- /dev/null +++ b/components/email/ComposeEmailAdapter.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import ComposeEmail from './ComposeEmail'; +import { EmailMessage as NewEmailMessage } from '@/types/email'; +import { + EmailMessage as OldEmailMessage, + formatReplyEmail as oldFormatReplyEmail, + formatForwardedEmail as oldFormatForwardedEmail +} from '@/lib/utils/email-formatter'; + +interface ComposeEmailAdapterProps { + initialEmail?: NewEmailMessage | null; + type?: 'new' | 'reply' | 'reply-all' | 'forward'; + onClose: () => void; + onSend: (emailData: { + to: string; + cc?: string; + bcc?: string; + subject: string; + body: string; + attachments?: Array<{ + name: string; + content: string; + type: string; + }>; + }) => Promise; +} + +/** + * Adapter component that converts between the new EmailMessage format + * and the format expected by the legacy ComposeEmail component + */ +export default function ComposeEmailAdapter({ + initialEmail, + type = 'new', + onClose, + onSend +}: ComposeEmailAdapterProps) { + // Convert the new EmailMessage format to the old format + const [adaptedEmail, setAdaptedEmail] = useState(null); + + useEffect(() => { + if (!initialEmail) { + setAdaptedEmail(null); + return; + } + + try { + // Convert the new EmailMessage to the old format + const oldFormat: OldEmailMessage = { + id: initialEmail.id, + messageId: initialEmail.messageId, + subject: initialEmail.subject, + from: initialEmail.from, + to: initialEmail.to, + cc: initialEmail.cc, + bcc: initialEmail.bcc, + date: initialEmail.date, + // Convert new flags object to old format string array + flags: initialEmail.flags ? { + seen: initialEmail.flags.seen || false, + flagged: initialEmail.flags.flagged || false, + answered: initialEmail.flags.answered || false, + deleted: initialEmail.flags.deleted || false, + draft: initialEmail.flags.draft || false + } : undefined, + // Convert new content format to old format + content: initialEmail.content.isHtml && initialEmail.content.html + ? initialEmail.content.html + : initialEmail.content.text, + html: initialEmail.content.isHtml ? initialEmail.content.html : undefined, + text: initialEmail.content.text, + attachments: initialEmail.attachments.map(att => ({ + filename: att.filename, + contentType: att.contentType, + content: att.content, + size: att.size || 0 + })) + }; + + console.log('ComposeEmailAdapter: Converted new format to old format', oldFormat); + setAdaptedEmail(oldFormat); + } catch (error) { + console.error('Error adapting email for ComposeEmail:', error); + setAdaptedEmail(null); + } + }, [initialEmail]); + + // If still adapting, show loading + if (initialEmail && !adaptedEmail) { + return
Loading email...
; + } + + // Pass the adapted email to the original ComposeEmail component + return ( + + ); +} \ No newline at end of file diff --git a/components/email/EmailContentDisplay.tsx b/components/email/EmailContentDisplay.tsx index 67c0ddde..2ac38800 100644 --- a/components/email/EmailContentDisplay.tsx +++ b/components/email/EmailContentDisplay.tsx @@ -9,6 +9,7 @@ interface EmailContentDisplayProps { className?: string; showQuotedText?: boolean; type?: 'html' | 'text' | 'auto'; + debug?: boolean; } /** @@ -19,45 +20,76 @@ const EmailContentDisplay: React.FC = ({ content, className = '', showQuotedText = true, - type = 'auto' + type = 'auto', + debug = false }) => { // Normalize the content to our standard format if needed const normalizedContent = useMemo(() => { - // If content is already in our EmailContent format - if (content && - typeof content === 'object' && - 'text' in content && - 'isHtml' in content) { - return content as EmailContent; + try { + // Handle different input types + if (!content) { + return { + html: undefined, + text: 'No content available', + isHtml: false, + direction: 'ltr' + } as EmailContent; + } + + // If content is already in our EmailContent format + if (content && + typeof content === 'object' && + 'text' in content && + 'isHtml' in content) { + return content as EmailContent; + } + + // Special case for simple string content + if (typeof content === 'string') { + return normalizeEmailContent({ content }); + } + + // Otherwise normalize it + return normalizeEmailContent(content); + } catch (error) { + console.error('Error normalizing content in EmailContentDisplay:', error); + return { + html: undefined, + text: `Error processing email content: ${error instanceof Error ? error.message : 'Unknown error'}`, + isHtml: false, + direction: 'ltr' + } as EmailContent; } - - // Otherwise normalize it - return normalizeEmailContent(content); }, [content]); // Render the normalized content const htmlContent = useMemo(() => { if (!normalizedContent) return ''; - // Override content type if specified - let contentToRender: EmailContent = { ...normalizedContent }; - - if (type === 'html' && !contentToRender.isHtml) { - // Force HTML rendering for text content - contentToRender = { - ...contentToRender, - isHtml: true, - html: `

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

` - }; - } else if (type === 'text' && contentToRender.isHtml) { - // Force text rendering - contentToRender = { - ...contentToRender, - isHtml: false - }; + try { + // Override content type if specified + let contentToRender: EmailContent = { ...normalizedContent }; + + if (type === 'html' && !contentToRender.isHtml) { + // Force HTML rendering for text content + contentToRender = { + ...contentToRender, + isHtml: true, + html: `

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

` + }; + } else if (type === 'text' && contentToRender.isHtml) { + // Force text rendering + contentToRender = { + ...contentToRender, + isHtml: false + }; + } + + return renderEmailContent(contentToRender); + } catch (error) { + console.error('Error rendering content in EmailContentDisplay:', error); + return `
Error rendering email content: ${error instanceof Error ? error.message : 'Unknown error'}
`; } - - return renderEmailContent(contentToRender); }, [normalizedContent, type]); // Apply quoted text styling if needed @@ -75,6 +107,20 @@ const EmailContentDisplay: React.FC = ({ dangerouslySetInnerHTML={{ __html: htmlContent }} /> + {/* Debug output if enabled */} + {debug && ( +
+

Content Type: {typeof content}

+ {typeof content === 'object' && ( +

Keys: {Object.keys(content).join(', ')}

+ )} +

Normalized: {normalizedContent.isHtml ? 'HTML' : 'Text'}

+

Direction: {normalizedContent.direction}

+

Has HTML: {!!normalizedContent.html}

+

Text Length: {normalizedContent.text?.length || 0}

+
+ )} + ); diff --git a/components/email/EmailPanel.tsx b/components/email/EmailPanel.tsx index 6031a42b..cf099f6a 100644 --- a/components/email/EmailPanel.tsx +++ b/components/email/EmailPanel.tsx @@ -2,45 +2,12 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import EmailPreview from './EmailPreview'; -import ComposeEmail from './ComposeEmail'; +import ComposeEmailAdapter from './ComposeEmailAdapter'; import { Loader2 } from 'lucide-react'; -import { formatReplyEmail, EmailMessage as FormatterEmailMessage } from '@/lib/utils/email-formatter'; import { useEmailFetch } from '@/hooks/use-email-fetch'; import { debounce } from '@/lib/utils/debounce'; -import { formatEmailContent } from '@/lib/utils/email-content'; - -// Add local EmailMessage interface -interface EmailAddress { - name: string; - address: string; -} - -interface EmailMessage { - id: string; - messageId?: string; - subject: string; - from: EmailAddress[]; - to: EmailAddress[]; - cc?: EmailAddress[]; - bcc?: EmailAddress[]; - date: Date | string; - flags?: { - seen: boolean; - flagged: boolean; - answered: boolean; - deleted: boolean; - draft: boolean; - }; - preview?: string; - content?: string; - html?: string; - text?: string; - hasAttachments?: boolean; - attachments?: any[]; - folder?: string; - size?: number; - contentFetched?: boolean; -} +import { EmailMessage } from '@/types/email'; +import { adaptLegacyEmail } from '@/lib/utils/email-adapter'; interface EmailPanelProps { selectedEmail: { @@ -48,7 +15,26 @@ interface EmailPanelProps { accountId: string; folder: string; } | null; - onSendEmail: (email: any) => void; + onSendEmail: (email: any) => Promise; +} + +// Type for the legacy ComposeEmail component props +interface ComposeEmailProps { + initialEmail?: any; + type?: 'new' | 'reply' | 'reply-all' | 'forward'; + onClose: () => void; + onSend: (emailData: { + to: string; + cc?: string; + bcc?: string; + subject: string; + body: string; + attachments?: Array<{ + name: string; + content: string; + type: string; + }>; + }) => Promise; } export default function EmailPanel({ @@ -70,8 +56,8 @@ export default function EmailPanel({ const [isComposing, setIsComposing] = useState(false); const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new'); - // Format the email content - const formattedEmail = useMemo(() => { + // Convert the email to the standardized format + const standardizedEmail = useMemo(() => { if (!email) { console.log('EmailPanel: No email provided'); return null; @@ -79,28 +65,13 @@ export default function EmailPanel({ console.log('EmailPanel: Raw email:', email); - // CRITICAL FIX: Simplify email formatting to prevent double processing - // Just normalize the content structure, don't try to format content here - // The actual formatting will happen in EmailPreview with formatEmailContent - - // If all fields are already present, just return as is - if (email.content && typeof email.content === 'object' && email.content.html && email.content.text) { - return email; + try { + // Use the adapter utility to convert to the standardized format + return adaptLegacyEmail(email); + } catch (error) { + console.error('EmailPanel: Error adapting email:', error); + return null; } - - // Create a standardized email object with consistent content structure - return { - ...email, - // Ensure content is an object with html and text properties - content: { - text: typeof email.content === 'object' ? email.content.text : - typeof email.text === 'string' ? email.text : - typeof email.content === 'string' ? email.content : '', - html: typeof email.content === 'object' ? email.content.html : - typeof email.html === 'string' ? email.html : - typeof email.content === 'string' ? email.content : '' - } - }; }, [email]); // Debounced email fetch @@ -141,6 +112,16 @@ export default function EmailPanel({ setComposeType('new'); }; + // Wrap the onSendEmail function to ensure it returns a Promise + const handleSendEmail = async (emailData: any) => { + try { + return await onSendEmail(emailData); + } catch (error) { + console.error('Error sending email:', error); + throw error; // Re-throw to let ComposeEmail handle it + } + }; + // If no email is selected and not composing if (!selectedEmail && !isComposing) { return ( @@ -194,16 +175,16 @@ export default function EmailPanel({ return (
{isComposing ? ( - ) : (
diff --git a/components/email/EmailPreview.tsx b/components/email/EmailPreview.tsx index 17d2e1f8..27ee1eac 100644 --- a/components/email/EmailPreview.tsx +++ b/components/email/EmailPreview.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRef } from 'react'; +import { useRef, useMemo } from 'react'; import { Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; @@ -8,10 +8,11 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Card } from '@/components/ui/card'; import { EmailMessage, EmailAddress } from '@/types/email'; import { formatEmailAddresses, formatEmailDate } from '@/lib/utils/email-utils'; +import { adaptLegacyEmail } from '@/lib/utils/email-adapter'; import EmailContentDisplay from './EmailContentDisplay'; interface EmailPreviewProps { - email: EmailMessage | null; + email: EmailMessage | any; loading?: boolean; onReply?: (type: 'reply' | 'reply-all' | 'forward') => void; } @@ -20,6 +21,31 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP // Add editorRef to match ComposeEmail exactly const editorRef = useRef(null); + // Convert legacy email to standardized format if needed + const standardizedEmail = useMemo(() => { + if (!email) return null; + + try { + // Check if the email is already in the standardized format + if ( + email.content && + typeof email.content === 'object' && + 'isHtml' in email.content && + 'text' in email.content + ) { + console.log('EmailPreview: Email is already in standardized format'); + return email as EmailMessage; + } + + // Otherwise, adapt it + console.log('EmailPreview: Adapting legacy email format'); + return adaptLegacyEmail(email); + } catch (error) { + console.error('Error adapting email:', error); + return null; + } + }, [email]); + // Get sender initials for avatar const getSenderInitials = (name: string) => { if (!name) return ''; @@ -44,7 +70,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP } // No email selected - if (!email) { + if (!standardizedEmail) { return (
@@ -54,17 +80,20 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP ); } - const sender = email.from && email.from.length > 0 ? email.from[0] : undefined; + // Debug output for content structure + console.log('EmailPreview: Standardized Email Content:', standardizedEmail.content); + + const sender = standardizedEmail.from && standardizedEmail.from.length > 0 ? standardizedEmail.from[0] : undefined; // Check for attachments - const hasAttachments = email.attachments && email.attachments.length > 0; + const hasAttachments = standardizedEmail.attachments && standardizedEmail.attachments.length > 0; return ( {/* Email header */}
-

{email.subject}

+

{standardizedEmail.subject}

@@ -74,16 +103,16 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
{sender?.name || sender?.address}
- {formatEmailDate(email.date)} + {formatEmailDate(standardizedEmail.date)}
- To: {formatEmailAddresses(email.to)} + To: {formatEmailAddresses(standardizedEmail.to)}
- {email.cc && email.cc.length > 0 && ( + {standardizedEmail.cc && standardizedEmail.cc.length > 0 && (
- Cc: {formatEmailAddresses(email.cc)} + Cc: {formatEmailAddresses(standardizedEmail.cc)}
)}
@@ -120,9 +149,9 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP {/* Attachments list */} {hasAttachments && (
-

Attachments ({email.attachments.length})

+

Attachments ({standardizedEmail.attachments.length})

- {email.attachments.map((attachment, index) => ( + {standardizedEmail.attachments.map((attachment, index) => (
- {/* Only in development mode: Show debugging info */} + {/* Always show debugging info in development mode */} {process.env.NODE_ENV === 'development' && ( -
+
Email Debug Info -
-

Email ID: {email.id}

-

Content Type: {email.content.isHtml ? 'HTML' : 'Plain Text'}

-

Text Direction: {email.content.direction || 'ltr'}

+
+

Email ID: {standardizedEmail.id}

+

Content Type: {standardizedEmail.content.isHtml ? 'HTML' : 'Plain Text'}

+

Text Direction: {standardizedEmail.content.direction || 'ltr'}

Content Size: - HTML: {email.content.html?.length || 0} chars, - Text: {email.content.text?.length || 0} chars + HTML: {standardizedEmail.content.html?.length || 0} chars, + Text: {standardizedEmail.content.text?.length || 0} chars

+

Content Structure: {JSON.stringify(standardizedEmail.content, null, 2)}

+
+

Original Email Type: {typeof email}

+

Original Content Type: {typeof email.content}

+ {email && typeof email.content === 'object' && ( +

Original Content Keys: {Object.keys(email.content).join(', ')}

+ )} + {email && email.html && ( +

Has HTML property: {email.html.length} chars

+ )} + {email && email.text && ( +

Has Text property: {email.text.length} chars

+ )}
)}