courrier preview

This commit is contained in:
alma 2025-04-30 22:38:36 +02:00
parent a82fb22b93
commit 748d0bbff4
4 changed files with 73 additions and 315 deletions

View File

@ -5,7 +5,7 @@ import { renderEmailContent } from '@/lib/utils/email-utils';
import { EmailContent } from '@/types/email';
interface EmailContentDisplayProps {
content: EmailContent | any;
content: EmailContent;
className?: string;
showQuotedText?: boolean;
type?: 'html' | 'text' | 'auto';
@ -23,81 +23,26 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
type = 'auto',
debug = false
}) => {
// Normalize the content to our standard format if needed
const normalizedContent = useMemo(() => {
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 email message with content property
if (content && typeof content === 'object' && content.content &&
typeof content.content === 'object' &&
'text' in content.content &&
'isHtml' in content.content) {
return content.content as EmailContent;
}
// Special case for simple string content
if (typeof content === 'string') {
return {
text: content,
isHtml: content.trim().startsWith('<'),
direction: 'ltr'
} as EmailContent;
}
// For HTML/text properties
if (content && typeof content === 'object') {
if (content.html || content.text) {
return {
html: content.html,
text: content.text || '',
isHtml: !!content.html,
direction: 'ltr'
} as EmailContent;
}
}
// Fallback
console.warn('EmailContentDisplay: Unable to properly normalize content format');
return {
text: 'Content format not supported',
isHtml: false,
direction: 'ltr'
} as EmailContent;
} catch (error) {
console.error('Error normalizing content in EmailContentDisplay:', error);
// Ensure we have valid content to work with
const safeContent = useMemo(() => {
if (!content) {
return {
html: undefined,
text: `Error processing email content: ${error instanceof Error ? error.message : 'Unknown error'}`,
text: 'No content available',
isHtml: false,
direction: 'ltr'
} as EmailContent;
}
return content;
}, [content]);
// Render the normalized content
// Render the content with proper formatting
const htmlContent = useMemo(() => {
if (!normalizedContent) return '';
if (!safeContent) return '';
try {
// Override content type if specified
let contentToRender: EmailContent = { ...normalizedContent };
let contentToRender: EmailContent = { ...safeContent };
if (type === 'html' && !contentToRender.isHtml) {
// Force HTML rendering for text content
@ -116,10 +61,10 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
return renderEmailContent(contentToRender);
} catch (error) {
console.error('Error rendering content in EmailContentDisplay:', error);
console.error('Error rendering content:', error);
return `<div class="error-message p-4 text-red-500">Error rendering email content: ${error instanceof Error ? error.message : 'Unknown error'}</div>`;
}
}, [normalizedContent, type]);
}, [safeContent, type]);
// Apply quoted text styling if needed
const containerStyle: CSSProperties = showQuotedText
@ -139,14 +84,10 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
{/* 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>
<p><strong>Content Type:</strong> {safeContent.isHtml ? 'HTML' : 'Text'}</p>
<p><strong>Direction:</strong> {safeContent.direction}</p>
<p><strong>Has HTML:</strong> {!!safeContent.html}</p>
<p><strong>Text Length:</strong> {safeContent.text?.length || 0}</p>
</div>
)}

View File

@ -59,73 +59,11 @@ export default function EmailPanel({
// Convert the email to the standardized format
const standardizedEmail = useMemo(() => {
if (!email) {
console.log('EmailPanel: No email provided');
return null;
}
console.log('EmailPanel: Raw email:', email);
console.log('EmailPanel: Raw email content type:', typeof email.content);
if (email.content) {
// Log detailed content structure for debugging
if (typeof email.content === 'object') {
console.log('EmailPanel: Content object keys:', Object.keys(email.content));
console.log('EmailPanel: Content has isHtml?', 'isHtml' in email.content);
console.log('EmailPanel: Content has text?', 'text' in email.content);
console.log('EmailPanel: Content has direction?', 'direction' in email.content);
} else if (typeof email.content === 'string') {
console.log('EmailPanel: Content is string, first 100 chars:', email.content.substring(0, 100));
}
}
try {
// Check if it's already in standardized format
if (email.content &&
typeof email.content === 'object' &&
'isHtml' in email.content &&
'text' in email.content &&
'direction' in email.content) {
console.log('EmailPanel: Email already in standardized format');
// Normalize address format before returning
const normalizedEmail = {
...email,
// Ensure from, to, cc are strings as expected by the EmailMessage interface
from: normalizeAddress(email.from),
to: normalizeAddress(email.to),
cc: email.cc ? normalizeAddress(email.cc) : undefined,
bcc: email.bcc ? normalizeAddress(email.bcc) : undefined
};
return normalizedEmail as EmailMessage;
}
// Use the adapter utility to convert to the standardized format
console.log('EmailPanel: Adapting email to standardized format');
const adapted = adaptLegacyEmail(email);
// Log adapted email for debugging
console.log('EmailPanel: Adapted email content:', adapted.content);
return adapted;
} catch (error) {
console.error('EmailPanel: Error adapting email:', error);
// If adaptation fails, create a minimal valid email for display
return {
id: email.id || 'unknown',
subject: email.subject || 'Error displaying email',
from: normalizeAddress(email.from) || '',
to: normalizeAddress(email.to) || '',
date: email.date || new Date().toISOString(),
flags: [],
content: {
text: `Error processing email: ${error instanceof Error ? error.message : 'Unknown error'}`,
html: undefined,
isHtml: false,
direction: 'ltr'
}
} as EmailMessage;
}
// The useEmailFetch hook now provides fully formatted email
return email;
}, [email]);
// Debounced email fetch

View File

@ -1,118 +1,24 @@
'use client';
import { useRef, useMemo } from 'react';
import { useRef } from 'react';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
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-adapters';
import { EmailMessage } from '@/types/email';
import { formatEmailDate } from '@/lib/utils/email-utils';
import EmailContentDisplay from './EmailContentDisplay';
interface EmailPreviewProps {
email: EmailMessage | any;
email: EmailMessage | null;
loading?: boolean;
onReply?: (type: 'reply' | 'reply-all' | 'forward') => void;
}
export default function EmailPreview({ email, loading = false, onReply }: EmailPreviewProps) {
// 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 {
// Log input email details for debugging
console.log('EmailPreview: Input email type:', typeof email);
console.log('EmailPreview: Input email properties:', Object.keys(email));
// Check if from field is an array or string and log it
if (email.from) {
console.log('EmailPreview: From field type:', Array.isArray(email.from) ?
'array' : typeof email.from);
if (Array.isArray(email.from)) {
console.log('EmailPreview: From array length:', email.from.length);
if (email.from.length > 0) {
console.log('EmailPreview: First from item type:', typeof email.from[0]);
}
}
}
if (email.content) {
console.log('EmailPreview: Content type:', typeof email.content);
if (typeof email.content === 'object') {
console.log('EmailPreview: Content properties:', Object.keys(email.content));
} else {
console.log('EmailPreview: Content first 100 chars:', email.content.substring(0, 100));
}
}
// 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');
const adapted = adaptLegacyEmail(email);
// Log the adapted email structure for debugging
console.log('EmailPreview: Adapted email:', {
id: adapted.id,
subject: adapted.subject,
from: adapted.from,
fromType: typeof adapted.from,
isFromArray: Array.isArray(adapted.from),
content: adapted.content ? {
isHtml: adapted.content.isHtml,
direction: adapted.content.direction,
textLength: adapted.content.text?.length,
htmlExists: !!adapted.content.html
} : 'No content'
});
return adapted;
} catch (error) {
console.error('Error adapting email:', error);
// Instead of returning null, try to create a minimal valid email to display the error
return {
id: email?.id || 'error',
subject: email?.subject || 'Error processing email',
from: email?.from || '',
to: email?.to || '',
date: email?.date || new Date().toISOString(),
flags: [],
content: {
text: `Error processing email: ${error instanceof Error ? error.message : 'Unknown error'}`,
isHtml: false,
direction: 'ltr'
}
} as EmailMessage;
}
}, [email]);
// Get sender initials for avatar
const getSenderInitials = (name: string) => {
if (!name) return '';
return name
.split(" ")
.map((n) => n?.[0] || '')
.join("")
.toUpperCase()
.slice(0, 2);
};
// Display loading state
if (loading) {
return (
@ -126,7 +32,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
}
// No email selected
if (!standardizedEmail) {
if (!email) {
return (
<div className="flex items-center justify-center h-full p-6">
<div className="text-center text-muted-foreground">
@ -136,45 +42,37 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
);
}
// Debug output for content structure
console.log('EmailPreview: Standardized Email Content:', standardizedEmail.content);
// Extract sender from various possible formats - handle both string and array formats
// Extract sender name from email.from (which is a string in our standardized format)
let senderName = '';
let senderEmail = '';
// Handle 'from' field which might be a string or an array of EmailAddress objects
if (standardizedEmail.from) {
if (Array.isArray(standardizedEmail.from)) {
// If it's an array of EmailAddress objects
if (standardizedEmail.from.length > 0) {
const sender = standardizedEmail.from[0];
if (typeof sender === 'object') {
senderName = sender.name || sender.address || '';
senderEmail = sender.address || '';
} else {
// Handle case where array contains strings
senderName = String(sender);
senderEmail = String(sender);
}
}
} else if (typeof standardizedEmail.from === 'string') {
// If it's a string, try to extract name and email with regex
const senderInfo = standardizedEmail.from.match(/^(?:"?([^"]*)"?\s)?<?([^\s>]+@[^\s>]+)>?$/);
senderName = senderInfo ? senderInfo[1] || senderInfo[2] : standardizedEmail.from;
senderEmail = senderInfo ? senderInfo[2] : standardizedEmail.from;
}
if (email.from) {
// If it's a string, try to extract name and email with regex
const senderInfo = email.from.match(/^(?:"?([^"]*)"?\s)?<?([^\s>]+@[^\s>]+)>?$/);
senderName = senderInfo ? senderInfo[1] || senderInfo[2] : email.from;
senderEmail = senderInfo ? senderInfo[2] : email.from;
}
// Check for attachments
const hasAttachments = standardizedEmail.attachments && standardizedEmail.attachments.length > 0;
const hasAttachments = email.attachments && email.attachments.length > 0;
// Get sender initials for avatar
const getSenderInitials = (name: string) => {
if (!name) return '';
return name
.split(" ")
.map((n) => n?.[0] || '')
.join("")
.toUpperCase()
.slice(0, 2);
};
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">{standardizedEmail.subject}</h2>
<h2 className="text-xl font-semibold mb-4">{email.subject}</h2>
<div className="flex items-start gap-3 mb-4">
<Avatar className="h-10 w-10">
@ -184,16 +82,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">{senderName}</div>
<span className="text-sm text-muted-foreground">{formatEmailDate(standardizedEmail.date)}</span>
<span className="text-sm text-muted-foreground">{formatEmailDate(email.date)}</span>
</div>
<div className="text-sm text-muted-foreground truncate mt-1">
To: {standardizedEmail.to}
To: {email.to}
</div>
{standardizedEmail.cc && (
{email.cc && (
<div className="text-sm text-muted-foreground truncate mt-1">
Cc: {standardizedEmail.cc}
Cc: {email.cc}
</div>
)}
</div>
@ -228,11 +126,11 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
</div>
{/* Attachments list */}
{hasAttachments && standardizedEmail.attachments && (
{hasAttachments && email.attachments && (
<div className="px-6 py-3 border-b bg-muted/20">
<h3 className="text-sm font-medium mb-2">Attachments ({standardizedEmail.attachments.length})</h3>
<h3 className="text-sm font-medium mb-2">Attachments ({email.attachments.length})</h3>
<div className="flex flex-wrap gap-2">
{standardizedEmail.attachments.map((attachment, index) => (
{email.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"
@ -251,7 +149,6 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
{/* Email body */}
<ScrollArea className="flex-1">
<div className="p-6">
{/* Render the email content using the new standardized component */}
<div
ref={editorRef}
className="email-content-container rounded-lg overflow-hidden bg-white shadow-sm"
@ -261,38 +158,25 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
}}
>
<EmailContentDisplay
content={standardizedEmail.content}
content={email.content}
type="auto"
className="p-6"
debug={process.env.NODE_ENV === 'development'}
/>
</div>
{/* Always show debugging info in development mode */}
{/* Debugging info - simplified */}
{process.env.NODE_ENV === 'development' && (
<details className="mt-4 text-xs text-muted-foreground border rounded-md p-2" open>
<details className="mt-4 text-xs text-muted-foreground border rounded-md p-2">
<summary className="cursor-pointer">Email Debug Info</summary>
<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>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}</p>
<p><strong>Content Size:</strong>
HTML: {standardizedEmail.content.html?.length || 0} chars,
Text: {standardizedEmail.content.text?.length || 0} chars
HTML: {email.content.html?.length || 0} chars,
Text: {email.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>
)}

View File

@ -1,14 +1,15 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useToast } from './use-toast';
import { EmailMessage, EmailContent } from '@/types/email';
interface EmailFetchState {
email: any | null;
email: EmailMessage | null;
loading: boolean;
error: string | null;
}
interface UseEmailFetchProps {
onEmailLoaded?: (email: any) => void;
onEmailLoaded?: (email: EmailMessage) => void;
onError?: (error: string) => void;
}
@ -49,20 +50,15 @@ export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {
return;
}
// CRITICAL FIX: Always abort any previous request when fetching a new email
// This prevents race conditions when switching accounts or folders
// Abort any previous request
if (abortControllerRef.current) {
console.log(`useEmailFetch: Aborting previous request to fetch email ${emailId} from account ${accountId}`);
abortControllerRef.current.abort();
}
// Create a new abort controller for this request
abortControllerRef.current = new AbortController();
setState(prev => ({ ...prev, loading: true, error: null }));
try {
console.log('useEmailFetch: Fetching email with params:', { emailId, accountId, folder });
const response = await fetch(
`/api/courrier/${emailId}?accountId=${encodeURIComponent(accountId)}&folder=${encodeURIComponent(folder)}`,
{
@ -75,24 +71,24 @@ export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {
}
const data = await response.json();
console.log('useEmailFetch: Raw API response:', JSON.stringify(data, null, 2));
// Use the data directly as it already has the standardized format from the server
const transformedEmail = data;
console.log('useEmailFetch: Email from API ready to use:', JSON.stringify({
id: transformedEmail.id,
subject: transformedEmail.subject,
contentType: typeof transformedEmail.content,
hasIsHtml: transformedEmail.content && 'isHtml' in transformedEmail.content,
hasDirection: transformedEmail.content && 'direction' in transformedEmail.content
}, null, 2));
// Create properly formatted email with all required fields
const transformedEmail: EmailMessage = {
...data,
// Ensure content is properly formatted
content: {
text: data.content?.text || '',
html: data.content?.html || undefined,
isHtml: data.content?.html ? true : false,
direction: data.content?.direction || 'ltr'
}
};
setState({ email: transformedEmail, loading: false, error: null });
onEmailLoaded?.(transformedEmail);
// Mark as read if not already
if (!transformedEmail.flags?.seen) {
if (!transformedEmail.flags?.includes('seen')) {
try {
await fetch(`/api/courrier/${emailId}/mark-read`, {
method: 'POST',
@ -109,12 +105,11 @@ export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {
return;
}
console.error('useEmailFetch: Error fetching email:', err);
console.error('Error fetching email:', err);
const errorMessage = err instanceof Error ? err.message : 'Failed to load email';
setState(prev => ({ ...prev, loading: false, error: errorMessage }));
onError?.(errorMessage);
// Show toast for user feedback
toast({
title: 'Error',
description: errorMessage,