courrier preview

This commit is contained in:
alma 2025-04-30 21:15:50 +02:00
parent d12a6c4670
commit b71336e749
4 changed files with 292 additions and 113 deletions

View File

@ -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<void>;
}
/**
* 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<OldEmailMessage | null>(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 <div>Loading email...</div>;
}
// Pass the adapted email to the original ComposeEmail component
return (
<ComposeEmail
initialEmail={adaptedEmail}
type={type}
onClose={onClose}
onSend={onSend}
/>
);
}

View File

@ -9,6 +9,7 @@ interface EmailContentDisplayProps {
className?: string; className?: string;
showQuotedText?: boolean; showQuotedText?: boolean;
type?: 'html' | 'text' | 'auto'; type?: 'html' | 'text' | 'auto';
debug?: boolean;
} }
/** /**
@ -19,45 +20,76 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
content, content,
className = '', className = '',
showQuotedText = true, showQuotedText = true,
type = 'auto' type = 'auto',
debug = false
}) => { }) => {
// Normalize the content to our standard format if needed // Normalize the content to our standard format if needed
const normalizedContent = useMemo(() => { const normalizedContent = useMemo(() => {
// If content is already in our EmailContent format try {
if (content && // Handle different input types
typeof content === 'object' && if (!content) {
'text' in content && return {
'isHtml' in content) { html: undefined,
return content as EmailContent; 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]); }, [content]);
// Render the normalized content // Render the normalized content
const htmlContent = useMemo(() => { const htmlContent = useMemo(() => {
if (!normalizedContent) return ''; if (!normalizedContent) return '';
// Override content type if specified try {
let contentToRender: EmailContent = { ...normalizedContent }; // Override content type if specified
let contentToRender: EmailContent = { ...normalizedContent };
if (type === 'html' && !contentToRender.isHtml) {
// Force HTML rendering for text content if (type === 'html' && !contentToRender.isHtml) {
contentToRender = { // Force HTML rendering for text content
...contentToRender, contentToRender = {
isHtml: true, ...contentToRender,
html: `<p>${contentToRender.text.replace(/\n/g, '<br>')}</p>` isHtml: true,
}; html: `<p>${contentToRender.text.replace(/\n/g, '<br>')}</p>`
} else if (type === 'text' && contentToRender.isHtml) { };
// Force text rendering } else if (type === 'text' && contentToRender.isHtml) {
contentToRender = { // Force text rendering
...contentToRender, contentToRender = {
isHtml: false ...contentToRender,
}; isHtml: false
};
}
return renderEmailContent(contentToRender);
} catch (error) {
console.error('Error rendering content in EmailContentDisplay:', error);
return `<div class="error-message p-4 text-red-500">Error rendering email content: ${error instanceof Error ? error.message : 'Unknown error'}</div>`;
} }
return renderEmailContent(contentToRender);
}, [normalizedContent, type]); }, [normalizedContent, type]);
// Apply quoted text styling if needed // Apply quoted text styling if needed
@ -75,6 +107,20 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
dangerouslySetInnerHTML={{ __html: htmlContent }} dangerouslySetInnerHTML={{ __html: htmlContent }}
/> />
{/* Debug output if enabled */}
{debug && (
<div className="content-debug mt-4 p-2 text-xs bg-gray-100 border rounded">
<p><strong>Content Type:</strong> {typeof content}</p>
{typeof content === 'object' && (
<p><strong>Keys:</strong> {Object.keys(content).join(', ')}</p>
)}
<p><strong>Normalized:</strong> {normalizedContent.isHtml ? 'HTML' : 'Text'}</p>
<p><strong>Direction:</strong> {normalizedContent.direction}</p>
<p><strong>Has HTML:</strong> {!!normalizedContent.html}</p>
<p><strong>Text Length:</strong> {normalizedContent.text?.length || 0}</p>
</div>
)}
<style jsx>{` <style jsx>{`
.email-content-display { .email-content-display {
width: 100%; width: 100%;
@ -101,6 +147,11 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
padding: 0.5rem; padding: 0.5rem;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.content-debug {
font-family: monospace;
color: #666;
}
`}</style> `}</style>
</div> </div>
); );

View File

