courrier refactor rebuild preview

This commit is contained in:
alma 2025-04-27 00:02:35 +02:00
parent c44ce9d41e
commit ae1087f401
3 changed files with 151 additions and 115 deletions

View File

@ -212,3 +212,102 @@ div[style*="---------- Forwarded message ---------"] {
margin: 0;
}
/* Email display styles */
.email-content-display {
max-width: 100%;
word-wrap: break-word;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5;
}
/* Preserve email structure */
.email-content-display * {
max-width: 100% !important;
}
/* Images */
.email-content-display img {
max-width: 100%;
height: auto;
display: inline-block;
margin: 8px 0;
}
/* Tables */
.email-content-display table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.email-content-display td,
.email-content-display th {
padding: 8px;
border: 1px solid #e5e7eb;
}
/* Buttons */
.email-content-display button,
.email-content-display a[role="button"],
.email-content-display a.button,
.email-content-display div.button,
.email-content-display [class*="btn"],
.email-content-display [class*="button"] {
display: inline-block;
padding: 8px 16px;
background-color: #f97316;
color: white;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
margin: 8px 0;
text-align: center;
cursor: pointer;
border: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Links */
.email-content-display a {
color: #3b82f6;
text-decoration: underline;
}
/* Headers and text */
.email-content-display h1,
.email-content-display h2,
.email-content-display h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.email-content-display p {
margin-bottom: 16px;
}
/* Quote blocks for email replies */
.email-content-display blockquote {
margin: 16px 0;
padding: 8px 16px;
border-left: 3px solid #e5e7eb;
color: #4b5563;
background-color: #f9fafb;
}
/* Support for RTL content */
.email-content-display[dir="rtl"],
.email-content-display [dir="rtl"] {
text-align: right;
}
/* Remove any padding/margins from the first and last elements */
.email-content-display > *:first-child {
margin-top: 0;
}
.email-content-display > *:last-child {
margin-bottom: 0;
}

View File

@ -1,80 +1,45 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Loader2, Paperclip, FileDown, Download } from 'lucide-react';
import { Loader2, Paperclip, Download } from 'lucide-react';
import { sanitizeHtml } from '@/lib/utils/email-formatter';
import { Button } from '@/components/ui/button';
import { Email } from '@/hooks/use-courrier';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
interface EmailAddress {
name: string;
address: string;
}
interface Email {
id: string;
subject: string;
from: EmailAddress[];
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
date: Date | string;
content?: string;
html?: string;
text?: string;
hasAttachments?: boolean;
attachments?: Array<{
filename: string;
contentType: string;
size: number;
path?: string;
content?: string;
}>;
}
interface EmailContentProps {
email: Email;
}
export default function EmailContent({ email }: EmailContentProps) {
const [content, setContent] = useState<React.ReactNode>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!email) return;
const renderContent = async () => {
setIsLoading(true);
setError(null);
try {
if (!email.content || email.content.length === 0) {
setContent(<div className="text-gray-500">Email content is empty</div>);
return;
}
// Use the sanitizer from the centralized formatter
const sanitizedHtml = sanitizeHtml(email.content);
// Look for specific markers that indicate this is a forwarded or replied email
const isForwarded = sanitizedHtml.includes('---------- Forwarded message ---------');
const isReply = sanitizedHtml.includes('class="reply-body"') ||
sanitizedHtml.includes('blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left:');
// For forwarded or replied emails, ensure we keep the exact structure
if (isForwarded || isReply) {
setContent(
<div
className="email-content prose max-w-none preserve-email-formatting"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
dir="rtl"
style={{ textAlign: 'right' }}
/>
);
} else {
// For regular emails, wrap in the same structure used in the compose editor
setContent(
<div
className="email-content prose max-w-none preserve-email-formatting"
dir="rtl"
style={{ textAlign: 'right' }}
>
<div style={{ minHeight: "20px" }} dir="rtl">
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
</div>
</div>
);
}
} catch (err) {
console.error('Error rendering email content:', err);
setError('Error rendering email content. Please try again.');
setContent(null);
} finally {
setIsLoading(false);
}
};
renderContent();
}, [email]);
// Render attachments if they exist
const renderAttachments = () => {
if (!email?.attachments || email.attachments.length === 0) {
@ -102,7 +67,7 @@ export default function EmailContent({ email }: EmailContentProps) {
if (isLoading) {
return (
<div className="flex justify-center items-center h-full p-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
@ -156,7 +121,14 @@ export default function EmailContent({ email }: EmailContentProps) {
</div>
</div>
{content}
{email.content ? (
<div className="email-content-display">
<div dangerouslySetInnerHTML={{ __html: sanitizeHtml(email.content) }} />
</div>
) : (
<p className="text-gray-500">No content available</p>
)}
{renderAttachments()}
</div>
);

View File

@ -1,8 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import DOMPurify from 'isomorphic-dompurify';
import { Loader2, Paperclip, Download } from 'lucide-react';
import { useState } from 'react';
import { Loader2, Paperclip } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
@ -55,49 +54,6 @@ interface EmailPreviewProps {
export default function EmailPreview({ email, loading = false, onReply }: EmailPreviewProps) {
const [contentLoading, setContentLoading] = useState<boolean>(false);
// Handle sanitizing and rendering HTML content
const renderContent = () => {
if (!email?.content) return <p>No content available</p>;
try {
// Use the centralized sanitizeHtml function which preserves direction
const sanitizedContent = sanitizeHtml(email.content);
// Look for specific markers that indicate this is a forwarded or replied email
const isForwarded = sanitizedContent.includes('---------- Forwarded message ---------');
const isReply = sanitizedContent.includes('class="reply-body"') ||
sanitizedContent.includes('blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left:');
// For forwarded or replied emails, ensure we keep the exact structure
if (isForwarded || isReply) {
return (
<div
className="email-content prose max-w-none dark:prose-invert preserve-email-formatting"
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
dir="rtl"
style={{ textAlign: 'right' }}
/>
);
}
// For regular emails, preserve all HTML elements with minimal wrapping
return (
<div
className="email-content prose max-w-none dark:prose-invert preserve-email-formatting"
dir="rtl"
style={{ textAlign: 'right' }}
>
<div style={{ minHeight: "20px" }} dir="rtl">
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
</div>
</div>
);
} catch (error) {
console.error('Error rendering email content:', error);
return <p>Error displaying email content</p>;
}
};
// Format the date
const formatDate = (date: Date | string) => {
if (!date) return '';
@ -124,17 +80,19 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
).join(', ');
};
// Display loading state
if (loading || contentLoading) {
return (
<div className="flex items-center justify-center h-full p-6">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
<p>Loading email content...</p>
<p>Loading email...</p>
</div>
</div>
);
}
// No email selected
if (!email) {
return (
<div className="flex items-center justify-center h-full p-6">
@ -221,10 +179,17 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
</div>
{/* Email content */}
<ScrollArea className="flex-1 p-4">
<div className="email-content-wrapper border rounded-md p-4 mb-4">
{renderContent()}
</div>
<ScrollArea className="flex-1 px-4 py-3">
{email.content ? (
<div
className="email-content-display"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(email.content)
}}
/>
) : (
<p className="text-muted-foreground">No content available</p>
)}
</ScrollArea>
</div>
);