courrier refactor rebuild preview
This commit is contained in:
parent
c44ce9d41e
commit
ae1087f401
@ -212,3 +212,102 @@ div[style*="---------- Forwarded message ---------"] {
|
|||||||
margin: 0;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,80 +1,45 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
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 { 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 { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
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 {
|
interface EmailContentProps {
|
||||||
email: Email;
|
email: Email;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmailContent({ email }: EmailContentProps) {
|
export default function EmailContent({ email }: EmailContentProps) {
|
||||||
const [content, setContent] = useState<React.ReactNode>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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
|
// Render attachments if they exist
|
||||||
const renderAttachments = () => {
|
const renderAttachments = () => {
|
||||||
if (!email?.attachments || email.attachments.length === 0) {
|
if (!email?.attachments || email.attachments.length === 0) {
|
||||||
@ -102,7 +67,7 @@ export default function EmailContent({ email }: EmailContentProps) {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center h-full p-8">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,7 +121,14 @@ export default function EmailContent({ email }: EmailContentProps) {
|
|||||||
</div>
|
</div>
|
||||||
</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()}
|
{renderAttachments()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import { Loader2, Paperclip } from 'lucide-react';
|
||||||
import { Loader2, Paperclip, Download } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
@ -55,49 +54,6 @@ interface EmailPreviewProps {
|
|||||||
export default function EmailPreview({ email, loading = false, onReply }: EmailPreviewProps) {
|
export default function EmailPreview({ email, loading = false, onReply }: EmailPreviewProps) {
|
||||||
const [contentLoading, setContentLoading] = useState<boolean>(false);
|
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
|
// Format the date
|
||||||
const formatDate = (date: Date | string) => {
|
const formatDate = (date: Date | string) => {
|
||||||
if (!date) return '';
|
if (!date) return '';
|
||||||
@ -124,17 +80,19 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
).join(', ');
|
).join(', ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Display loading state
|
||||||
if (loading || contentLoading) {
|
if (loading || contentLoading) {
|
||||||
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">
|
<div className="text-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No email selected
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full p-6">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Email content */}
|
{/* Email content */}
|
||||||
<ScrollArea className="flex-1 p-4">
|
<ScrollArea className="flex-1 px-4 py-3">
|
||||||
<div className="email-content-wrapper border rounded-md p-4 mb-4">
|
{email.content ? (
|
||||||
{renderContent()}
|
<div
|
||||||
</div>
|
className="email-content-display"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: sanitizeHtml(email.content)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">No content available</p>
|
||||||
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user