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;
showQuotedText?: boolean;
type?: 'html' | 'text' | 'auto';
debug?: boolean;
}
/**
@ -19,45 +20,76 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
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: `<p>${contentToRender.text.replace(/\n/g, '<br>')}</p>`
};
} 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: `<p>${contentToRender.text.replace(/\n/g, '<br>')}</p>`
};
} 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 `<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]);
// Apply quoted text styling if needed
@ -75,6 +107,20 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
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>{`
.email-content-display {
width: 100%;
@ -101,6 +147,11 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
padding: 0.5rem;
border: 1px solid #ddd;
}
.content-debug {
font-family: monospace;
color: #666;
}
`}</style>
</div>
);

View File

@ -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<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({
@ -70,8 +56,8 @@ export default function EmailPanel({
const [isComposing, setIsComposing] = useState<boolean>(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 (
<div className="h-full">
{isComposing ? (
<ComposeEmail
initialEmail={formattedEmail}
<ComposeEmailAdapter
initialEmail={standardizedEmail}
type={composeType}
onClose={handleComposeClose}
onSend={onSendEmail}
onSend={handleSendEmail}
/>
) : (
<div className="max-w-4xl mx-auto h-full">
<EmailPreview
email={formattedEmail}
email={standardizedEmail}
onReply={handleReply}
/>
</div>

View File

@ -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<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
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 (
<div className="flex items-center justify-center h-full p-6">
<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
const hasAttachments = email.attachments && email.attachments.length > 0;
const hasAttachments = standardizedEmail.attachments && standardizedEmail.attachments.length > 0;
return (
<Card className="flex flex-col h-full overflow-hidden border-0 shadow-none">
{/* Email header */}
<div className="p-6 border-b">
<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">
<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 items-center justify-between">
<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 className="text-sm text-muted-foreground truncate mt-1">
To: {formatEmailAddresses(email.to)}
To: {formatEmailAddresses(standardizedEmail.to)}
</div>
{email.cc && email.cc.length > 0 && (
{standardizedEmail.cc && standardizedEmail.cc.length > 0 && (
<div className="text-sm text-muted-foreground truncate mt-1">
Cc: {formatEmailAddresses(email.cc)}
Cc: {formatEmailAddresses(standardizedEmail.cc)}
</div>
)}
</div>
@ -120,9 +149,9 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
{/* Attachments list */}
{hasAttachments && (
<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">
{email.attachments.map((attachment, index) => (
{standardizedEmail.attachments.map((attachment, index) => (
<div
key={index}
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
content={email.content}
content={standardizedEmail.content}
type="auto"
className="p-6"
debug={process.env.NODE_ENV === 'development'}
/>
</div>
{/* Only in development mode: Show debugging info */}
{/* Always show debugging info in development mode */}
{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>
<div className="mt-2 overflow-auto max-h-40 p-2 bg-gray-50 rounded">
<p><strong>Email ID:</strong> {email.id}</p>
<p><strong>Content Type:</strong> {email.content.isHtml ? 'HTML' : 'Plain Text'}</p>
<p><strong>Text Direction:</strong> {email.content.direction || 'ltr'}</p>
<div className="mt-2 overflow-auto max-h-80 p-2 bg-gray-50 rounded">
<p><strong>Email ID:</strong> {standardizedEmail.id}</p>
<p><strong>Content Type:</strong> {standardizedEmail.content.isHtml ? 'HTML' : 'Plain Text'}</p>
<p><strong>Text Direction:</strong> {standardizedEmail.content.direction || 'ltr'}</p>
<p><strong>Content Size:</strong>
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
</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>
</details>
)}