@ -2,45 +2,12 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react';
import EmailPreview from './EmailPreview'; import EmailPreview from './EmailPreview';
import ComposeEmail from './ComposeEmail'; import ComposeEmailAdapter from './ComposeEmailAdapter';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { formatReplyEmail, EmailMessage as FormatterEmailMessage } from '@/lib/utils/email-formatter';
import { useEmailFetch } from '@/hooks/use-email-fetch'; import { useEmailFetch } from '@/hooks/use-email-fetch';
import { debounce } from '@/lib/utils/debounce'; import { debounce } from '@/lib/utils/debounce';
import { formatEmailContent } from '@/lib/utils/email-content'; import { EmailMessage } from '@/types/email';
import { adaptLegacyEmail } from '@/lib/utils/email-adapter';
// 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;
}
interface EmailPanelProps { interface EmailPanelProps {
selectedEmail: { selectedEmail: {
@ -48,7 +15,26 @@ interface EmailPanelProps {
accountId: string; accountId: string;
folder: string; folder: string;
} | null; } | null;
onSendEmail: (email: any) => void; onSendEmail: (email: any) => Promise<void>;
}
// 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<void>;
} }
export default function EmailPanel({ export default function EmailPanel({
@ -70,8 +56,8 @@ export default function EmailPanel({
const [isComposing, setIsComposing] = useState<boolean>(false); const [isComposing, setIsComposing] = useState<boolean>(false);
const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new'); const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new');
// Format the email content // Convert the email to the standardized format
const formattedEmail = useMemo(() => { const standardizedEmail = useMemo(() => {
if (!email) { if (!email) {
console.log('EmailPanel: No email provided'); console.log('EmailPanel: No email provided');
return null; return null;
@ -79,28 +65,13 @@ export default function EmailPanel({
console.log('EmailPanel: Raw email:', email); console.log('EmailPanel: Raw email:', email);
// CRITICAL FIX: Simplify email formatting to prevent double processing try {
// Just normalize the content structure, don't try to format content here // Use the adapter utility to convert to the standardized format
// The actual formatting will happen in EmailPreview with formatEmailContent return adaptLegacyEmail(email);
} catch (error) {
// If all fields are already present, just return as is console.error('EmailPanel: Error adapting email:', error);
if (email.content && typeof email.content === 'object' && email.content.html && email.content.text) { return null;
return email;
} }
// 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]); }, [email]);
// Debounced email fetch // Debounced email fetch
@ -141,6 +112,16 @@ export default function EmailPanel({
setComposeType('new'); 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 no email is selected and not composing
if (!selectedEmail && !isComposing) { if (!selectedEmail && !isComposing) {
return ( return (
@ -194,16 +175,16 @@ export default function EmailPanel({
return ( return (
<div className="h-full"> <div className="h-full">
{isComposing ? ( {isComposing ? (
<ComposeEmail <ComposeEmailAdapter
initialEmail={formattedEmail} initialEmail={standardizedEmail}
type={composeType} type={composeType}
onClose={handleComposeClose} onClose={handleComposeClose}
onSend={onSendEmail} onSend={handleSendEmail}
/> />
) : ( ) : (
<div className="max-w-4xl mx-auto h-full"> <div className="max-w-4xl mx-auto h-full">
<EmailPreview <EmailPreview
email={formattedEmail} email={standardizedEmail}
onReply={handleReply} onReply={handleReply}
/> />
</div> </div>

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useRef } from 'react'; import { useRef, useMemo } from 'react';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area'; 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 { Card } from '@/components/ui/card';
import { EmailMessage, EmailAddress } from '@/types/email'; import { EmailMessage, EmailAddress } from '@/types/email';
import { formatEmailAddresses, formatEmailDate } from '@/lib/utils/email-utils'; import { formatEmailAddresses, formatEmailDate } from '@/lib/utils/email-utils';
import { adaptLegacyEmail } from '@/lib/utils/email-adapter';
import EmailContentDisplay from './EmailContentDisplay'; import EmailContentDisplay from './EmailContentDisplay';
interface EmailPreviewProps { interface EmailPreviewProps {
email: EmailMessage | null; email: EmailMessage | any;
loading?: boolean; loading?: boolean;
onReply?: (type: 'reply' | 'reply-all' | 'forward') => void; 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 // Add editorRef to match ComposeEmail exactly
const editorRef = useRef<HTMLDivElement>(null); const editorRef = useRef<HTMLDivElement>(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 // Get sender initials for avatar
const getSenderInitials = (name: string) => { const getSenderInitials = (name: string) => {
if (!name) return ''; if (!name) return '';
@ -44,7 +70,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
} }
// No email selected // No email selected
if (!email) { if (!standardizedEmail) {
return ( return (
<div className="flex items-center justify-center h-full p-6"> <div className="flex items-center justify-center h-full p-6">
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
@ -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 // Check for attachments
const hasAttachments = email.attachments && email.attachments.length > 0; const hasAttachments = standardizedEmail.attachments && standardizedEmail.attachments.length > 0;
return ( return (
<Card className="flex flex-col h-full overflow-hidden border-0 shadow-none"> <Card className="flex flex-col h-full overflow-hidden border-0 shadow-none">
{/* Email header */} {/* Email header */}
<div className="p-6 border-b"> <div className="p-6 border-b">
<div className="mb-4"> <div className="mb-4">
<h2 className="text-xl font-semibold mb-4">{email.subject}</h2> <h2 className="text-xl font-semibold mb-4">{standardizedEmail.subject}</h2>
<div className="flex items-start gap-3 mb-4"> <div className="flex items-start gap-3 mb-4">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
@ -74,16 +103,16 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="font-medium">{sender?.name || sender?.address}</div> <div className="font-medium">{sender?.name || sender?.address}</div>
<span className="text-sm text-muted-foreground">{formatEmailDate(email.date)}</span> <span className="text-sm text-muted-foreground">{formatEmailDate(standardizedEmail.date)}</span>
</div> </div>
<div className="text-sm text-muted-foreground truncate mt-1"> <div className="text-sm text-muted-foreground truncate mt-1">
To: {formatEmailAddresses(email.to)} To: {formatEmailAddresses(standardizedEmail.to)}
</div> </div>
{email.cc && email.cc.length > 0 && ( {standardizedEmail.cc && standardizedEmail.cc.length > 0 && (
<div className="text-sm text-muted-foreground truncate mt-1"> <div className="text-sm text-muted-foreground truncate mt-1">
Cc: {formatEmailAddresses(email.cc)} Cc: {formatEmailAddresses(standardizedEmail.cc)}
</div> </div>
)} )}
</div> </div>
@ -120,9 +149,9 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
{/* Attachments list */} {/* Attachments list */}
{hasAttachments && ( {hasAttachments && (
<div className="px-6 py-3 border-b bg-muted/20"> <div className="px-6 py-3 border-b bg-muted/20">
<h3 className="text-sm font-medium mb-2">Attachments ({email.attachments.length})</h3> <h3 className="text-sm font-medium mb-2">Attachments ({standardizedEmail.attachments.length})</h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{email.attachments.map((attachment, index) => ( {standardizedEmail.attachments.map((attachment, index) => (
<div <div
key={index} key={index}
className="flex items-center gap-2 bg-background rounded-md px-3 py-1.5 text-sm border" className="flex items-center gap-2 bg-background rounded-md px-3 py-1.5 text-sm border"
@ -151,24 +180,38 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
}} }}
> >
<EmailContentDisplay <EmailContentDisplay
content={email.content} content={standardizedEmail.content}
type="auto" type="auto"
className="p-6" className="p-6"
debug={process.env.NODE_ENV === 'development'}
/> />
</div> </div>
{/* Only in development mode: Show debugging info */} {/* Always show debugging info in development mode */}
{process.env.NODE_ENV === 'development' && ( {process.env.NODE_ENV === 'development' && (
<details className="mt-4 text-xs text-muted-foreground border rounded-md p-2"> <details className="mt-4 text-xs text-muted-foreground border rounded-md p-2" open>
<summary className="cursor-pointer">Email Debug Info</summary> <summary className="cursor-pointer">Email Debug Info</summary>
<div className="mt-2 overflow-auto max-h-40 p-2 bg-gray-50 rounded"> <div className="mt-2 overflow-auto max-h-80 p-2 bg-gray-50 rounded">
<p><strong>Email ID:</strong> {email.id}</p> <p><strong>Email ID:</strong> {standardizedEmail.id}</p>
<p><strong>Content Type:</strong> {email.content.isHtml ? 'HTML' : 'Plain Text'}</p> <p><strong>Content Type:</strong> {standardizedEmail.content.isHtml ? 'HTML' : 'Plain Text'}</p>
<p><strong>Text Direction:</strong> {email.content.direction || 'ltr'}</p> <p><strong>Text Direction:</strong> {standardizedEmail.content.direction || 'ltr'}</p>
<p><strong>Content Size:</strong> <p><strong>Content Size:</strong>
HTML: {email.content.html?.length || 0} chars, HTML: {standardizedEmail.content.html?.length || 0} chars,
Text: {email.content.text?.length || 0} chars Text: {standardizedEmail.content.text?.length || 0} chars
</p> </p>
<p><strong>Content Structure:</strong> {JSON.stringify(standardizedEmail.content, null, 2)}</p>
<hr className="my-2" />
<p><strong>Original Email Type:</strong> {typeof email}</p>
<p><strong>Original Content Type:</strong> {typeof email.content}</p>
{email && typeof email.content === 'object' && (
<p><strong>Original Content Keys:</strong> {Object.keys(email.content).join(', ')}</p>
)}
{email && email.html && (
<p><strong>Has HTML property:</strong> {email.html.length} chars</p>
)}
{email && email.text && (
<p><strong>Has Text property:</strong> {email.text.length} chars</p>
)}
</div> </div>
</details> </details>
)} )}